@hermespilot/link 0.4.3 → 0.4.5
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.
|
@@ -3602,17 +3602,6 @@ function isEnvValueConfigured(value) {
|
|
|
3602
3602
|
}
|
|
3603
3603
|
async function readHermesApiServerEnvOverrides(profileName) {
|
|
3604
3604
|
const values = await readHermesEnvFile(profileName);
|
|
3605
|
-
for (const key of [
|
|
3606
|
-
"API_SERVER_ENABLED",
|
|
3607
|
-
"API_SERVER_HOST",
|
|
3608
|
-
"API_SERVER_PORT",
|
|
3609
|
-
"API_SERVER_KEY"
|
|
3610
|
-
]) {
|
|
3611
|
-
const value = process.env[key];
|
|
3612
|
-
if (typeof value === "string" && value.trim()) {
|
|
3613
|
-
values[key] = value;
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
3605
|
const port = Number.parseInt(values.API_SERVER_PORT ?? "", 10);
|
|
3617
3606
|
return {
|
|
3618
3607
|
enabled: parseEnvBoolean(values.API_SERVER_ENABLED),
|
|
@@ -3996,7 +3985,7 @@ import os2 from "os";
|
|
|
3996
3985
|
import path5 from "path";
|
|
3997
3986
|
|
|
3998
3987
|
// src/constants.ts
|
|
3999
|
-
var LINK_VERSION = "0.4.
|
|
3988
|
+
var LINK_VERSION = "0.4.5";
|
|
4000
3989
|
var LINK_COMMAND = "hermeslink";
|
|
4001
3990
|
var LINK_DEFAULT_PORT = 52379;
|
|
4002
3991
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -4496,6 +4485,7 @@ var DASHBOARD_STATUS_URL = "http://127.0.0.1:9119/api/status";
|
|
|
4496
4485
|
var DASHBOARD_STATUS_TIMEOUT_MS = 1500;
|
|
4497
4486
|
var DEFAULT_VERSION_CACHE_TTL_MS = 6e4;
|
|
4498
4487
|
var MAX_VERSION_LOG_OUTPUT_LENGTH = 1200;
|
|
4488
|
+
var HERMES_GATEWAY_ENV_BLOCKLIST_PREFIXES = ["API_SERVER_"];
|
|
4499
4489
|
var gatewayStartInFlightByProfile = /* @__PURE__ */ new Map();
|
|
4500
4490
|
var hermesVersionCache = /* @__PURE__ */ new Map();
|
|
4501
4491
|
var hermesVersionInFlight = /* @__PURE__ */ new Map();
|
|
@@ -4720,7 +4710,7 @@ async function startHermesGateway(paths, profileName, logger) {
|
|
|
4720
4710
|
try {
|
|
4721
4711
|
const child = spawn(resolveHermesBin(), gatewayRunArgs(profileName), {
|
|
4722
4712
|
detached: true,
|
|
4723
|
-
env:
|
|
4713
|
+
env: buildHermesGatewayChildEnv(),
|
|
4724
4714
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4725
4715
|
windowsHide: true
|
|
4726
4716
|
});
|
|
@@ -4786,6 +4776,7 @@ async function restartHermesGatewayServiceIfAvailable(options) {
|
|
|
4786
4776
|
}
|
|
4787
4777
|
try {
|
|
4788
4778
|
await execFileAsync2(resolveHermesBin(), ["gateway", "restart"], {
|
|
4779
|
+
env: buildHermesGatewayChildEnv(),
|
|
4789
4780
|
timeout: options.timeoutMs ?? DEFAULT_START_TIMEOUT_MS,
|
|
4790
4781
|
windowsHide: true
|
|
4791
4782
|
});
|
|
@@ -4840,6 +4831,17 @@ function gatewayRunArgs(profileName) {
|
|
|
4840
4831
|
function formatHermesGatewayRunCommand(profileName) {
|
|
4841
4832
|
return `${resolveHermesBin()} ${gatewayRunArgs(profileName).join(" ")}`;
|
|
4842
4833
|
}
|
|
4834
|
+
function buildHermesGatewayChildEnv() {
|
|
4835
|
+
const env = { ...process.env };
|
|
4836
|
+
for (const key of Object.keys(env)) {
|
|
4837
|
+
if (HERMES_GATEWAY_ENV_BLOCKLIST_PREFIXES.some(
|
|
4838
|
+
(prefix) => key.startsWith(prefix)
|
|
4839
|
+
)) {
|
|
4840
|
+
delete env[key];
|
|
4841
|
+
}
|
|
4842
|
+
}
|
|
4843
|
+
return env;
|
|
4844
|
+
}
|
|
4843
4845
|
async function fileExists(filePath) {
|
|
4844
4846
|
return access(filePath).then(() => true).catch(() => false);
|
|
4845
4847
|
}
|
|
@@ -4875,6 +4877,10 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
|
|
|
4875
4877
|
}
|
|
4876
4878
|
const issue = describeDetailedHealthIssue(record);
|
|
4877
4879
|
const terminal = isTerminalDetailedHealth(record);
|
|
4880
|
+
const authIssue = issue ? null : await probeHermesApiServerAuth(resolvedConfig, fetcher);
|
|
4881
|
+
if (authIssue) {
|
|
4882
|
+
return authIssue;
|
|
4883
|
+
}
|
|
4878
4884
|
return {
|
|
4879
4885
|
healthy: !issue,
|
|
4880
4886
|
terminal,
|
|
@@ -4911,20 +4917,9 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
|
|
|
4911
4917
|
};
|
|
4912
4918
|
}
|
|
4913
4919
|
if (resolvedConfig.key) {
|
|
4914
|
-
const
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
method: "GET",
|
|
4918
|
-
headers: authHeaders(resolvedConfig)
|
|
4919
|
-
},
|
|
4920
|
-
fetcher
|
|
4921
|
-
);
|
|
4922
|
-
if (authProbe?.status === 401) {
|
|
4923
|
-
return {
|
|
4924
|
-
healthy: false,
|
|
4925
|
-
authInvalid: true,
|
|
4926
|
-
issue: "Hermes API Server \u8FD4\u56DE 401\uFF0C\u5F53\u524D\u7AEF\u53E3\u53EF\u80FD\u5C5E\u4E8E\u53E6\u4E00\u4E2A Profile\uFF0C\u6216 API Server key \u5DF2\u53D8\u66F4\u3002"
|
|
4927
|
-
};
|
|
4920
|
+
const authIssue = await probeHermesApiServerAuth(resolvedConfig, fetcher);
|
|
4921
|
+
if (authIssue) {
|
|
4922
|
+
return authIssue;
|
|
4928
4923
|
}
|
|
4929
4924
|
}
|
|
4930
4925
|
return { healthy: true };
|
|
@@ -4941,6 +4936,39 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
|
|
|
4941
4936
|
issue: describePortHealthFailure(resolvedConfig.port)
|
|
4942
4937
|
};
|
|
4943
4938
|
}
|
|
4939
|
+
async function probeHermesApiServerAuth(config, fetcher) {
|
|
4940
|
+
if (!config.key) {
|
|
4941
|
+
return null;
|
|
4942
|
+
}
|
|
4943
|
+
const response = await fetchWithTimeout(
|
|
4944
|
+
`http://127.0.0.1:${config.port}/v1/models`,
|
|
4945
|
+
{
|
|
4946
|
+
method: "GET",
|
|
4947
|
+
headers: authHeaders(config)
|
|
4948
|
+
},
|
|
4949
|
+
fetcher
|
|
4950
|
+
);
|
|
4951
|
+
if (!response) {
|
|
4952
|
+
return {
|
|
4953
|
+
healthy: false,
|
|
4954
|
+
issue: "Hermes API Server \u9274\u6743\u63A2\u6D4B\u65E0\u54CD\u5E94\uFF1A/v1/models \u6CA1\u6709\u5728\u8D85\u65F6\u65F6\u95F4\u5185\u8FD4\u56DE\u3002"
|
|
4955
|
+
};
|
|
4956
|
+
}
|
|
4957
|
+
if (response?.status === 401) {
|
|
4958
|
+
return {
|
|
4959
|
+
healthy: false,
|
|
4960
|
+
authInvalid: true,
|
|
4961
|
+
issue: "Hermes API Server \u8FD4\u56DE 401\uFF0C\u5F53\u524D\u7AEF\u53E3\u53EF\u80FD\u5C5E\u4E8E\u53E6\u4E00\u4E2A Profile\uFF0C\u6216 API Server key \u5DF2\u53D8\u66F4\u3002"
|
|
4962
|
+
};
|
|
4963
|
+
}
|
|
4964
|
+
if (response && !response.ok) {
|
|
4965
|
+
return {
|
|
4966
|
+
healthy: false,
|
|
4967
|
+
issue: `Hermes API Server \u9274\u6743\u63A2\u6D4B\u5931\u8D25\uFF1A/v1/models \u8FD4\u56DE HTTP ${response.status}\u3002`
|
|
4968
|
+
};
|
|
4969
|
+
}
|
|
4970
|
+
return null;
|
|
4971
|
+
}
|
|
4944
4972
|
function fetchWithTimeout(input, init, fetcher) {
|
|
4945
4973
|
const controller = new AbortController();
|
|
4946
4974
|
const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
|
|
@@ -8979,24 +9007,15 @@ var ConversationQueryCoordinator = class {
|
|
|
8979
9007
|
async listConversationPage(options = {}) {
|
|
8980
9008
|
const limit = normalizeConversationListPageLimit(options.limit);
|
|
8981
9009
|
const cursor = decodeConversationListCursor(options.cursor);
|
|
8982
|
-
|
|
9010
|
+
return this.listIndexedConversationPage({
|
|
8983
9011
|
limit,
|
|
8984
|
-
cursor
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
return this.listConversationPageFromStore({ limit, cursor });
|
|
8988
|
-
}
|
|
8989
|
-
const summaries = await this.summarizeIndexedConversations(
|
|
8990
|
-
indexedPage.records
|
|
8991
|
-
);
|
|
8992
|
-
return {
|
|
8993
|
-
conversations: summaries,
|
|
8994
|
-
page: {
|
|
9012
|
+
cursor,
|
|
9013
|
+
fallback: () => this.listConversationPageFromStore({ limit, cursor }),
|
|
9014
|
+
listPage: (pageCursor) => listConversationStatsPage(this.deps.paths, {
|
|
8995
9015
|
limit,
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
};
|
|
9016
|
+
cursor: pageCursor
|
|
9017
|
+
})
|
|
9018
|
+
});
|
|
9000
9019
|
}
|
|
9001
9020
|
async searchConversationPage(options = {}) {
|
|
9002
9021
|
const query = normalizeConversationSearchQuery(options.query);
|
|
@@ -9005,20 +9024,72 @@ var ConversationQueryCoordinator = class {
|
|
|
9005
9024
|
}
|
|
9006
9025
|
const limit = normalizeConversationListPageLimit(options.limit);
|
|
9007
9026
|
const cursor = decodeConversationListCursor(options.cursor);
|
|
9008
|
-
|
|
9027
|
+
return this.listIndexedConversationPage({
|
|
9009
9028
|
limit,
|
|
9010
9029
|
cursor,
|
|
9011
|
-
|
|
9030
|
+
listPage: (pageCursor) => searchConversationStatsPage(this.deps.paths, {
|
|
9031
|
+
limit,
|
|
9032
|
+
cursor: pageCursor,
|
|
9033
|
+
query
|
|
9034
|
+
})
|
|
9012
9035
|
});
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9036
|
+
}
|
|
9037
|
+
async listIndexedConversationPage(input) {
|
|
9038
|
+
const collected = [];
|
|
9039
|
+
const seenConversationIds = /* @__PURE__ */ new Set();
|
|
9040
|
+
let scanCursor = input.cursor;
|
|
9041
|
+
let usedIndex = false;
|
|
9042
|
+
while (collected.length <= input.limit) {
|
|
9043
|
+
const indexedPage = await input.listPage(scanCursor);
|
|
9044
|
+
if (indexedPage.records.length === 0) {
|
|
9045
|
+
if (!usedIndex && input.fallback) {
|
|
9046
|
+
return input.fallback();
|
|
9047
|
+
}
|
|
9048
|
+
return this.buildConversationListPage({
|
|
9049
|
+
limit: input.limit,
|
|
9050
|
+
conversations: collected,
|
|
9051
|
+
hasMore: false
|
|
9052
|
+
});
|
|
9053
|
+
}
|
|
9054
|
+
usedIndex = true;
|
|
9055
|
+
const summaries = await this.summarizeIndexedConversations(
|
|
9056
|
+
indexedPage.records
|
|
9057
|
+
);
|
|
9058
|
+
for (const summary of summaries) {
|
|
9059
|
+
if (!seenConversationIds.add(summary.id)) {
|
|
9060
|
+
continue;
|
|
9061
|
+
}
|
|
9062
|
+
collected.push(summary);
|
|
9063
|
+
if (collected.length > input.limit) {
|
|
9064
|
+
break;
|
|
9065
|
+
}
|
|
9066
|
+
}
|
|
9067
|
+
if (collected.length > input.limit) {
|
|
9068
|
+
break;
|
|
9069
|
+
}
|
|
9070
|
+
if (!indexedPage.hasMore) {
|
|
9071
|
+
return this.buildConversationListPage({
|
|
9072
|
+
limit: input.limit,
|
|
9073
|
+
conversations: collected,
|
|
9074
|
+
hasMore: false
|
|
9075
|
+
});
|
|
9076
|
+
}
|
|
9077
|
+
scanCursor = conversationListCursorFromRecord(indexedPage.records.at(-1));
|
|
9078
|
+
}
|
|
9079
|
+
return this.buildConversationListPage({
|
|
9080
|
+
limit: input.limit,
|
|
9081
|
+
conversations: collected,
|
|
9082
|
+
hasMore: true
|
|
9083
|
+
});
|
|
9084
|
+
}
|
|
9085
|
+
buildConversationListPage(input) {
|
|
9086
|
+
const conversations = input.conversations.slice(0, input.limit);
|
|
9016
9087
|
return {
|
|
9017
|
-
conversations
|
|
9088
|
+
conversations,
|
|
9018
9089
|
page: {
|
|
9019
|
-
limit,
|
|
9020
|
-
has_more:
|
|
9021
|
-
next_cursor:
|
|
9090
|
+
limit: input.limit,
|
|
9091
|
+
has_more: input.hasMore,
|
|
9092
|
+
next_cursor: input.hasMore && conversations.length > 0 ? encodeConversationListCursorFromSummary(conversations.at(-1)) : null
|
|
9022
9093
|
}
|
|
9023
9094
|
};
|
|
9024
9095
|
}
|
|
@@ -9177,6 +9248,12 @@ function encodeConversationListCursor(record) {
|
|
|
9177
9248
|
"utf8"
|
|
9178
9249
|
).toString("base64url");
|
|
9179
9250
|
}
|
|
9251
|
+
function conversationListCursorFromRecord(record) {
|
|
9252
|
+
return {
|
|
9253
|
+
updatedAt: record.updatedAt,
|
|
9254
|
+
conversationId: record.conversationId
|
|
9255
|
+
};
|
|
9256
|
+
}
|
|
9180
9257
|
function encodeConversationListCursorFromSummary(summary) {
|
|
9181
9258
|
return encodeConversationListCursor({
|
|
9182
9259
|
conversationId: summary.id,
|
|
@@ -15305,6 +15382,24 @@ function readHeader(ctx, name) {
|
|
|
15305
15382
|
const value = ctx.get(name).trim();
|
|
15306
15383
|
return value ? value : null;
|
|
15307
15384
|
}
|
|
15385
|
+
function readUploadFilenameHeader(ctx) {
|
|
15386
|
+
const encoded = readHeader(ctx, "x-filename-base64") ?? readHeader(ctx, "x-filename-b64");
|
|
15387
|
+
if (encoded) {
|
|
15388
|
+
const decoded = decodeBase64Utf8Header(encoded);
|
|
15389
|
+
if (decoded) {
|
|
15390
|
+
return decoded;
|
|
15391
|
+
}
|
|
15392
|
+
}
|
|
15393
|
+
return readHeader(ctx, "x-filename");
|
|
15394
|
+
}
|
|
15395
|
+
function decodeBase64Utf8Header(value) {
|
|
15396
|
+
const trimmed = value.trim();
|
|
15397
|
+
if (!trimmed || !/^[A-Za-z0-9+/]+={0,2}$/u.test(trimmed)) {
|
|
15398
|
+
return null;
|
|
15399
|
+
}
|
|
15400
|
+
const decoded = Buffer.from(trimmed, "base64").toString("utf8").trim();
|
|
15401
|
+
return decoded || null;
|
|
15402
|
+
}
|
|
15308
15403
|
var CONVERSATION_HISTORY_REPLAY_FIELDS = [
|
|
15309
15404
|
"tool_call_id",
|
|
15310
15405
|
"tool_calls",
|
|
@@ -15797,7 +15892,7 @@ function registerConversationRoutes(router, options) {
|
|
|
15797
15892
|
}
|
|
15798
15893
|
const blob = await conversations.writeBlob(ctx.params.conversationId, {
|
|
15799
15894
|
bytes,
|
|
15800
|
-
filename:
|
|
15895
|
+
filename: readUploadFilenameHeader(ctx) ?? void 0,
|
|
15801
15896
|
mime: ctx.get("content-type") || void 0
|
|
15802
15897
|
});
|
|
15803
15898
|
ctx.status = 201;
|
|
@@ -20641,6 +20736,8 @@ import { mkdir as mkdir12, rm as rm9, writeFile as writeFile3 } from "fs/promise
|
|
|
20641
20736
|
|
|
20642
20737
|
// src/relay/control-client.ts
|
|
20643
20738
|
import WebSocket from "ws";
|
|
20739
|
+
var RELAY_SSE_BATCH_FLUSH_INTERVAL_MS = 50;
|
|
20740
|
+
var RELAY_SSE_BATCH_FLUSH_BYTES = 2 * 1024;
|
|
20644
20741
|
function connectRelayControl(options) {
|
|
20645
20742
|
const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
|
|
20646
20743
|
wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
@@ -20776,6 +20873,7 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
|
|
|
20776
20873
|
}
|
|
20777
20874
|
const abortController = new AbortController();
|
|
20778
20875
|
abortControllers.set(frame.id, abortController);
|
|
20876
|
+
let sseBatcher = null;
|
|
20779
20877
|
try {
|
|
20780
20878
|
const response = await fetch(`http://127.0.0.1:${localPort}${frame.path}`, {
|
|
20781
20879
|
method: frame.method,
|
|
@@ -20787,26 +20885,92 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
|
|
|
20787
20885
|
const contentType = response.headers.get("content-type") ?? "";
|
|
20788
20886
|
if (response.body && contentType.includes("text/event-stream")) {
|
|
20789
20887
|
socket.send(JSON.stringify({ type: "http.stream.start", id: frame.id, status: response.status, headers }));
|
|
20888
|
+
sseBatcher = createRelayStreamChunkBatcher(socket, frame.id);
|
|
20790
20889
|
const reader = response.body.getReader();
|
|
20791
20890
|
while (true) {
|
|
20792
20891
|
const next = await reader.read();
|
|
20793
20892
|
if (next.done) {
|
|
20794
20893
|
break;
|
|
20795
20894
|
}
|
|
20796
|
-
|
|
20895
|
+
sseBatcher.push(next.value);
|
|
20797
20896
|
}
|
|
20897
|
+
sseBatcher.flush();
|
|
20798
20898
|
socket.send(JSON.stringify({ type: "http.stream.end", id: frame.id }));
|
|
20799
20899
|
return;
|
|
20800
20900
|
}
|
|
20801
20901
|
const body = Buffer.from(await response.arrayBuffer()).toString("base64");
|
|
20802
20902
|
socket.send(JSON.stringify({ type: "http.response", id: frame.id, status: response.status, headers, bodyBase64: body }));
|
|
20803
20903
|
} catch (error) {
|
|
20904
|
+
if (abortController.signal.aborted || isAbortError2(error)) {
|
|
20905
|
+
return;
|
|
20906
|
+
}
|
|
20907
|
+
sseBatcher?.flush();
|
|
20804
20908
|
const message = error instanceof Error ? error.message : "Relay request failed";
|
|
20805
20909
|
socket.send(JSON.stringify({ type: "http.error", id: frame.id, status: 502, message }));
|
|
20806
20910
|
} finally {
|
|
20911
|
+
sseBatcher?.dispose();
|
|
20807
20912
|
abortControllers.delete(frame.id);
|
|
20808
20913
|
}
|
|
20809
20914
|
}
|
|
20915
|
+
function isAbortError2(error) {
|
|
20916
|
+
return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
20917
|
+
}
|
|
20918
|
+
function createRelayStreamChunkBatcher(socket, id) {
|
|
20919
|
+
let chunks = [];
|
|
20920
|
+
let totalBytes = 0;
|
|
20921
|
+
let flushTimer = null;
|
|
20922
|
+
const clearFlushTimer = () => {
|
|
20923
|
+
if (flushTimer == null) {
|
|
20924
|
+
return;
|
|
20925
|
+
}
|
|
20926
|
+
clearTimeout(flushTimer);
|
|
20927
|
+
flushTimer = null;
|
|
20928
|
+
};
|
|
20929
|
+
const flush = () => {
|
|
20930
|
+
clearFlushTimer();
|
|
20931
|
+
if (totalBytes <= 0) {
|
|
20932
|
+
return;
|
|
20933
|
+
}
|
|
20934
|
+
const bodyBase64 = Buffer.concat(chunks, totalBytes).toString("base64");
|
|
20935
|
+
chunks = [];
|
|
20936
|
+
totalBytes = 0;
|
|
20937
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
20938
|
+
return;
|
|
20939
|
+
}
|
|
20940
|
+
socket.send(JSON.stringify({ type: "http.stream.chunk", id, bodyBase64 }));
|
|
20941
|
+
};
|
|
20942
|
+
const scheduleFlush = () => {
|
|
20943
|
+
if (flushTimer != null) {
|
|
20944
|
+
return;
|
|
20945
|
+
}
|
|
20946
|
+
flushTimer = setTimeout(() => {
|
|
20947
|
+
flushTimer = null;
|
|
20948
|
+
flush();
|
|
20949
|
+
}, RELAY_SSE_BATCH_FLUSH_INTERVAL_MS);
|
|
20950
|
+
flushTimer.unref?.();
|
|
20951
|
+
};
|
|
20952
|
+
return {
|
|
20953
|
+
push(chunk) {
|
|
20954
|
+
if (chunk.byteLength <= 0) {
|
|
20955
|
+
return;
|
|
20956
|
+
}
|
|
20957
|
+
const buffer = Buffer.from(chunk);
|
|
20958
|
+
chunks.push(buffer);
|
|
20959
|
+
totalBytes += buffer.byteLength;
|
|
20960
|
+
if (totalBytes >= RELAY_SSE_BATCH_FLUSH_BYTES) {
|
|
20961
|
+
flush();
|
|
20962
|
+
return;
|
|
20963
|
+
}
|
|
20964
|
+
scheduleFlush();
|
|
20965
|
+
},
|
|
20966
|
+
flush,
|
|
20967
|
+
dispose() {
|
|
20968
|
+
clearFlushTimer();
|
|
20969
|
+
chunks = [];
|
|
20970
|
+
totalBytes = 0;
|
|
20971
|
+
}
|
|
20972
|
+
};
|
|
20973
|
+
}
|
|
20810
20974
|
|
|
20811
20975
|
// src/runtime/system-info.ts
|
|
20812
20976
|
import { execFileSync } from "child_process";
|
|
@@ -20827,8 +20991,8 @@ function readLinkSystemInfo() {
|
|
|
20827
20991
|
function buildDefaultDisplayName(input) {
|
|
20828
20992
|
const hostname = normalizeText(input.hostname);
|
|
20829
20993
|
const osLabel = normalizeText(input.osLabel);
|
|
20830
|
-
if (hostname
|
|
20831
|
-
return truncateText(
|
|
20994
|
+
if (hostname) {
|
|
20995
|
+
return truncateText(hostname, 128);
|
|
20832
20996
|
}
|
|
20833
20997
|
return truncateText(hostname ?? osLabel ?? `Hermes Link ${input.platform}`, 128);
|
|
20834
20998
|
}
|
|
@@ -21126,6 +21290,10 @@ function unique(values) {
|
|
|
21126
21290
|
|
|
21127
21291
|
// src/link/network-report-state.ts
|
|
21128
21292
|
var DEFAULT_AUTO_DAILY_LIMIT = 20;
|
|
21293
|
+
async function readNetworkReportState(paths) {
|
|
21294
|
+
const state = await readLinkState(paths);
|
|
21295
|
+
return normalizeNetworkReportState(state.networkReport);
|
|
21296
|
+
}
|
|
21129
21297
|
async function markNetworkStatusReported(paths, snapshotInput, reportedAt = /* @__PURE__ */ new Date()) {
|
|
21130
21298
|
const snapshot = normalizeNetworkSnapshot(snapshotInput);
|
|
21131
21299
|
await updateNetworkReportState(paths, (current) => ({
|
|
@@ -21175,6 +21343,20 @@ async function reserveAutomaticNetworkReport(paths, snapshotInput, options = {})
|
|
|
21175
21343
|
});
|
|
21176
21344
|
return reservation;
|
|
21177
21345
|
}
|
|
21346
|
+
async function mergeLastReportedPublicRoutes(paths, snapshotInput) {
|
|
21347
|
+
const state = await readNetworkReportState(paths);
|
|
21348
|
+
return {
|
|
21349
|
+
...snapshotInput,
|
|
21350
|
+
publicIpv4s: uniqueStrings([
|
|
21351
|
+
...snapshotInput.publicIpv4s,
|
|
21352
|
+
...state.lastReportedPublicIpv4s
|
|
21353
|
+
]).slice(0, 2),
|
|
21354
|
+
publicIpv6s: uniqueStrings([
|
|
21355
|
+
...snapshotInput.publicIpv6s,
|
|
21356
|
+
...state.lastReportedPublicIpv6s
|
|
21357
|
+
]).slice(0, 2)
|
|
21358
|
+
};
|
|
21359
|
+
}
|
|
21178
21360
|
async function updateNetworkReportState(paths, update) {
|
|
21179
21361
|
const state = await readLinkState(paths);
|
|
21180
21362
|
const next = {
|
|
@@ -21263,6 +21445,9 @@ function sameStringList(left, right) {
|
|
|
21263
21445
|
}
|
|
21264
21446
|
return left.every((value, index) => value === right[index]);
|
|
21265
21447
|
}
|
|
21448
|
+
function uniqueStrings(values) {
|
|
21449
|
+
return [...new Set(values)];
|
|
21450
|
+
}
|
|
21266
21451
|
function formatUtcDay(date) {
|
|
21267
21452
|
return date.toISOString().slice(0, 10);
|
|
21268
21453
|
}
|
|
@@ -21274,7 +21459,7 @@ async function reportLinkStatusToServer(options = {}) {
|
|
|
21274
21459
|
if (!identity?.link_id) {
|
|
21275
21460
|
return null;
|
|
21276
21461
|
}
|
|
21277
|
-
const
|
|
21462
|
+
const discoveredRoutes = options.routes ?? await discoverRouteCandidates({
|
|
21278
21463
|
port: config.port,
|
|
21279
21464
|
relayBaseUrl: config.relayBaseUrl,
|
|
21280
21465
|
linkId: identity.link_id,
|
|
@@ -21284,6 +21469,7 @@ async function reportLinkStatusToServer(options = {}) {
|
|
|
21284
21469
|
configuredLanHost: config.lanHost,
|
|
21285
21470
|
fetchImpl: options.fetchImpl
|
|
21286
21471
|
});
|
|
21472
|
+
const routes = await mergeLastReportedPublicRoutes(paths, discoveredRoutes);
|
|
21287
21473
|
const systemInfo = readLinkSystemInfo();
|
|
21288
21474
|
const payload = {
|
|
21289
21475
|
type: "hermes_link_status_report",
|
|
@@ -21375,12 +21561,16 @@ function startLanIpMonitor(options) {
|
|
|
21375
21561
|
running = false;
|
|
21376
21562
|
}
|
|
21377
21563
|
};
|
|
21378
|
-
current = check({ forceReport: true, publishToRelay: true });
|
|
21564
|
+
current = check({ forceReport: true, publishToRelay: true, observePublicRoute: true });
|
|
21379
21565
|
const timer = setInterval(() => {
|
|
21380
|
-
current = check();
|
|
21566
|
+
current = check({ observePublicRoute: false });
|
|
21381
21567
|
}, options.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
21382
21568
|
timer.unref?.();
|
|
21383
21569
|
return {
|
|
21570
|
+
async refreshPublicRoutes() {
|
|
21571
|
+
current = check({ forceReport: true, publishToRelay: true, observePublicRoute: true });
|
|
21572
|
+
await current;
|
|
21573
|
+
},
|
|
21384
21574
|
async close() {
|
|
21385
21575
|
closed = true;
|
|
21386
21576
|
clearInterval(timer);
|
|
@@ -21396,16 +21586,17 @@ async function checkLanIpChange(options, context = {}) {
|
|
|
21396
21586
|
if (!identity?.link_id) {
|
|
21397
21587
|
return;
|
|
21398
21588
|
}
|
|
21399
|
-
const
|
|
21589
|
+
const discoveredRoutes = await discoverRouteCandidates({
|
|
21400
21590
|
port: config.port,
|
|
21401
21591
|
relayBaseUrl: config.relayBaseUrl,
|
|
21402
21592
|
linkId: identity.link_id,
|
|
21403
21593
|
installId: identity.install_id,
|
|
21404
21594
|
publicKeyPem: identity.public_key_pem,
|
|
21405
|
-
observePublicRoute: true,
|
|
21595
|
+
observePublicRoute: context.observePublicRoute === true,
|
|
21406
21596
|
configuredLanHost: config.lanHost,
|
|
21407
21597
|
fetchImpl: options.fetchImpl
|
|
21408
21598
|
});
|
|
21599
|
+
const routes = await mergeLastReportedPublicRoutes(options.paths, discoveredRoutes);
|
|
21409
21600
|
if (context.publishToRelay) {
|
|
21410
21601
|
options.onNetworkRoutes?.(routes);
|
|
21411
21602
|
}
|
|
@@ -21515,6 +21706,7 @@ function startHermesSessionSyncScheduler(options) {
|
|
|
21515
21706
|
|
|
21516
21707
|
// src/daemon/service.ts
|
|
21517
21708
|
var DEFAULT_RELAY_READY_TIMEOUT_MS = 2e3;
|
|
21709
|
+
var RELAY_RECONNECT_PUBLIC_ROUTE_REFRESH_MIN_INTERVAL_MS = 15 * 6e4;
|
|
21518
21710
|
async function startLinkService(options = {}) {
|
|
21519
21711
|
const paths = options.paths ?? resolveRuntimePaths();
|
|
21520
21712
|
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
@@ -21585,6 +21777,9 @@ async function startLinkService(options = {}) {
|
|
|
21585
21777
|
logger
|
|
21586
21778
|
});
|
|
21587
21779
|
let relay = null;
|
|
21780
|
+
let lanIpMonitor = null;
|
|
21781
|
+
let hasSeenRelayConnected = false;
|
|
21782
|
+
let lastRelayReconnectPublicRouteRefreshAt = 0;
|
|
21588
21783
|
if (identity?.link_id) {
|
|
21589
21784
|
let resolveRelayReady = null;
|
|
21590
21785
|
const relayReady = new Promise((resolve) => {
|
|
@@ -21600,6 +21795,12 @@ async function startLinkService(options = {}) {
|
|
|
21600
21795
|
onStatus: (status) => {
|
|
21601
21796
|
void logger.info("relay_status", status);
|
|
21602
21797
|
if (status.state === "connected") {
|
|
21798
|
+
const now = Date.now();
|
|
21799
|
+
if (hasSeenRelayConnected && lanIpMonitor && now - lastRelayReconnectPublicRouteRefreshAt >= RELAY_RECONNECT_PUBLIC_ROUTE_REFRESH_MIN_INTERVAL_MS) {
|
|
21800
|
+
lastRelayReconnectPublicRouteRefreshAt = now;
|
|
21801
|
+
void lanIpMonitor.refreshPublicRoutes();
|
|
21802
|
+
}
|
|
21803
|
+
hasSeenRelayConnected = true;
|
|
21603
21804
|
resolveRelayReady?.(true);
|
|
21604
21805
|
resolveRelayReady = null;
|
|
21605
21806
|
} else if (status.state === "failed") {
|
|
@@ -21618,7 +21819,7 @@ async function startLinkService(options = {}) {
|
|
|
21618
21819
|
} else {
|
|
21619
21820
|
void logger.info("relay_skipped", { reason: "link_not_paired" });
|
|
21620
21821
|
}
|
|
21621
|
-
|
|
21822
|
+
lanIpMonitor = startLanIpMonitor({
|
|
21622
21823
|
paths,
|
|
21623
21824
|
logger,
|
|
21624
21825
|
intervalMs: options.lanIpMonitorIntervalMs,
|
|
@@ -21638,7 +21839,7 @@ async function startLinkService(options = {}) {
|
|
|
21638
21839
|
await Promise.all([
|
|
21639
21840
|
scheduler.close(),
|
|
21640
21841
|
hermesSessionSyncScheduler.close(),
|
|
21641
|
-
lanIpMonitor
|
|
21842
|
+
lanIpMonitor?.close(),
|
|
21642
21843
|
hermesSessionSync.catch(() => void 0)
|
|
21643
21844
|
]);
|
|
21644
21845
|
await logger.info("service_stopped");
|
package/dist/cli/index.js
CHANGED
package/dist/http/app.js
CHANGED