@hermespilot/link 0.6.2 → 0.6.4
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/{chunk-UBWRPRZ4.js → chunk-FCCY3M5Z.js} +340 -73
- package/dist/cli/index.js +258 -39
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
|
@@ -1183,6 +1183,8 @@ import {
|
|
|
1183
1183
|
stat
|
|
1184
1184
|
} from "fs/promises";
|
|
1185
1185
|
import path2 from "path";
|
|
1186
|
+
var TRANSIENT_RENAME_ERROR_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
1187
|
+
var RENAME_RETRY_DELAYS_MS = [25, 50, 100, 200, 400];
|
|
1186
1188
|
async function atomicWriteFilePreservingMetadata(filePath, value, options = {}) {
|
|
1187
1189
|
const resolvedPath = path2.resolve(filePath);
|
|
1188
1190
|
const directory = path2.dirname(resolvedPath);
|
|
@@ -1214,7 +1216,7 @@ async function atomicWriteFilePreservingMetadata(filePath, value, options = {})
|
|
|
1214
1216
|
await handle.close();
|
|
1215
1217
|
}
|
|
1216
1218
|
await applyMetadata(tempPath, metadata);
|
|
1217
|
-
await
|
|
1219
|
+
await renameWithTransientRetry(tempPath, resolvedPath);
|
|
1218
1220
|
} catch (error) {
|
|
1219
1221
|
await rm(tempPath, { force: true });
|
|
1220
1222
|
throw error;
|
|
@@ -1321,6 +1323,30 @@ async function applyOwner(filePath, metadata) {
|
|
|
1321
1323
|
}
|
|
1322
1324
|
}
|
|
1323
1325
|
}
|
|
1326
|
+
async function renameWithTransientRetry(from, to) {
|
|
1327
|
+
let lastError;
|
|
1328
|
+
for (let attempt = 0; attempt <= RENAME_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
1329
|
+
try {
|
|
1330
|
+
await rename(from, to);
|
|
1331
|
+
return;
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
lastError = error;
|
|
1334
|
+
if (attempt >= RENAME_RETRY_DELAYS_MS.length || !isTransientRenameError(error)) {
|
|
1335
|
+
throw error;
|
|
1336
|
+
}
|
|
1337
|
+
await delay(RENAME_RETRY_DELAYS_MS[attempt]);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
throw lastError;
|
|
1341
|
+
}
|
|
1342
|
+
function isTransientRenameError(error) {
|
|
1343
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && TRANSIENT_RENAME_ERROR_CODES.has(error.code);
|
|
1344
|
+
}
|
|
1345
|
+
function delay(ms) {
|
|
1346
|
+
return new Promise((resolve) => {
|
|
1347
|
+
setTimeout(resolve, ms);
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1324
1350
|
function metadataFromStats(statsValue) {
|
|
1325
1351
|
return {
|
|
1326
1352
|
uid: statsValue.uid,
|
|
@@ -4852,7 +4878,7 @@ import { execFile as execFile2, spawn } from "child_process";
|
|
|
4852
4878
|
import { constants as fsConstants } from "fs";
|
|
4853
4879
|
import { access, readFile as readFile5, realpath, stat as stat4 } from "fs/promises";
|
|
4854
4880
|
import path7 from "path";
|
|
4855
|
-
import { setTimeout as
|
|
4881
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
4856
4882
|
import { promisify as promisify2 } from "util";
|
|
4857
4883
|
|
|
4858
4884
|
// src/runtime/logger.ts
|
|
@@ -4865,7 +4891,7 @@ import os2 from "os";
|
|
|
4865
4891
|
import path5 from "path";
|
|
4866
4892
|
|
|
4867
4893
|
// src/constants.ts
|
|
4868
|
-
var LINK_VERSION = "0.6.
|
|
4894
|
+
var LINK_VERSION = "0.6.4";
|
|
4869
4895
|
var LINK_COMMAND = "hermeslink";
|
|
4870
4896
|
var LINK_DEFAULT_PORT = 52379;
|
|
4871
4897
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -5396,6 +5422,17 @@ var HERMES_GATEWAY_ENV_BLOCKLIST_PREFIXES = ["API_SERVER_"];
|
|
|
5396
5422
|
var gatewayStartInFlightByProfile = /* @__PURE__ */ new Map();
|
|
5397
5423
|
var hermesVersionCache = /* @__PURE__ */ new Map();
|
|
5398
5424
|
var hermesVersionInFlight = /* @__PURE__ */ new Map();
|
|
5425
|
+
var HermesApiServerUnavailableError = class extends LinkHttpError {
|
|
5426
|
+
constructor(details) {
|
|
5427
|
+
super(
|
|
5428
|
+
503,
|
|
5429
|
+
"hermes_api_server_unavailable",
|
|
5430
|
+
buildUnavailableErrorMessage(details)
|
|
5431
|
+
);
|
|
5432
|
+
this.details = details;
|
|
5433
|
+
}
|
|
5434
|
+
details;
|
|
5435
|
+
};
|
|
5399
5436
|
async function ensureHermesApiServerAvailable(options = {}) {
|
|
5400
5437
|
const profileName = normalizeProfileName(options.profileName);
|
|
5401
5438
|
await assertProfileExists(profileName);
|
|
@@ -5426,11 +5463,12 @@ async function ensureHermesApiServerAvailable(options = {}) {
|
|
|
5426
5463
|
reason: "auto_start_disabled",
|
|
5427
5464
|
issue: health.issue ?? "unknown"
|
|
5428
5465
|
});
|
|
5429
|
-
throw new
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5466
|
+
throw new HermesApiServerUnavailableError({
|
|
5467
|
+
profileName,
|
|
5468
|
+
port: configResult.apiServer.port ?? null,
|
|
5469
|
+
health,
|
|
5470
|
+
attemptedStart: false
|
|
5471
|
+
});
|
|
5434
5472
|
}
|
|
5435
5473
|
void options.logger?.info("gateway_auto_start_requested", {
|
|
5436
5474
|
profile: profileName,
|
|
@@ -5504,11 +5542,15 @@ async function ensureHermesApiServerAvailable(options = {}) {
|
|
|
5504
5542
|
issue: health.issue ?? "unknown",
|
|
5505
5543
|
...logHint ? { log_hint: logHint } : {}
|
|
5506
5544
|
});
|
|
5507
|
-
throw new
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5545
|
+
throw new HermesApiServerUnavailableError({
|
|
5546
|
+
profileName,
|
|
5547
|
+
port: configResult.apiServer.port ?? null,
|
|
5548
|
+
health,
|
|
5549
|
+
attemptedStart: true,
|
|
5550
|
+
startCommand: formatHermesGatewayRunCommand(profileName),
|
|
5551
|
+
gatewayLog: start.logPath,
|
|
5552
|
+
logHint: logHint || void 0
|
|
5553
|
+
});
|
|
5512
5554
|
}
|
|
5513
5555
|
async function reloadHermesGateway(options = {}) {
|
|
5514
5556
|
const profileName = normalizeProfileName(options.profileName);
|
|
@@ -5993,7 +6035,7 @@ async function waitForHermesApiHealth(config, fetcher, timeoutMs) {
|
|
|
5993
6035
|
const deadline = Date.now() + timeoutMs;
|
|
5994
6036
|
let health = await readHermesApiServerHealth(config, fetcher);
|
|
5995
6037
|
while (!health.healthy && !health.terminal && Date.now() < deadline) {
|
|
5996
|
-
await
|
|
6038
|
+
await delay2(400);
|
|
5997
6039
|
health = await readHermesApiServerHealth(config, fetcher);
|
|
5998
6040
|
}
|
|
5999
6041
|
return health;
|
|
@@ -6012,31 +6054,37 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
|
|
|
6012
6054
|
const payload = await detailed.json().catch(() => null);
|
|
6013
6055
|
const record = toRecord2(payload);
|
|
6014
6056
|
if (!isHermesApiServerHealthPayload(record, { allowStatusOnly: false })) {
|
|
6057
|
+
const issue3 = createHealthIssue("not_hermes_response", {
|
|
6058
|
+
probe: "GET /health/detailed"
|
|
6059
|
+
});
|
|
6015
6060
|
return {
|
|
6016
6061
|
healthy: false,
|
|
6017
6062
|
terminal: true,
|
|
6018
|
-
|
|
6063
|
+
...healthIssueFields(issue3),
|
|
6019
6064
|
detailed: record
|
|
6020
6065
|
};
|
|
6021
6066
|
}
|
|
6022
|
-
const
|
|
6067
|
+
const issue2 = describeDetailedHealthIssue(record);
|
|
6023
6068
|
const terminal = isTerminalDetailedHealth(record);
|
|
6024
|
-
const authIssue =
|
|
6069
|
+
const authIssue = issue2 ? null : await probeHermesApiServerAuth(resolvedConfig, fetcher);
|
|
6025
6070
|
if (authIssue) {
|
|
6026
6071
|
return authIssue;
|
|
6027
6072
|
}
|
|
6028
6073
|
return {
|
|
6029
|
-
healthy: !
|
|
6074
|
+
healthy: !issue2,
|
|
6030
6075
|
terminal,
|
|
6031
|
-
...
|
|
6076
|
+
...issue2 ? healthIssueFields(issue2) : {},
|
|
6032
6077
|
detailed: record
|
|
6033
6078
|
};
|
|
6034
6079
|
}
|
|
6035
6080
|
if (detailed?.status === 401) {
|
|
6081
|
+
const issue2 = createHealthIssue("auth_invalid", {
|
|
6082
|
+
probe: "GET /health/detailed"
|
|
6083
|
+
});
|
|
6036
6084
|
return {
|
|
6037
6085
|
healthy: false,
|
|
6038
6086
|
authInvalid: true,
|
|
6039
|
-
|
|
6087
|
+
...healthIssueFields(issue2)
|
|
6040
6088
|
};
|
|
6041
6089
|
}
|
|
6042
6090
|
const basic = await fetchWithTimeout(
|
|
@@ -6053,10 +6101,13 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
|
|
|
6053
6101
|
if (!isHermesApiServerHealthPayload(record, {
|
|
6054
6102
|
allowStatusOnly: true
|
|
6055
6103
|
})) {
|
|
6104
|
+
const issue2 = createHealthIssue("not_hermes_response", {
|
|
6105
|
+
probe: "GET /health"
|
|
6106
|
+
});
|
|
6056
6107
|
return {
|
|
6057
6108
|
healthy: false,
|
|
6058
6109
|
terminal: true,
|
|
6059
|
-
|
|
6110
|
+
...healthIssueFields(issue2),
|
|
6060
6111
|
detailed: record
|
|
6061
6112
|
};
|
|
6062
6113
|
}
|
|
@@ -6067,15 +6118,22 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
|
|
|
6067
6118
|
return { healthy: true };
|
|
6068
6119
|
}
|
|
6069
6120
|
if (basic?.status === 401) {
|
|
6121
|
+
const issue2 = createHealthIssue("auth_invalid", {
|
|
6122
|
+
probe: "GET /health"
|
|
6123
|
+
});
|
|
6070
6124
|
return {
|
|
6071
6125
|
healthy: false,
|
|
6072
6126
|
authInvalid: true,
|
|
6073
|
-
|
|
6127
|
+
...healthIssueFields(issue2)
|
|
6074
6128
|
};
|
|
6075
6129
|
}
|
|
6130
|
+
const issue = createHealthIssue("port_unreachable", {
|
|
6131
|
+
port: resolvedConfig.port,
|
|
6132
|
+
probe: "GET /health/detailed, GET /health"
|
|
6133
|
+
});
|
|
6076
6134
|
return {
|
|
6077
6135
|
healthy: false,
|
|
6078
|
-
issue
|
|
6136
|
+
...healthIssueFields(issue)
|
|
6079
6137
|
};
|
|
6080
6138
|
}
|
|
6081
6139
|
async function probeHermesApiServerAuth(config, fetcher) {
|
|
@@ -6088,22 +6146,32 @@ async function probeHermesApiServerAuth(config, fetcher) {
|
|
|
6088
6146
|
fetcher
|
|
6089
6147
|
);
|
|
6090
6148
|
if (!response) {
|
|
6149
|
+
const issue = createHealthIssue("models_probe_timeout", {
|
|
6150
|
+
probe: "GET /v1/models"
|
|
6151
|
+
});
|
|
6091
6152
|
return {
|
|
6092
6153
|
healthy: false,
|
|
6093
|
-
issue
|
|
6154
|
+
...healthIssueFields(issue)
|
|
6094
6155
|
};
|
|
6095
6156
|
}
|
|
6096
6157
|
if (response?.status === 401) {
|
|
6158
|
+
const issue = createHealthIssue("auth_invalid", {
|
|
6159
|
+
probe: "GET /v1/models"
|
|
6160
|
+
});
|
|
6097
6161
|
return {
|
|
6098
6162
|
healthy: false,
|
|
6099
6163
|
authInvalid: true,
|
|
6100
|
-
issue
|
|
6164
|
+
...healthIssueFields(issue)
|
|
6101
6165
|
};
|
|
6102
6166
|
}
|
|
6103
6167
|
if (response && !response.ok) {
|
|
6168
|
+
const issue = createHealthIssue("models_probe_http", {
|
|
6169
|
+
status: response.status,
|
|
6170
|
+
probe: "GET /v1/models"
|
|
6171
|
+
});
|
|
6104
6172
|
return {
|
|
6105
6173
|
healthy: false,
|
|
6106
|
-
issue
|
|
6174
|
+
...healthIssueFields(issue)
|
|
6107
6175
|
};
|
|
6108
6176
|
}
|
|
6109
6177
|
return null;
|
|
@@ -6129,14 +6197,92 @@ function shouldAutoStart(value) {
|
|
|
6129
6197
|
const raw = process.env.HERMESLINK_GATEWAY_AUTOSTART?.trim().toLowerCase();
|
|
6130
6198
|
return raw !== "0" && raw !== "false" && raw !== "off";
|
|
6131
6199
|
}
|
|
6132
|
-
function unavailableMessage() {
|
|
6133
|
-
return "Hermes API Server \u5F53\u524D\u4E0D\u53EF\u7528\u3002\u8BF7\u786E\u8BA4 Hermes Agent \u5DF2\u5B89\u88C5\uFF0C\u5E76\u53EF\u901A\u8FC7 `hermes gateway run` \u542F\u52A8\uFF1BHermesPilot Link \u9700\u8981 Hermes Agent \u7684\u672C\u673A API Server\u3002";
|
|
6134
|
-
}
|
|
6135
6200
|
function isHermesApiServerHealthPayload(payload, options) {
|
|
6136
6201
|
return payload.platform === "hermes-agent" || options.allowStatusOnly && (payload.status === "ok" || payload.ok === true);
|
|
6137
6202
|
}
|
|
6138
|
-
function
|
|
6139
|
-
|
|
6203
|
+
function buildUnavailableErrorMessage(details) {
|
|
6204
|
+
const reason = details.health.issue ?? "unknown reason";
|
|
6205
|
+
return `Hermes API Server is unavailable: ${reason}`;
|
|
6206
|
+
}
|
|
6207
|
+
function createHealthIssue(code, values = {}) {
|
|
6208
|
+
switch (code) {
|
|
6209
|
+
case "not_hermes_response":
|
|
6210
|
+
return {
|
|
6211
|
+
code,
|
|
6212
|
+
...values.probe !== void 0 ? { probe: values.probe } : {},
|
|
6213
|
+
message: "The configured port returned a non-Hermes API Server health response. Another local service may be using this port."
|
|
6214
|
+
};
|
|
6215
|
+
case "auth_invalid":
|
|
6216
|
+
return {
|
|
6217
|
+
code,
|
|
6218
|
+
...values.probe !== void 0 ? { probe: values.probe } : {},
|
|
6219
|
+
message: "Hermes API Server returned 401. The configured API key may be wrong, or this port may belong to another Profile."
|
|
6220
|
+
};
|
|
6221
|
+
case "models_probe_timeout":
|
|
6222
|
+
return {
|
|
6223
|
+
code,
|
|
6224
|
+
...values.probe !== void 0 ? { probe: values.probe } : {},
|
|
6225
|
+
message: "Hermes API Server did not respond to /v1/models before the timeout."
|
|
6226
|
+
};
|
|
6227
|
+
case "models_probe_http":
|
|
6228
|
+
return {
|
|
6229
|
+
code,
|
|
6230
|
+
...values.status !== void 0 ? { status: values.status } : {},
|
|
6231
|
+
...values.probe !== void 0 ? { probe: values.probe } : {},
|
|
6232
|
+
message: `Hermes API Server /v1/models returned HTTP ${values.status ?? "unknown"}.`
|
|
6233
|
+
};
|
|
6234
|
+
case "port_unreachable":
|
|
6235
|
+
return {
|
|
6236
|
+
code,
|
|
6237
|
+
...values.port !== void 0 ? { port: values.port } : {},
|
|
6238
|
+
...values.probe !== void 0 ? { probe: values.probe } : {},
|
|
6239
|
+
message: values.port ? `Hermes API Server port ${values.port} did not respond.` : "Hermes API Server port did not respond."
|
|
6240
|
+
};
|
|
6241
|
+
case "gateway_state":
|
|
6242
|
+
return {
|
|
6243
|
+
code,
|
|
6244
|
+
...values.state !== void 0 ? { state: values.state } : {},
|
|
6245
|
+
...values.probe !== void 0 ? { probe: values.probe } : {},
|
|
6246
|
+
message: `Hermes Gateway state is ${values.state ?? "unknown"}.`
|
|
6247
|
+
};
|
|
6248
|
+
case "api_server_state":
|
|
6249
|
+
return {
|
|
6250
|
+
code,
|
|
6251
|
+
...values.state !== void 0 ? { state: values.state } : {},
|
|
6252
|
+
...values.detail !== void 0 ? { detail: values.detail } : {},
|
|
6253
|
+
...values.probe !== void 0 ? { probe: values.probe } : {},
|
|
6254
|
+
message: `Hermes API Server state is ${values.state ?? "unknown"}${values.detail ? `: ${values.detail}` : ""}.`
|
|
6255
|
+
};
|
|
6256
|
+
case "exit_reason":
|
|
6257
|
+
return {
|
|
6258
|
+
code,
|
|
6259
|
+
...values.detail !== void 0 ? { detail: values.detail } : {},
|
|
6260
|
+
...values.probe !== void 0 ? { probe: values.probe } : {},
|
|
6261
|
+
message: `Hermes Gateway exit reason: ${values.detail ?? "unknown"}.`
|
|
6262
|
+
};
|
|
6263
|
+
}
|
|
6264
|
+
}
|
|
6265
|
+
function healthIssueFields(issue) {
|
|
6266
|
+
const fields = {
|
|
6267
|
+
issue: issue.message,
|
|
6268
|
+
issueCode: issue.code
|
|
6269
|
+
};
|
|
6270
|
+
if (issue.port !== void 0) {
|
|
6271
|
+
fields.issuePort = issue.port;
|
|
6272
|
+
}
|
|
6273
|
+
if (issue.status !== void 0) {
|
|
6274
|
+
fields.issueStatus = issue.status;
|
|
6275
|
+
}
|
|
6276
|
+
if (issue.state !== void 0) {
|
|
6277
|
+
fields.issueState = issue.state;
|
|
6278
|
+
}
|
|
6279
|
+
if (issue.detail !== void 0) {
|
|
6280
|
+
fields.issueDetail = issue.detail;
|
|
6281
|
+
}
|
|
6282
|
+
if (issue.probe !== void 0) {
|
|
6283
|
+
fields.issueProbe = issue.probe;
|
|
6284
|
+
}
|
|
6285
|
+
return fields;
|
|
6140
6286
|
}
|
|
6141
6287
|
function describeDetailedHealthIssue(payload) {
|
|
6142
6288
|
const gatewayState = readString4(payload, "gateway_state");
|
|
@@ -6145,22 +6291,26 @@ function describeDetailedHealthIssue(payload) {
|
|
|
6145
6291
|
const apiServer = toRecord2(platforms.api_server);
|
|
6146
6292
|
const apiServerState = readString4(apiServer, "state");
|
|
6147
6293
|
const apiServerError = readString4(apiServer, "error_message");
|
|
6148
|
-
const platformIssue = firstFatalPlatformIssue(platforms);
|
|
6149
|
-
const parts = [];
|
|
6150
6294
|
if (gatewayState && gatewayState !== "running") {
|
|
6151
|
-
|
|
6295
|
+
return createHealthIssue("gateway_state", {
|
|
6296
|
+
state: gatewayState,
|
|
6297
|
+
probe: "GET /health/detailed"
|
|
6298
|
+
});
|
|
6152
6299
|
}
|
|
6153
6300
|
if (apiServerState && !isHealthyPlatformState(apiServerState)) {
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6301
|
+
return createHealthIssue("api_server_state", {
|
|
6302
|
+
state: apiServerState,
|
|
6303
|
+
...apiServerError ? { detail: apiServerError } : {},
|
|
6304
|
+
probe: "GET /health/detailed"
|
|
6305
|
+
});
|
|
6157
6306
|
}
|
|
6158
|
-
if (exitReason) {
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6307
|
+
if (exitReason && gatewayState !== "running") {
|
|
6308
|
+
return createHealthIssue("exit_reason", {
|
|
6309
|
+
detail: exitReason,
|
|
6310
|
+
probe: "GET /health/detailed"
|
|
6311
|
+
});
|
|
6162
6312
|
}
|
|
6163
|
-
return
|
|
6313
|
+
return null;
|
|
6164
6314
|
}
|
|
6165
6315
|
function isTerminalDetailedHealth(payload) {
|
|
6166
6316
|
const gatewayState = readString4(payload, "gateway_state");
|
|
@@ -6168,18 +6318,6 @@ function isTerminalDetailedHealth(payload) {
|
|
|
6168
6318
|
const apiServerState = readString4(toRecord2(platforms.api_server), "state");
|
|
6169
6319
|
return gatewayState === "startup_failed" || apiServerState === "fatal" || apiServerState === "failed";
|
|
6170
6320
|
}
|
|
6171
|
-
function firstFatalPlatformIssue(platforms) {
|
|
6172
|
-
for (const [name, value] of Object.entries(platforms)) {
|
|
6173
|
-
const platform = toRecord2(value);
|
|
6174
|
-
const state = readString4(platform, "state");
|
|
6175
|
-
if (state !== "fatal" && state !== "failed") {
|
|
6176
|
-
continue;
|
|
6177
|
-
}
|
|
6178
|
-
const message = readString4(platform, "error_message");
|
|
6179
|
-
return `${name} \u72B6\u6001\u662F ${state}${message ? `\uFF1A${message}` : ""}`;
|
|
6180
|
-
}
|
|
6181
|
-
return null;
|
|
6182
|
-
}
|
|
6183
6321
|
function isHealthyPlatformState(value) {
|
|
6184
6322
|
return ["connected", "running", "ready", "ok", "healthy"].includes(
|
|
6185
6323
|
value.toLowerCase()
|
|
@@ -9168,12 +9306,12 @@ var ConversationMetadataCoordinator = class {
|
|
|
9168
9306
|
return { ...next, last_event_seq: event.seq, updated_at: event.created_at };
|
|
9169
9307
|
}
|
|
9170
9308
|
scheduleGeneratedTitleRefresh(conversationId) {
|
|
9171
|
-
for (const
|
|
9309
|
+
for (const delay4 of GENERATED_TITLE_RETRY_DELAYS_MS) {
|
|
9172
9310
|
setTimeout(() => {
|
|
9173
9311
|
void this.generateTitleFromFirstRound(conversationId).catch(
|
|
9174
9312
|
() => void 0
|
|
9175
9313
|
);
|
|
9176
|
-
},
|
|
9314
|
+
}, delay4);
|
|
9177
9315
|
}
|
|
9178
9316
|
}
|
|
9179
9317
|
async renameConversation(conversationId, title, input) {
|
|
@@ -14742,6 +14880,17 @@ async function buildRunTranscriptEvents(input) {
|
|
|
14742
14880
|
}
|
|
14743
14881
|
return events;
|
|
14744
14882
|
}
|
|
14883
|
+
async function readRunFinalAssistantText(input) {
|
|
14884
|
+
const sessionId = input.hermesSessionId.trim();
|
|
14885
|
+
if (!sessionId) {
|
|
14886
|
+
return null;
|
|
14887
|
+
}
|
|
14888
|
+
const profileName = isValidProfileName3(input.profileName) ? input.profileName : "default";
|
|
14889
|
+
const rows = await readHermesTranscriptRows(profileName, sessionId);
|
|
14890
|
+
const lowerBoundSeconds = runStartedLowerBoundSeconds(input.runStartedAt);
|
|
14891
|
+
const assistantRows = rows.filter((row) => isRunRow(row, lowerBoundSeconds)).filter((row) => normalizeRole(row.role) === "assistant").map((row) => normalizeContent3(row.content).trim()).filter(Boolean);
|
|
14892
|
+
return assistantRows.at(-1) ?? null;
|
|
14893
|
+
}
|
|
14745
14894
|
async function readHermesTranscriptRows(profileName, sessionId) {
|
|
14746
14895
|
const profileDir = resolveHermesProfileDir(profileName);
|
|
14747
14896
|
const dbPath = path19.join(profileDir, "state.db");
|
|
@@ -15648,6 +15797,9 @@ function toRecord12(value) {
|
|
|
15648
15797
|
var RUN_STATUS_RECOVERY_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
15649
15798
|
var RUN_STATUS_RECOVERY_INITIAL_DELAY_MS = 500;
|
|
15650
15799
|
var RUN_STATUS_RECOVERY_MAX_DELAY_MS = 2500;
|
|
15800
|
+
var RESPONSE_TRANSCRIPT_RECOVERY_TIMEOUT_MS = 3e4;
|
|
15801
|
+
var RESPONSE_TRANSCRIPT_RECOVERY_INITIAL_DELAY_MS = 750;
|
|
15802
|
+
var RESPONSE_TRANSCRIPT_RECOVERY_MAX_DELAY_MS = 2500;
|
|
15651
15803
|
var ConversationRunLifecycle = class {
|
|
15652
15804
|
constructor(deps) {
|
|
15653
15805
|
this.deps = deps;
|
|
@@ -16025,10 +16177,31 @@ var ConversationRunLifecycle = class {
|
|
|
16025
16177
|
await this.cancelRunAfterAbort(input.conversationId, input.runId);
|
|
16026
16178
|
return;
|
|
16027
16179
|
}
|
|
16028
|
-
|
|
16180
|
+
let hasAssistantOutput = await this.runHasAssistantOutput(
|
|
16029
16181
|
input.conversationId,
|
|
16030
16182
|
input.runId
|
|
16031
16183
|
);
|
|
16184
|
+
if (input.backend === "responses" && !hasAssistantOutput) {
|
|
16185
|
+
const recoveredEvent2 = await this.recoverResponseRunFromHermesTranscript({
|
|
16186
|
+
conversationId: input.conversationId,
|
|
16187
|
+
runId: input.runId,
|
|
16188
|
+
profileName: input.profileName,
|
|
16189
|
+
signal: input.controller.signal
|
|
16190
|
+
});
|
|
16191
|
+
if (recoveredEvent2) {
|
|
16192
|
+
await this.completeRun(input.conversationId, input.runId, recoveredEvent2);
|
|
16193
|
+
this.deps.scheduleTitleRefresh(input.conversationId);
|
|
16194
|
+
return;
|
|
16195
|
+
}
|
|
16196
|
+
if (input.controller.signal.aborted) {
|
|
16197
|
+
await this.cancelRunAfterAbort(input.conversationId, input.runId);
|
|
16198
|
+
return;
|
|
16199
|
+
}
|
|
16200
|
+
hasAssistantOutput = await this.runHasAssistantOutput(
|
|
16201
|
+
input.conversationId,
|
|
16202
|
+
input.runId
|
|
16203
|
+
);
|
|
16204
|
+
}
|
|
16032
16205
|
if (input.backend === "responses" && !streamError && hasAssistantOutput) {
|
|
16033
16206
|
await this.completeRun(input.conversationId, input.runId);
|
|
16034
16207
|
} else {
|
|
@@ -16302,6 +16475,65 @@ var ConversationRunLifecycle = class {
|
|
|
16302
16475
|
await this.persistHermesEvent(input.conversationId, input.runId, event);
|
|
16303
16476
|
}
|
|
16304
16477
|
}
|
|
16478
|
+
async recoverResponseRunFromHermesTranscript(input) {
|
|
16479
|
+
const deadline = Date.now() + RESPONSE_TRANSCRIPT_RECOVERY_TIMEOUT_MS;
|
|
16480
|
+
let delayMs = RESPONSE_TRANSCRIPT_RECOVERY_INITIAL_DELAY_MS;
|
|
16481
|
+
await this.deps.logger.warn("hermes_response_transcript_recovery_started", {
|
|
16482
|
+
conversation_id: input.conversationId,
|
|
16483
|
+
run_id: input.runId,
|
|
16484
|
+
timeout_ms: RESPONSE_TRANSCRIPT_RECOVERY_TIMEOUT_MS
|
|
16485
|
+
});
|
|
16486
|
+
while (!input.signal.aborted && Date.now() < deadline) {
|
|
16487
|
+
if (!await this.deps.isConversationRunnable(input.conversationId)) {
|
|
16488
|
+
return null;
|
|
16489
|
+
}
|
|
16490
|
+
const snapshot = await this.deps.readSnapshot(input.conversationId).catch(() => null);
|
|
16491
|
+
const run = snapshot?.runs.find((item) => item.id === input.runId);
|
|
16492
|
+
if (!run || run.status !== "running") {
|
|
16493
|
+
return null;
|
|
16494
|
+
}
|
|
16495
|
+
const recoveredText = await readRunFinalAssistantText({
|
|
16496
|
+
profileName: input.profileName,
|
|
16497
|
+
hermesSessionId: run.hermes_session_id,
|
|
16498
|
+
runStartedAt: run.started_at
|
|
16499
|
+
}).catch(async (error) => {
|
|
16500
|
+
await this.deps.logger.warn(
|
|
16501
|
+
"hermes_response_transcript_recovery_read_failed",
|
|
16502
|
+
{
|
|
16503
|
+
conversation_id: input.conversationId,
|
|
16504
|
+
run_id: input.runId,
|
|
16505
|
+
hermes_session_id: run.hermes_session_id,
|
|
16506
|
+
error: error instanceof Error ? error.message : String(error)
|
|
16507
|
+
}
|
|
16508
|
+
);
|
|
16509
|
+
return null;
|
|
16510
|
+
});
|
|
16511
|
+
if (recoveredText?.trim()) {
|
|
16512
|
+
const deltaEvent = transcriptRecoveryDeltaEvent(
|
|
16513
|
+
run.hermes_session_id,
|
|
16514
|
+
recoveredText
|
|
16515
|
+
);
|
|
16516
|
+
await this.persistHermesEvent(
|
|
16517
|
+
input.conversationId,
|
|
16518
|
+
input.runId,
|
|
16519
|
+
deltaEvent
|
|
16520
|
+
);
|
|
16521
|
+
await this.deps.logger.info("hermes_response_transcript_recovered", {
|
|
16522
|
+
conversation_id: input.conversationId,
|
|
16523
|
+
run_id: input.runId,
|
|
16524
|
+
hermes_session_id: run.hermes_session_id,
|
|
16525
|
+
content_length: recoveredText.length
|
|
16526
|
+
});
|
|
16527
|
+
return transcriptRecoveryCompletedEvent(run.hermes_session_id);
|
|
16528
|
+
}
|
|
16529
|
+
await sleep(Math.min(delayMs, deadline - Date.now()), input.signal);
|
|
16530
|
+
delayMs = Math.min(
|
|
16531
|
+
RESPONSE_TRANSCRIPT_RECOVERY_MAX_DELAY_MS,
|
|
16532
|
+
delayMs + 500
|
|
16533
|
+
);
|
|
16534
|
+
}
|
|
16535
|
+
return null;
|
|
16536
|
+
}
|
|
16305
16537
|
async resolveRunInput(input) {
|
|
16306
16538
|
const userMessage = input.snapshot.messages.find(
|
|
16307
16539
|
(message) => message.id === input.run.trigger_message_id
|
|
@@ -17236,6 +17468,39 @@ function normalizeRunProfileForCompare(profileName) {
|
|
|
17236
17468
|
return null;
|
|
17237
17469
|
}
|
|
17238
17470
|
}
|
|
17471
|
+
function transcriptRecoveryDeltaEvent(hermesSessionId, text) {
|
|
17472
|
+
return {
|
|
17473
|
+
eventName: "message.delta",
|
|
17474
|
+
payloadType: "message.delta",
|
|
17475
|
+
payload: {
|
|
17476
|
+
type: "message.delta",
|
|
17477
|
+
delta: text,
|
|
17478
|
+
recovered_from: "hermes_transcript",
|
|
17479
|
+
hermes_session_id: hermesSessionId
|
|
17480
|
+
},
|
|
17481
|
+
rawPayload: {
|
|
17482
|
+
format: "hermes-transcript-recovery",
|
|
17483
|
+
hermes_session_id: hermesSessionId,
|
|
17484
|
+
content: text
|
|
17485
|
+
}
|
|
17486
|
+
};
|
|
17487
|
+
}
|
|
17488
|
+
function transcriptRecoveryCompletedEvent(hermesSessionId) {
|
|
17489
|
+
return {
|
|
17490
|
+
eventName: "run.completed",
|
|
17491
|
+
payloadType: "run.completed",
|
|
17492
|
+
payload: {
|
|
17493
|
+
type: "run.completed",
|
|
17494
|
+
recovered_from: "hermes_transcript",
|
|
17495
|
+
hermes_session_id: hermesSessionId
|
|
17496
|
+
},
|
|
17497
|
+
rawPayload: {
|
|
17498
|
+
format: "hermes-transcript-recovery",
|
|
17499
|
+
hermes_session_id: hermesSessionId,
|
|
17500
|
+
type: "run.completed"
|
|
17501
|
+
}
|
|
17502
|
+
};
|
|
17503
|
+
}
|
|
17239
17504
|
function readResponseId(payload) {
|
|
17240
17505
|
if (!payload) {
|
|
17241
17506
|
return null;
|
|
@@ -19600,7 +19865,7 @@ function isExpectedClientDisconnectError2(error, options = {}) {
|
|
|
19600
19865
|
import { execFile as execFile4 } from "child_process";
|
|
19601
19866
|
import { readdir as readdir9, readFile as readFile13, rename as rename3, stat as stat13 } from "fs/promises";
|
|
19602
19867
|
import path20 from "path";
|
|
19603
|
-
import { setTimeout as
|
|
19868
|
+
import { setTimeout as delay3 } from "timers/promises";
|
|
19604
19869
|
import { promisify as promisify4 } from "util";
|
|
19605
19870
|
import YAML2 from "yaml";
|
|
19606
19871
|
var DEFAULT_PROFILE = "default";
|
|
@@ -19861,7 +20126,7 @@ async function waitForProcessesToExit(pids, timeoutMs) {
|
|
|
19861
20126
|
const deadline = Date.now() + timeoutMs;
|
|
19862
20127
|
let remaining = pids.filter(isProcessRunning);
|
|
19863
20128
|
while (remaining.length > 0 && Date.now() < deadline) {
|
|
19864
|
-
await
|
|
20129
|
+
await delay3(100);
|
|
19865
20130
|
remaining = remaining.filter(isProcessRunning);
|
|
19866
20131
|
}
|
|
19867
20132
|
return remaining;
|
|
@@ -19880,7 +20145,7 @@ async function waitForProfilePathToRemainAbsent(profilePath) {
|
|
|
19880
20145
|
if (await pathExists(profilePath)) {
|
|
19881
20146
|
return false;
|
|
19882
20147
|
}
|
|
19883
|
-
await
|
|
20148
|
+
await delay3(PROFILE_DELETE_VERIFY_INTERVAL_MS);
|
|
19884
20149
|
}
|
|
19885
20150
|
return !await pathExists(profilePath);
|
|
19886
20151
|
}
|
|
@@ -25352,12 +25617,12 @@ function connectRelayControl(options) {
|
|
|
25352
25617
|
onUpdate: options.onStreamBatchPolicy
|
|
25353
25618
|
};
|
|
25354
25619
|
const startConnect = () => {
|
|
25355
|
-
void waitForPersistedCooldown().then((
|
|
25620
|
+
void waitForPersistedCooldown().then((delay4) => {
|
|
25356
25621
|
if (closedByUser) {
|
|
25357
25622
|
return;
|
|
25358
25623
|
}
|
|
25359
|
-
if (
|
|
25360
|
-
scheduleTimer(
|
|
25624
|
+
if (delay4 > 0) {
|
|
25625
|
+
scheduleTimer(delay4, "cooldown", `Relay reconnect cooldown active for ${delay4}ms`);
|
|
25361
25626
|
return;
|
|
25362
25627
|
}
|
|
25363
25628
|
connect();
|
|
@@ -25466,8 +25731,8 @@ function connectRelayControl(options) {
|
|
|
25466
25731
|
}
|
|
25467
25732
|
if (recorded.cooldownUntilMs !== null) {
|
|
25468
25733
|
reconnectAttempts = 0;
|
|
25469
|
-
const
|
|
25470
|
-
scheduleTimer(
|
|
25734
|
+
const delay5 = Math.max(0, recorded.cooldownUntilMs - Date.now());
|
|
25735
|
+
scheduleTimer(delay5, "cooldown", `Relay reconnect storm guard active for ${delay5}ms`);
|
|
25471
25736
|
return;
|
|
25472
25737
|
}
|
|
25473
25738
|
reconnectAttempts += 1;
|
|
@@ -25475,15 +25740,15 @@ function connectRelayControl(options) {
|
|
|
25475
25740
|
baseMs: backoffBaseMs,
|
|
25476
25741
|
maxMs: backoffMaxMs
|
|
25477
25742
|
});
|
|
25478
|
-
const
|
|
25479
|
-
scheduleTimer(
|
|
25743
|
+
const delay4 = Math.max(backoffMs, relayRetryAfterMs ?? 0);
|
|
25744
|
+
scheduleTimer(delay4, "retrying", `Retrying in ${delay4}ms`);
|
|
25480
25745
|
}
|
|
25481
25746
|
async function waitForPersistedCooldown() {
|
|
25482
25747
|
return await readRelayCooldownDelayMs(paths).catch(() => 0);
|
|
25483
25748
|
}
|
|
25484
|
-
function scheduleTimer(
|
|
25749
|
+
function scheduleTimer(delay4, state, message) {
|
|
25485
25750
|
options.onStatus?.({ state, attempt: reconnectAttempts, message });
|
|
25486
|
-
retryTimer = setTimeout(connect,
|
|
25751
|
+
retryTimer = setTimeout(connect, delay4);
|
|
25487
25752
|
retryTimer.unref?.();
|
|
25488
25753
|
}
|
|
25489
25754
|
return {
|
|
@@ -27723,13 +27988,14 @@ async function buildOfficialInstallCommand(options, targetVersion) {
|
|
|
27723
27988
|
HERMESLINK_YES: "1",
|
|
27724
27989
|
HERMESLINK_NO_PROFILE_EDIT: "1",
|
|
27725
27990
|
HERMESLINK_NO_PATH_PROMPT: "1",
|
|
27991
|
+
HERMESLINK_SKIP_DOCTOR: "1",
|
|
27726
27992
|
HERMESLINK_SKIP_RESTART: "1"
|
|
27727
27993
|
};
|
|
27728
27994
|
if (process.platform === "win32") {
|
|
27729
27995
|
const windowsCommand = `& { $ErrorActionPreference = "Stop"; Invoke-RestMethod ${quotePowerShellString(installer.windowsUrl)} | Invoke-Expression }`;
|
|
27730
27996
|
return {
|
|
27731
27997
|
command: windowsCommand,
|
|
27732
|
-
displayCommand: `$env:HERMESLINK_VERSION="${targetVersion}"; $env:HERMESLINK_YES="1"; $env:HERMESLINK_NO_PATH_PROMPT="1"; $env:HERMESLINK_SKIP_RESTART="1"; ${windowsCommand}; hermeslink restart`,
|
|
27998
|
+
displayCommand: `$env:HERMESLINK_VERSION="${targetVersion}"; $env:HERMESLINK_YES="1"; $env:HERMESLINK_NO_PATH_PROMPT="1"; $env:HERMESLINK_SKIP_DOCTOR="1"; $env:HERMESLINK_SKIP_RESTART="1"; ${windowsCommand}; hermeslink restart`,
|
|
27733
27999
|
env,
|
|
27734
28000
|
source: "official-installer",
|
|
27735
28001
|
installerUrl: installer.windowsUrl
|
|
@@ -27738,7 +28004,7 @@ async function buildOfficialInstallCommand(options, targetVersion) {
|
|
|
27738
28004
|
const unixCommand = buildUnixInstallCommand(installer.unixUrl);
|
|
27739
28005
|
return {
|
|
27740
28006
|
command: unixCommand,
|
|
27741
|
-
displayCommand: `HERMESLINK_VERSION=${targetVersion} HERMESLINK_YES=1 HERMESLINK_NO_PROFILE_EDIT=1 HERMESLINK_NO_PATH_PROMPT=1 HERMESLINK_SKIP_RESTART=1 sh -c ${quoteShellToken(unixCommand)} && hermeslink restart`,
|
|
28007
|
+
displayCommand: `HERMESLINK_VERSION=${targetVersion} HERMESLINK_YES=1 HERMESLINK_NO_PROFILE_EDIT=1 HERMESLINK_NO_PATH_PROMPT=1 HERMESLINK_SKIP_DOCTOR=1 HERMESLINK_SKIP_RESTART=1 sh -c ${quoteShellToken(unixCommand)} && hermeslink restart`,
|
|
27742
28008
|
env,
|
|
27743
28009
|
source: "official-installer",
|
|
27744
28010
|
installerUrl: installer.unixUrl
|
|
@@ -27920,8 +28186,9 @@ function buildWindowsDetachedUpdaterScript(input) {
|
|
|
27920
28186
|
" $env:HERMESLINK_VERSION = $TargetVersion",
|
|
27921
28187
|
' $env:HERMESLINK_YES = "1"',
|
|
27922
28188
|
' $env:HERMESLINK_NO_PATH_PROMPT = "1"',
|
|
28189
|
+
' $env:HERMESLINK_SKIP_DOCTOR = "1"',
|
|
27923
28190
|
' $env:HERMESLINK_SKIP_RESTART = "1"',
|
|
27924
|
-
' Invoke-Step "Installing Hermes Link $TargetVersion" { & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $InstallerPath -Version $TargetVersion -NoPathPrompt -SkipRestart } | Out-Null',
|
|
28191
|
+
' Invoke-Step "Installing Hermes Link $TargetVersion" { & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $InstallerPath -Version $TargetVersion -NoPathPrompt -SkipDoctor -SkipRestart } | Out-Null',
|
|
27925
28192
|
' Invoke-Step "Starting Hermes Link after update" { & $NodePath $CliScriptPath start } | Out-Null',
|
|
27926
28193
|
' Add-UpdateLog ""',
|
|
27927
28194
|
' Add-UpdateLog ("=== link update finished " + (Get-Date).ToUniversalTime().ToString("o") + " exit=0 signal=null ===")',
|
|
@@ -29745,7 +30012,6 @@ export {
|
|
|
29745
30012
|
LinkHttpError,
|
|
29746
30013
|
resolveHermesProfileDir,
|
|
29747
30014
|
resolveHermesConfigPath,
|
|
29748
|
-
readHermesApiServerConfig,
|
|
29749
30015
|
ensureHermesApiServerConfig,
|
|
29750
30016
|
resolveRuntimePaths,
|
|
29751
30017
|
createFileLogger,
|
|
@@ -29755,6 +30021,7 @@ export {
|
|
|
29755
30021
|
getGatewayLogFiles,
|
|
29756
30022
|
readRecentGatewayLogEntries,
|
|
29757
30023
|
flushLogFiles,
|
|
30024
|
+
HermesApiServerUnavailableError,
|
|
29758
30025
|
ensureHermesApiServerAvailable,
|
|
29759
30026
|
readHermesVersion,
|
|
29760
30027
|
defaultLinkConfig,
|
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ConversationService,
|
|
4
|
+
HermesApiServerUnavailableError,
|
|
4
5
|
LINK_COMMAND,
|
|
5
6
|
LINK_VERSION,
|
|
6
7
|
LinkHttpError,
|
|
@@ -28,7 +29,6 @@ import {
|
|
|
28
29
|
parseLogLevel,
|
|
29
30
|
preparePairing,
|
|
30
31
|
probeLocalLinkService,
|
|
31
|
-
readHermesApiServerConfig,
|
|
32
32
|
readHermesVersion,
|
|
33
33
|
readPairingClaim,
|
|
34
34
|
readRecentGatewayLogEntries,
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
startDaemonProcess,
|
|
45
45
|
startLinkService,
|
|
46
46
|
stopDaemonProcess
|
|
47
|
-
} from "../chunk-
|
|
47
|
+
} from "../chunk-FCCY3M5Z.js";
|
|
48
48
|
|
|
49
49
|
// src/cli/index.ts
|
|
50
50
|
import { Command } from "commander";
|
|
@@ -452,6 +452,38 @@ var messages = {
|
|
|
452
452
|
"doctor.apiReady": "Hermes API Server: ready",
|
|
453
453
|
"doctor.apiStarted": "Hermes API Server: started and ready",
|
|
454
454
|
"doctor.apiUnavailable": "Hermes API Server: unavailable. {message}",
|
|
455
|
+
"doctor.apiUnavailable.summary": "Hermes API Server: unavailable",
|
|
456
|
+
"doctor.apiUnavailable.profile": "Profile: {profile}; port: {port}",
|
|
457
|
+
"doctor.apiUnavailable.diagnosisDivider": "----- API Server diagnosis -----",
|
|
458
|
+
"doctor.apiUnavailable.check": "Check: {check}",
|
|
459
|
+
"doctor.apiUnavailable.result": "Result: {result}",
|
|
460
|
+
"doctor.apiUnavailable.startAttempt": "Link tried to start it with: {command}",
|
|
461
|
+
"doctor.apiUnavailable.startSkipped": "Auto-start was skipped; Link only checked the existing server.",
|
|
462
|
+
"doctor.apiUnavailable.recentLog": "Recent Gateway log: {message}",
|
|
463
|
+
"doctor.apiUnavailable.gatewayLog": "Gateway log: {path}",
|
|
464
|
+
"doctor.apiUnavailable.fix.port": "Suggestion: check whether another process is using port {port}, or run `hermes gateway run --replace` to view the Hermes startup error directly.",
|
|
465
|
+
"doctor.apiUnavailable.fix.auth": "Suggestion: check API_SERVER_KEY in the current Profile .env/config.yaml, or make sure this port belongs to the same Hermes Profile.",
|
|
466
|
+
"doctor.apiUnavailable.fix.notHermes": "Suggestion: change the Hermes API Server port or stop the non-Hermes service using this port.",
|
|
467
|
+
"doctor.apiUnavailable.fix.models": "Suggestion: the health endpoint responds, but the model API does not. Check Hermes provider/model configuration and Gateway logs.",
|
|
468
|
+
"doctor.apiUnavailable.fix.gateway": "Suggestion: start Hermes Gateway with `hermes gateway run --replace` and check the printed Hermes error.",
|
|
469
|
+
"doctor.apiCheck.notHermes": "Read the Hermes health response from {probe}.",
|
|
470
|
+
"doctor.apiCheck.authInvalid": "Call {probe} with the configured API key.",
|
|
471
|
+
"doctor.apiCheck.modelsTimeout": "Call {probe} to verify the API key and model API.",
|
|
472
|
+
"doctor.apiCheck.modelsHttp": "Call {probe} to verify the API key and model API.",
|
|
473
|
+
"doctor.apiCheck.port": "Connect to the Hermes health endpoints: {probe}.",
|
|
474
|
+
"doctor.apiCheck.gatewayState": "Read gateway_state from /health/detailed.",
|
|
475
|
+
"doctor.apiCheck.apiServerState": "Read platforms.api_server.state from /health/detailed.",
|
|
476
|
+
"doctor.apiCheck.exitReason": "Read exit_reason from /health/detailed.",
|
|
477
|
+
"doctor.apiCheck.unknown": "Run the Hermes API Server health checks.",
|
|
478
|
+
"doctor.apiIssue.notHermes": "The configured port returned a non-Hermes health response. Another local service may be using this port.",
|
|
479
|
+
"doctor.apiIssue.authInvalid": "The API key was rejected (HTTP 401). The key in Profile .env/config.yaml may be stale, or this port may belong to another Profile.",
|
|
480
|
+
"doctor.apiIssue.modelsTimeout": "/v1/models did not respond before the timeout.",
|
|
481
|
+
"doctor.apiIssue.modelsHttp": "/v1/models returned HTTP {status}.",
|
|
482
|
+
"doctor.apiIssue.port": "Port {port} did not respond.",
|
|
483
|
+
"doctor.apiIssue.gatewayState": "Gateway state is {state}.",
|
|
484
|
+
"doctor.apiIssue.apiServerState": "API Server state is {state}{detail}.",
|
|
485
|
+
"doctor.apiIssue.exitReason": "Gateway exit reason: {detail}.",
|
|
486
|
+
"doctor.apiIssue.unknown": "{message}",
|
|
455
487
|
"error.relayPublicKeyMismatch": "Relay rejected the pairing request because the Server-issued bootstrap token does not match this Link public key. Make sure Server and Relay are deployed with the same bootstrap key configuration, then run `hermeslink pair` again.",
|
|
456
488
|
"error.relayChallengeInvalid": "Relay did not return a valid install challenge.",
|
|
457
489
|
"error.relayLinkInvalid": "Relay did not return a valid link_id.",
|
|
@@ -616,6 +648,38 @@ var messages = {
|
|
|
616
648
|
"doctor.apiReady": "Hermes API Server\uFF1A\u5DF2\u5C31\u7EEA",
|
|
617
649
|
"doctor.apiStarted": "Hermes API Server\uFF1A\u5DF2\u81EA\u52A8\u542F\u52A8\u5E76\u5C31\u7EEA",
|
|
618
650
|
"doctor.apiUnavailable": "Hermes API Server\uFF1A\u4E0D\u53EF\u7528\u3002{message}",
|
|
651
|
+
"doctor.apiUnavailable.summary": "Hermes API Server\uFF1A\u4E0D\u53EF\u7528",
|
|
652
|
+
"doctor.apiUnavailable.profile": "Profile\uFF1A{profile}\uFF1B\u7AEF\u53E3\uFF1A{port}",
|
|
653
|
+
"doctor.apiUnavailable.diagnosisDivider": "----- API Server \u8BCA\u65AD -----",
|
|
654
|
+
"doctor.apiUnavailable.check": "\u68C0\u6D4B\u73AF\u8282\uFF1A{check}",
|
|
655
|
+
"doctor.apiUnavailable.result": "\u68C0\u6D4B\u7ED3\u679C\uFF1A{result}",
|
|
656
|
+
"doctor.apiUnavailable.startAttempt": "Link \u5DF2\u5C1D\u8BD5\u542F\u52A8\uFF1A{command}",
|
|
657
|
+
"doctor.apiUnavailable.startSkipped": "\u5DF2\u8DF3\u8FC7\u81EA\u52A8\u542F\u52A8\uFF1BLink \u53EA\u68C0\u67E5\u4E86\u5F53\u524D\u5DF2\u6709\u7684 API Server\u3002",
|
|
658
|
+
"doctor.apiUnavailable.recentLog": "\u6700\u8FD1 Gateway \u65E5\u5FD7\uFF1A{message}",
|
|
659
|
+
"doctor.apiUnavailable.gatewayLog": "Gateway \u65E5\u5FD7\uFF1A{path}",
|
|
660
|
+
"doctor.apiUnavailable.fix.port": "\u5EFA\u8BAE\uFF1A\u786E\u8BA4\u7AEF\u53E3 {port} \u662F\u5426\u88AB\u5176\u4ED6\u8FDB\u7A0B\u5360\u7528\uFF0C\u6216\u76F4\u63A5\u8FD0\u884C `hermes gateway run --replace` \u67E5\u770B Hermes \u539F\u59CB\u542F\u52A8\u9519\u8BEF\u3002",
|
|
661
|
+
"doctor.apiUnavailable.fix.auth": "\u5EFA\u8BAE\uFF1A\u68C0\u67E5\u5F53\u524D Profile \u7684 .env/config.yaml \u4E2D API_SERVER_KEY \u662F\u5426\u4E00\u81F4\uFF0C\u6216\u786E\u8BA4\u8FD9\u4E2A\u7AEF\u53E3\u5C5E\u4E8E\u540C\u4E00\u4E2A Hermes Profile\u3002",
|
|
662
|
+
"doctor.apiUnavailable.fix.notHermes": "\u5EFA\u8BAE\uFF1A\u8C03\u6574 Hermes API Server \u7AEF\u53E3\uFF0C\u6216\u505C\u6B62\u5360\u7528\u8BE5\u7AEF\u53E3\u7684\u975E Hermes \u670D\u52A1\u3002",
|
|
663
|
+
"doctor.apiUnavailable.fix.models": "\u5EFA\u8BAE\uFF1A\u5065\u5EB7\u63A5\u53E3\u5DF2\u6709\u54CD\u5E94\uFF0C\u4F46\u6A21\u578B\u63A5\u53E3\u4E0D\u53EF\u7528\uFF1B\u8BF7\u68C0\u67E5 Hermes \u7684\u6A21\u578B/provider \u914D\u7F6E\u548C Gateway \u65E5\u5FD7\u3002",
|
|
664
|
+
"doctor.apiUnavailable.fix.gateway": "\u5EFA\u8BAE\uFF1A\u76F4\u63A5\u8FD0\u884C `hermes gateway run --replace`\uFF0C\u67E5\u770B Hermes Gateway \u8F93\u51FA\u7684\u539F\u59CB\u9519\u8BEF\u3002",
|
|
665
|
+
"doctor.apiCheck.notHermes": "\u8BFB\u53D6 Hermes \u5065\u5EB7\u68C0\u67E5\u54CD\u5E94\uFF08{probe}\uFF09\u3002",
|
|
666
|
+
"doctor.apiCheck.authInvalid": "\u4F7F\u7528\u5F53\u524D\u914D\u7F6E\u7684 API key \u8BF7\u6C42 {probe}\u3002",
|
|
667
|
+
"doctor.apiCheck.modelsTimeout": "\u8BF7\u6C42 {probe}\uFF0C\u9A8C\u8BC1 API key \u548C\u6A21\u578B\u63A5\u53E3\u662F\u5426\u53EF\u7528\u3002",
|
|
668
|
+
"doctor.apiCheck.modelsHttp": "\u8BF7\u6C42 {probe}\uFF0C\u9A8C\u8BC1 API key \u548C\u6A21\u578B\u63A5\u53E3\u662F\u5426\u53EF\u7528\u3002",
|
|
669
|
+
"doctor.apiCheck.port": "\u8FDE\u63A5 Hermes \u5065\u5EB7\u68C0\u67E5\u63A5\u53E3\uFF1A{probe}\u3002",
|
|
670
|
+
"doctor.apiCheck.gatewayState": "\u8BFB\u53D6 /health/detailed \u91CC\u7684 gateway_state\u3002",
|
|
671
|
+
"doctor.apiCheck.apiServerState": "\u8BFB\u53D6 /health/detailed \u91CC\u7684 platforms.api_server.state\u3002",
|
|
672
|
+
"doctor.apiCheck.exitReason": "\u8BFB\u53D6 /health/detailed \u91CC\u7684 exit_reason\u3002",
|
|
673
|
+
"doctor.apiCheck.unknown": "\u6267\u884C Hermes API Server \u5065\u5EB7\u68C0\u67E5\u3002",
|
|
674
|
+
"doctor.apiIssue.notHermes": "\u5F53\u524D\u914D\u7F6E\u7AEF\u53E3\u8FD4\u56DE\u7684\u4E0D\u662F Hermes API Server \u5065\u5EB7\u54CD\u5E94\uFF0C\u53EF\u80FD\u88AB\u5176\u4ED6\u672C\u673A\u670D\u52A1\u5360\u7528\u3002",
|
|
675
|
+
"doctor.apiIssue.authInvalid": "API key \u88AB\u62D2\u7EDD\uFF08HTTP 401\uFF09\u3002\u5F53\u524D Profile .env/config.yaml \u91CC\u7684 key \u53EF\u80FD\u5DF2\u8FC7\u671F\uFF0C\u6216\u8FD9\u4E2A\u7AEF\u53E3\u5C5E\u4E8E\u53E6\u4E00\u4E2A Profile\u3002",
|
|
676
|
+
"doctor.apiIssue.modelsTimeout": "/v1/models \u5728\u8D85\u65F6\u65F6\u95F4\u5185\u6CA1\u6709\u54CD\u5E94\u3002",
|
|
677
|
+
"doctor.apiIssue.modelsHttp": "/v1/models \u8FD4\u56DE HTTP {status}\u3002",
|
|
678
|
+
"doctor.apiIssue.port": "\u7AEF\u53E3 {port} \u6CA1\u6709\u54CD\u5E94\u3002",
|
|
679
|
+
"doctor.apiIssue.gatewayState": "Gateway \u72B6\u6001\u662F {state}\u3002",
|
|
680
|
+
"doctor.apiIssue.apiServerState": "API Server \u72B6\u6001\u662F {state}{detail}\u3002",
|
|
681
|
+
"doctor.apiIssue.exitReason": "Gateway \u9000\u51FA\u539F\u56E0\uFF1A{detail}\u3002",
|
|
682
|
+
"doctor.apiIssue.unknown": "{message}",
|
|
619
683
|
"error.relayPublicKeyMismatch": "Relay \u62D2\u7EDD\u4E86\u914D\u5BF9\u8BF7\u6C42\uFF1AServer \u7B7E\u53D1\u7684 bootstrap token \u4E0E\u672C\u673A Link \u516C\u94A5\u4E0D\u5339\u914D\u3002\u8BF7\u786E\u8BA4 Server \u548C Relay \u4F7F\u7528\u540C\u4E00\u5957 bootstrap key \u914D\u7F6E\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C `hermeslink pair`\u3002",
|
|
620
684
|
"error.relayChallengeInvalid": "Relay \u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684\u5B89\u88C5\u6311\u6218\u3002",
|
|
621
685
|
"error.relayLinkInvalid": "Relay \u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684 link_id\u3002",
|
|
@@ -717,6 +781,136 @@ function parseLanguage(value) {
|
|
|
717
781
|
return null;
|
|
718
782
|
}
|
|
719
783
|
|
|
784
|
+
// src/hermes/api-server-diagnostics.ts
|
|
785
|
+
function formatHermesApiServerUnavailable(error, language) {
|
|
786
|
+
if (!(error instanceof HermesApiServerUnavailableError)) {
|
|
787
|
+
return translate(language, "doctor.apiUnavailable", {
|
|
788
|
+
message: localizeErrorMessage(error, language)
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
const details = error.details;
|
|
792
|
+
const port = details.port === null ? "unknown" : String(details.port);
|
|
793
|
+
const lines = [
|
|
794
|
+
translate(language, "doctor.apiUnavailable.summary"),
|
|
795
|
+
translate(language, "doctor.apiUnavailable.profile", {
|
|
796
|
+
profile: details.profileName,
|
|
797
|
+
port
|
|
798
|
+
}),
|
|
799
|
+
translate(language, "doctor.apiUnavailable.diagnosisDivider"),
|
|
800
|
+
translate(language, "doctor.apiUnavailable.check", {
|
|
801
|
+
check: formatHealthCheck(details.health, language)
|
|
802
|
+
}),
|
|
803
|
+
translate(language, "doctor.apiUnavailable.result", {
|
|
804
|
+
result: formatHealthIssue(details.health, language)
|
|
805
|
+
})
|
|
806
|
+
];
|
|
807
|
+
if (details.attemptedStart && details.startCommand) {
|
|
808
|
+
lines.push(
|
|
809
|
+
translate(language, "doctor.apiUnavailable.startAttempt", {
|
|
810
|
+
command: details.startCommand
|
|
811
|
+
})
|
|
812
|
+
);
|
|
813
|
+
} else if (!details.attemptedStart) {
|
|
814
|
+
lines.push(translate(language, "doctor.apiUnavailable.startSkipped"));
|
|
815
|
+
}
|
|
816
|
+
const fix = formatFix(details.health, language, port);
|
|
817
|
+
if (fix) {
|
|
818
|
+
lines.push(fix);
|
|
819
|
+
}
|
|
820
|
+
if (details.logHint) {
|
|
821
|
+
lines.push(
|
|
822
|
+
translate(language, "doctor.apiUnavailable.recentLog", {
|
|
823
|
+
message: details.logHint
|
|
824
|
+
})
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
if (details.gatewayLog) {
|
|
828
|
+
lines.push(
|
|
829
|
+
translate(language, "doctor.apiUnavailable.gatewayLog", {
|
|
830
|
+
path: details.gatewayLog
|
|
831
|
+
})
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
return lines.join("\n");
|
|
835
|
+
}
|
|
836
|
+
function formatHealthCheck(health, language) {
|
|
837
|
+
const probe = health.issueProbe ?? "unknown";
|
|
838
|
+
switch (health.issueCode) {
|
|
839
|
+
case "not_hermes_response":
|
|
840
|
+
return translate(language, "doctor.apiCheck.notHermes", { probe });
|
|
841
|
+
case "auth_invalid":
|
|
842
|
+
return translate(language, "doctor.apiCheck.authInvalid", { probe });
|
|
843
|
+
case "models_probe_timeout":
|
|
844
|
+
return translate(language, "doctor.apiCheck.modelsTimeout", { probe });
|
|
845
|
+
case "models_probe_http":
|
|
846
|
+
return translate(language, "doctor.apiCheck.modelsHttp", { probe });
|
|
847
|
+
case "port_unreachable":
|
|
848
|
+
return translate(language, "doctor.apiCheck.port", { probe });
|
|
849
|
+
case "gateway_state":
|
|
850
|
+
return translate(language, "doctor.apiCheck.gatewayState");
|
|
851
|
+
case "api_server_state":
|
|
852
|
+
return translate(language, "doctor.apiCheck.apiServerState");
|
|
853
|
+
case "exit_reason":
|
|
854
|
+
return translate(language, "doctor.apiCheck.exitReason");
|
|
855
|
+
default:
|
|
856
|
+
return translate(language, "doctor.apiCheck.unknown");
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function formatHealthIssue(health, language) {
|
|
860
|
+
switch (health.issueCode) {
|
|
861
|
+
case "not_hermes_response":
|
|
862
|
+
return translate(language, "doctor.apiIssue.notHermes");
|
|
863
|
+
case "auth_invalid":
|
|
864
|
+
return translate(language, "doctor.apiIssue.authInvalid");
|
|
865
|
+
case "models_probe_timeout":
|
|
866
|
+
return translate(language, "doctor.apiIssue.modelsTimeout");
|
|
867
|
+
case "models_probe_http":
|
|
868
|
+
return translate(language, "doctor.apiIssue.modelsHttp", {
|
|
869
|
+
status: health.issueStatus ?? "unknown"
|
|
870
|
+
});
|
|
871
|
+
case "port_unreachable":
|
|
872
|
+
return translate(language, "doctor.apiIssue.port", {
|
|
873
|
+
port: health.issuePort ?? "unknown"
|
|
874
|
+
});
|
|
875
|
+
case "gateway_state":
|
|
876
|
+
return translate(language, "doctor.apiIssue.gatewayState", {
|
|
877
|
+
state: health.issueState ?? "unknown"
|
|
878
|
+
});
|
|
879
|
+
case "api_server_state":
|
|
880
|
+
return translate(language, "doctor.apiIssue.apiServerState", {
|
|
881
|
+
state: health.issueState ?? "unknown",
|
|
882
|
+
detail: health.issueDetail ? `${language === "zh-CN" ? "\uFF1A" : ": "}${health.issueDetail}` : ""
|
|
883
|
+
});
|
|
884
|
+
case "exit_reason":
|
|
885
|
+
return translate(language, "doctor.apiIssue.exitReason", {
|
|
886
|
+
detail: health.issueDetail ?? "unknown"
|
|
887
|
+
});
|
|
888
|
+
default:
|
|
889
|
+
return translate(language, "doctor.apiIssue.unknown", {
|
|
890
|
+
message: health.issue ?? "unknown"
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function formatFix(health, language, port) {
|
|
895
|
+
switch (health.issueCode) {
|
|
896
|
+
case "port_unreachable":
|
|
897
|
+
return translate(language, "doctor.apiUnavailable.fix.port", { port });
|
|
898
|
+
case "auth_invalid":
|
|
899
|
+
return translate(language, "doctor.apiUnavailable.fix.auth");
|
|
900
|
+
case "not_hermes_response":
|
|
901
|
+
return translate(language, "doctor.apiUnavailable.fix.notHermes");
|
|
902
|
+
case "models_probe_timeout":
|
|
903
|
+
case "models_probe_http":
|
|
904
|
+
return translate(language, "doctor.apiUnavailable.fix.models");
|
|
905
|
+
case "gateway_state":
|
|
906
|
+
case "api_server_state":
|
|
907
|
+
case "exit_reason":
|
|
908
|
+
return translate(language, "doctor.apiUnavailable.fix.gateway");
|
|
909
|
+
default:
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
720
914
|
// src/pairing/preflight.ts
|
|
721
915
|
import { access, stat } from "fs/promises";
|
|
722
916
|
import path2 from "path";
|
|
@@ -755,7 +949,7 @@ async function assertPairingPreflightReady(options = {}) {
|
|
|
755
949
|
});
|
|
756
950
|
}
|
|
757
951
|
if (failures.length > 0) {
|
|
758
|
-
throwPairingPreflightError(failures);
|
|
952
|
+
throwPairingPreflightError(failures, options.language);
|
|
759
953
|
}
|
|
760
954
|
options.onProgress?.("hermes_cli");
|
|
761
955
|
let hermesVersion;
|
|
@@ -763,29 +957,20 @@ async function assertPairingPreflightReady(options = {}) {
|
|
|
763
957
|
const readVersion = options.readHermesVersion ?? readHermesVersion;
|
|
764
958
|
hermesVersion = await readVersion();
|
|
765
959
|
} catch {
|
|
766
|
-
throwPairingPreflightError(
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
960
|
+
throwPairingPreflightError(
|
|
961
|
+
[
|
|
962
|
+
{
|
|
963
|
+
code: "hermes_cli_unavailable",
|
|
964
|
+
zh: "\u6682\u65F6\u627E\u4E0D\u5230\u53EF\u7528\u7684 Hermes CLI \u547D\u4EE4\u3002",
|
|
965
|
+
en: "Hermes CLI is not available right now.",
|
|
966
|
+
actionZh: "\u8BF7\u786E\u8BA4 Hermes Link \u548C Hermes Agent \u5B89\u88C5\u5728\u540C\u4E00\u4E2A\u7CFB\u7EDF\u73AF\u5883\u4E2D\uFF0C\u5E76\u4E14\u7EC8\u7AEF\u80FD\u76F4\u63A5\u8FD0\u884C `hermes`\u3002\u5982\u679C\u4F60\u4F7F\u7528\u4E86\u81EA\u5B9A\u4E49\u5B89\u88C5\u8DEF\u5F84\uFF0C\u4E5F\u53EF\u4EE5\u8BBE\u7F6E HERMES_BIN\u3002",
|
|
967
|
+
actionEn: "Make sure Hermes Link and Hermes Agent are installed in the same system environment, and that the terminal can run `hermes` directly. If Hermes is installed in a custom location, set HERMES_BIN."
|
|
968
|
+
}
|
|
969
|
+
],
|
|
970
|
+
options.language
|
|
971
|
+
);
|
|
775
972
|
}
|
|
776
973
|
options.onProgress?.("hermes_api_server");
|
|
777
|
-
const apiServerConfig = await readHermesApiServerConfig(profileName, configPath);
|
|
778
|
-
if (apiServerConfig.enabled !== true) {
|
|
779
|
-
throwPairingPreflightError([
|
|
780
|
-
{
|
|
781
|
-
code: "hermes_api_server_disabled",
|
|
782
|
-
zh: "Hermes API Server \u8FD8\u6CA1\u6709\u5F00\u542F\u3002",
|
|
783
|
-
en: "Hermes API Server is not enabled.",
|
|
784
|
-
actionZh: "\u8BF7\u8FD0\u884C `hermeslink doctor` \u8BA9 Link \u81EA\u52A8\u8865\u9F50 API Server \u914D\u7F6E\uFF0C\u6216\u5728 Hermes \u914D\u7F6E\u4E2D\u542F\u7528 platforms.api_server\u3002",
|
|
785
|
-
actionEn: "Run `hermeslink doctor` so Link can prepare the API Server config, or enable platforms.api_server in Hermes config."
|
|
786
|
-
}
|
|
787
|
-
]);
|
|
788
|
-
}
|
|
789
974
|
try {
|
|
790
975
|
const ensureAvailable = options.ensureApiServerAvailable ?? ensureHermesApiServerAvailable;
|
|
791
976
|
const availability = await ensureAvailable({
|
|
@@ -806,32 +991,40 @@ async function assertPairingPreflightReady(options = {}) {
|
|
|
806
991
|
host: availability.configResult.apiServer.host ?? null,
|
|
807
992
|
port: availability.configResult.apiServer.port ?? null
|
|
808
993
|
},
|
|
994
|
+
notice: availability.configResult.notice,
|
|
995
|
+
backupPath: availability.configResult.backupPath,
|
|
809
996
|
hermesVersion: {
|
|
810
997
|
raw: hermesVersion.raw,
|
|
811
998
|
version: hermesVersion.version
|
|
812
999
|
}
|
|
813
1000
|
};
|
|
814
1001
|
} catch (error) {
|
|
815
|
-
throwPairingPreflightError(
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1002
|
+
throwPairingPreflightError(
|
|
1003
|
+
[
|
|
1004
|
+
{
|
|
1005
|
+
code: "hermes_api_server_unavailable",
|
|
1006
|
+
zh: "Hermes API Server \u9884\u68C0\u5931\u8D25\u3002",
|
|
1007
|
+
en: "Hermes API Server preflight failed.",
|
|
1008
|
+
actionZh: "\u8BF7\u6839\u636E\u4E0B\u9762\u201C\u7EC6\u8282\u201D\u4E2D\u7684\u68C0\u6D4B\u73AF\u8282\u548C\u68C0\u6D4B\u7ED3\u679C\u5904\u7406\u540E\uFF0C\u518D\u91CD\u65B0\u6267\u884C `hermeslink pair`\u3002",
|
|
1009
|
+
actionEn: "Use the check step and result in Detail below, then run `hermeslink pair` again.",
|
|
1010
|
+
detail: options.language === void 0 ? error instanceof Error ? error.message : String(error) : formatHermesApiServerUnavailable(error, options.language)
|
|
1011
|
+
}
|
|
1012
|
+
],
|
|
1013
|
+
options.language
|
|
1014
|
+
);
|
|
825
1015
|
}
|
|
826
1016
|
}
|
|
827
|
-
function throwPairingPreflightError(failures) {
|
|
1017
|
+
function throwPairingPreflightError(failures, language) {
|
|
828
1018
|
throw new LinkHttpError(
|
|
829
1019
|
503,
|
|
830
1020
|
failures[0]?.code ?? "pairing_preflight_failed",
|
|
831
|
-
formatPairingPreflightMessage(failures)
|
|
1021
|
+
formatPairingPreflightMessage(failures, language)
|
|
832
1022
|
);
|
|
833
1023
|
}
|
|
834
|
-
function formatPairingPreflightMessage(failures) {
|
|
1024
|
+
function formatPairingPreflightMessage(failures, language) {
|
|
1025
|
+
if (language) {
|
|
1026
|
+
return formatLocalizedPairingPreflightMessage(failures, language);
|
|
1027
|
+
}
|
|
835
1028
|
const lines = [
|
|
836
1029
|
"\u914D\u5BF9\u524D\u68C0\u67E5\u6CA1\u6709\u901A\u8FC7\uFF0C\u6682\u65F6\u4E0D\u4F1A\u5411 HermesPilot Server \u6216 Relay \u7533\u8BF7\u914D\u5BF9\u4E8C\u7EF4\u7801/\u914D\u5BF9\u7801\u3002",
|
|
837
1030
|
"Pairing preflight failed. Link did not request a pairing QR code or pairing code from HermesPilot Server or Relay.",
|
|
@@ -849,6 +1042,25 @@ function formatPairingPreflightMessage(failures) {
|
|
|
849
1042
|
});
|
|
850
1043
|
return lines.join("\n");
|
|
851
1044
|
}
|
|
1045
|
+
function formatLocalizedPairingPreflightMessage(failures, language) {
|
|
1046
|
+
const lines = language === "zh-CN" ? ["\u914D\u5BF9\u524D\u68C0\u67E5\u6CA1\u6709\u901A\u8FC7\uFF0C\u6682\u65F6\u4E0D\u4F1A\u7533\u8BF7\u914D\u5BF9\u4E8C\u7EF4\u7801/\u914D\u5BF9\u7801\u3002", ""] : [
|
|
1047
|
+
"Pairing preflight failed. Link did not request a pairing QR code or pairing code.",
|
|
1048
|
+
""
|
|
1049
|
+
];
|
|
1050
|
+
failures.forEach((failure, index) => {
|
|
1051
|
+
const prefix = failures.length > 1 ? `${index + 1}. ` : "";
|
|
1052
|
+
lines.push(`${prefix}${language === "zh-CN" ? failure.zh : failure.en}`);
|
|
1053
|
+
lines.push(
|
|
1054
|
+
language === "zh-CN" ? ` \u5904\u7406\u5EFA\u8BAE\uFF1A${failure.actionZh}` : ` Suggested fix: ${failure.actionEn}`
|
|
1055
|
+
);
|
|
1056
|
+
if (failure.detail) {
|
|
1057
|
+
lines.push(
|
|
1058
|
+
language === "zh-CN" ? ` \u7EC6\u8282\uFF1A${failure.detail}` : ` Detail: ${failure.detail}`
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
return lines.join("\n");
|
|
1063
|
+
}
|
|
852
1064
|
async function isDirectory(filePath) {
|
|
853
1065
|
return stat(filePath).then((value) => value.isDirectory()).catch(() => false);
|
|
854
1066
|
}
|
|
@@ -1242,12 +1454,19 @@ program.command("pair").description(helpText("pair.description")).action(async (
|
|
|
1242
1454
|
if (environment.warning) {
|
|
1243
1455
|
console.log(t("doctor.networkWarning", { message: environment.warning }));
|
|
1244
1456
|
}
|
|
1245
|
-
await assertPairingPreflightReady({
|
|
1457
|
+
const preflight = await assertPairingPreflightReady({
|
|
1246
1458
|
paths,
|
|
1459
|
+
language,
|
|
1247
1460
|
onProgress: (stage) => {
|
|
1248
1461
|
console.log(t(pairingPreflightProgressKey(stage)));
|
|
1249
1462
|
}
|
|
1250
1463
|
});
|
|
1464
|
+
if (preflight.notice) {
|
|
1465
|
+
console.log(preflight.notice);
|
|
1466
|
+
if (preflight.backupPath) {
|
|
1467
|
+
console.log(`Hermes config backup: ${preflight.backupPath}`);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1251
1470
|
console.log(t("pair.preparing"));
|
|
1252
1471
|
await ensureIdentity(paths);
|
|
1253
1472
|
const hadActiveDevices = await hasActiveDevices(paths);
|
|
@@ -1455,7 +1674,7 @@ program.command("doctor").option("--install", helpText("doctor.installOnly")).de
|
|
|
1455
1674
|
const availability = await ensureHermesApiServerAvailable({ timeoutMs: 5e3 });
|
|
1456
1675
|
console.log(t(availability.started ? "doctor.apiStarted" : "doctor.apiReady"));
|
|
1457
1676
|
} catch (error) {
|
|
1458
|
-
console.log(
|
|
1677
|
+
console.log(formatHermesApiServerUnavailable(error, language));
|
|
1459
1678
|
}
|
|
1460
1679
|
});
|
|
1461
1680
|
if (isCliEntrypoint()) {
|
package/dist/http/app.js
CHANGED