@hermespilot/link 0.6.1 → 0.6.3
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-RBMFF32Z.js → chunk-PCJGVIKH.js} +212 -72
- package/dist/cli/index.js +258 -39
- package/dist/http/app.js +1 -1
- package/package.json +20 -20
|
@@ -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.3";
|
|
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) {
|
|
@@ -19600,7 +19738,7 @@ function isExpectedClientDisconnectError2(error, options = {}) {
|
|
|
19600
19738
|
import { execFile as execFile4 } from "child_process";
|
|
19601
19739
|
import { readdir as readdir9, readFile as readFile13, rename as rename3, stat as stat13 } from "fs/promises";
|
|
19602
19740
|
import path20 from "path";
|
|
19603
|
-
import { setTimeout as
|
|
19741
|
+
import { setTimeout as delay3 } from "timers/promises";
|
|
19604
19742
|
import { promisify as promisify4 } from "util";
|
|
19605
19743
|
import YAML2 from "yaml";
|
|
19606
19744
|
var DEFAULT_PROFILE = "default";
|
|
@@ -19861,7 +19999,7 @@ async function waitForProcessesToExit(pids, timeoutMs) {
|
|
|
19861
19999
|
const deadline = Date.now() + timeoutMs;
|
|
19862
20000
|
let remaining = pids.filter(isProcessRunning);
|
|
19863
20001
|
while (remaining.length > 0 && Date.now() < deadline) {
|
|
19864
|
-
await
|
|
20002
|
+
await delay3(100);
|
|
19865
20003
|
remaining = remaining.filter(isProcessRunning);
|
|
19866
20004
|
}
|
|
19867
20005
|
return remaining;
|
|
@@ -19880,7 +20018,7 @@ async function waitForProfilePathToRemainAbsent(profilePath) {
|
|
|
19880
20018
|
if (await pathExists(profilePath)) {
|
|
19881
20019
|
return false;
|
|
19882
20020
|
}
|
|
19883
|
-
await
|
|
20021
|
+
await delay3(PROFILE_DELETE_VERIFY_INTERVAL_MS);
|
|
19884
20022
|
}
|
|
19885
20023
|
return !await pathExists(profilePath);
|
|
19886
20024
|
}
|
|
@@ -25352,12 +25490,12 @@ function connectRelayControl(options) {
|
|
|
25352
25490
|
onUpdate: options.onStreamBatchPolicy
|
|
25353
25491
|
};
|
|
25354
25492
|
const startConnect = () => {
|
|
25355
|
-
void waitForPersistedCooldown().then((
|
|
25493
|
+
void waitForPersistedCooldown().then((delay4) => {
|
|
25356
25494
|
if (closedByUser) {
|
|
25357
25495
|
return;
|
|
25358
25496
|
}
|
|
25359
|
-
if (
|
|
25360
|
-
scheduleTimer(
|
|
25497
|
+
if (delay4 > 0) {
|
|
25498
|
+
scheduleTimer(delay4, "cooldown", `Relay reconnect cooldown active for ${delay4}ms`);
|
|
25361
25499
|
return;
|
|
25362
25500
|
}
|
|
25363
25501
|
connect();
|
|
@@ -25466,8 +25604,8 @@ function connectRelayControl(options) {
|
|
|
25466
25604
|
}
|
|
25467
25605
|
if (recorded.cooldownUntilMs !== null) {
|
|
25468
25606
|
reconnectAttempts = 0;
|
|
25469
|
-
const
|
|
25470
|
-
scheduleTimer(
|
|
25607
|
+
const delay5 = Math.max(0, recorded.cooldownUntilMs - Date.now());
|
|
25608
|
+
scheduleTimer(delay5, "cooldown", `Relay reconnect storm guard active for ${delay5}ms`);
|
|
25471
25609
|
return;
|
|
25472
25610
|
}
|
|
25473
25611
|
reconnectAttempts += 1;
|
|
@@ -25475,15 +25613,15 @@ function connectRelayControl(options) {
|
|
|
25475
25613
|
baseMs: backoffBaseMs,
|
|
25476
25614
|
maxMs: backoffMaxMs
|
|
25477
25615
|
});
|
|
25478
|
-
const
|
|
25479
|
-
scheduleTimer(
|
|
25616
|
+
const delay4 = Math.max(backoffMs, relayRetryAfterMs ?? 0);
|
|
25617
|
+
scheduleTimer(delay4, "retrying", `Retrying in ${delay4}ms`);
|
|
25480
25618
|
}
|
|
25481
25619
|
async function waitForPersistedCooldown() {
|
|
25482
25620
|
return await readRelayCooldownDelayMs(paths).catch(() => 0);
|
|
25483
25621
|
}
|
|
25484
|
-
function scheduleTimer(
|
|
25622
|
+
function scheduleTimer(delay4, state, message) {
|
|
25485
25623
|
options.onStatus?.({ state, attempt: reconnectAttempts, message });
|
|
25486
|
-
retryTimer = setTimeout(connect,
|
|
25624
|
+
retryTimer = setTimeout(connect, delay4);
|
|
25487
25625
|
retryTimer.unref?.();
|
|
25488
25626
|
}
|
|
25489
25627
|
return {
|
|
@@ -27723,13 +27861,14 @@ async function buildOfficialInstallCommand(options, targetVersion) {
|
|
|
27723
27861
|
HERMESLINK_YES: "1",
|
|
27724
27862
|
HERMESLINK_NO_PROFILE_EDIT: "1",
|
|
27725
27863
|
HERMESLINK_NO_PATH_PROMPT: "1",
|
|
27864
|
+
HERMESLINK_SKIP_DOCTOR: "1",
|
|
27726
27865
|
HERMESLINK_SKIP_RESTART: "1"
|
|
27727
27866
|
};
|
|
27728
27867
|
if (process.platform === "win32") {
|
|
27729
27868
|
const windowsCommand = `& { $ErrorActionPreference = "Stop"; Invoke-RestMethod ${quotePowerShellString(installer.windowsUrl)} | Invoke-Expression }`;
|
|
27730
27869
|
return {
|
|
27731
27870
|
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`,
|
|
27871
|
+
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
27872
|
env,
|
|
27734
27873
|
source: "official-installer",
|
|
27735
27874
|
installerUrl: installer.windowsUrl
|
|
@@ -27738,7 +27877,7 @@ async function buildOfficialInstallCommand(options, targetVersion) {
|
|
|
27738
27877
|
const unixCommand = buildUnixInstallCommand(installer.unixUrl);
|
|
27739
27878
|
return {
|
|
27740
27879
|
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`,
|
|
27880
|
+
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
27881
|
env,
|
|
27743
27882
|
source: "official-installer",
|
|
27744
27883
|
installerUrl: installer.unixUrl
|
|
@@ -27920,8 +28059,9 @@ function buildWindowsDetachedUpdaterScript(input) {
|
|
|
27920
28059
|
" $env:HERMESLINK_VERSION = $TargetVersion",
|
|
27921
28060
|
' $env:HERMESLINK_YES = "1"',
|
|
27922
28061
|
' $env:HERMESLINK_NO_PATH_PROMPT = "1"',
|
|
28062
|
+
' $env:HERMESLINK_SKIP_DOCTOR = "1"',
|
|
27923
28063
|
' $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',
|
|
28064
|
+
' Invoke-Step "Installing Hermes Link $TargetVersion" { & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $InstallerPath -Version $TargetVersion -NoPathPrompt -SkipDoctor -SkipRestart } | Out-Null',
|
|
27925
28065
|
' Invoke-Step "Starting Hermes Link after update" { & $NodePath $CliScriptPath start } | Out-Null',
|
|
27926
28066
|
' Add-UpdateLog ""',
|
|
27927
28067
|
' Add-UpdateLog ("=== link update finished " + (Get-Date).ToUniversalTime().ToString("o") + " exit=0 signal=null ===")',
|
|
@@ -29745,7 +29885,6 @@ export {
|
|
|
29745
29885
|
LinkHttpError,
|
|
29746
29886
|
resolveHermesProfileDir,
|
|
29747
29887
|
resolveHermesConfigPath,
|
|
29748
|
-
readHermesApiServerConfig,
|
|
29749
29888
|
ensureHermesApiServerConfig,
|
|
29750
29889
|
resolveRuntimePaths,
|
|
29751
29890
|
createFileLogger,
|
|
@@ -29755,6 +29894,7 @@ export {
|
|
|
29755
29894
|
getGatewayLogFiles,
|
|
29756
29895
|
readRecentGatewayLogEntries,
|
|
29757
29896
|
flushLogFiles,
|
|
29897
|
+
HermesApiServerUnavailableError,
|
|
29758
29898
|
ensureHermesApiServerAvailable,
|
|
29759
29899
|
readHermesVersion,
|
|
29760
29900
|
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-PCJGVIKH.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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hermespilot/link",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,27 +40,27 @@
|
|
|
40
40
|
"publish:npm": "npm publish --access public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@koa/router": "
|
|
44
|
-
"better-sqlite3": "
|
|
45
|
-
"commander": "
|
|
46
|
-
"koa": "
|
|
47
|
-
"qrcode": "
|
|
48
|
-
"qrcode-terminal": "
|
|
49
|
-
"ws": "
|
|
50
|
-
"yaml": "
|
|
51
|
-
"zod": "
|
|
43
|
+
"@koa/router": "15.4.0",
|
|
44
|
+
"better-sqlite3": "12.9.0",
|
|
45
|
+
"commander": "12.1.0",
|
|
46
|
+
"koa": "2.15.3",
|
|
47
|
+
"qrcode": "1.5.4",
|
|
48
|
+
"qrcode-terminal": "0.12.0",
|
|
49
|
+
"ws": "8.18.0",
|
|
50
|
+
"yaml": "2.6.1",
|
|
51
|
+
"zod": "3.24.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@types/better-sqlite3": "
|
|
55
|
-
"@types/koa": "
|
|
56
|
-
"@types/node": "
|
|
57
|
-
"@types/qrcode": "
|
|
58
|
-
"@types/qrcode-terminal": "
|
|
59
|
-
"@types/ws": "
|
|
60
|
-
"tsup": "
|
|
61
|
-
"tsx": "
|
|
62
|
-
"typescript": "
|
|
63
|
-
"vitest": "
|
|
54
|
+
"@types/better-sqlite3": "7.6.13",
|
|
55
|
+
"@types/koa": "2.15.0",
|
|
56
|
+
"@types/node": "20.19.39",
|
|
57
|
+
"@types/qrcode": "1.5.6",
|
|
58
|
+
"@types/qrcode-terminal": "0.12.2",
|
|
59
|
+
"@types/ws": "8.5.13",
|
|
60
|
+
"tsup": "8.3.5",
|
|
61
|
+
"tsx": "4.19.2",
|
|
62
|
+
"typescript": "5.7.2",
|
|
63
|
+
"vitest": "2.1.8"
|
|
64
64
|
},
|
|
65
65
|
"engines": {
|
|
66
66
|
"node": ">=20.0.0"
|