@hermespilot/link 0.2.2 → 0.2.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-4YF43CT4.js → chunk-HIQBCPY4.js} +843 -67
- package/dist/chunk-HIQBCPY4.js.map +1 -0
- package/dist/cli/index.js +34 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/http/app.d.ts +8 -1
- package/dist/http/app.js +1 -1
- package/package.json +3 -1
- package/dist/chunk-4YF43CT4.js.map +0 -1
|
@@ -4643,7 +4643,7 @@ function toSummary(manifest, snapshot, profile) {
|
|
|
4643
4643
|
}
|
|
4644
4644
|
function isDefaultConversationTitle(title) {
|
|
4645
4645
|
const normalized = title.trim().toLowerCase();
|
|
4646
|
-
return !normalized || normalized === "untitled" || normalized === DEFAULT_CONVERSATION_TITLE;
|
|
4646
|
+
return !normalized || normalized === "untitled" || normalized === DEFAULT_CONVERSATION_TITLE.toLowerCase();
|
|
4647
4647
|
}
|
|
4648
4648
|
function temporaryTitleFromUserMessage(content) {
|
|
4649
4649
|
const normalized = content.replace(MEDIA_TAG_PATTERN, " ").replace(/\s+/gu, " ").trim();
|
|
@@ -4677,7 +4677,7 @@ import os2 from "os";
|
|
|
4677
4677
|
import path7 from "path";
|
|
4678
4678
|
|
|
4679
4679
|
// src/constants.ts
|
|
4680
|
-
var LINK_VERSION = "0.2.
|
|
4680
|
+
var LINK_VERSION = "0.2.3";
|
|
4681
4681
|
var LINK_COMMAND = "hermeslink";
|
|
4682
4682
|
var LINK_DEFAULT_PORT = 52379;
|
|
4683
4683
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -5068,6 +5068,46 @@ async function deleteHermesSession(sessionId, profileName = "default") {
|
|
|
5068
5068
|
);
|
|
5069
5069
|
}
|
|
5070
5070
|
}
|
|
5071
|
+
async function renameHermesSession(sessionId, title, profileName = "default") {
|
|
5072
|
+
const normalizedSessionId = sessionId.trim();
|
|
5073
|
+
const normalizedTitle = title.trim();
|
|
5074
|
+
if (!normalizedSessionId) {
|
|
5075
|
+
throw new LinkHttpError(
|
|
5076
|
+
400,
|
|
5077
|
+
"hermes_session_id_required",
|
|
5078
|
+
"Hermes session id is required"
|
|
5079
|
+
);
|
|
5080
|
+
}
|
|
5081
|
+
if (!normalizedTitle) {
|
|
5082
|
+
throw new LinkHttpError(
|
|
5083
|
+
400,
|
|
5084
|
+
"hermes_session_title_required",
|
|
5085
|
+
"Hermes session title is required"
|
|
5086
|
+
);
|
|
5087
|
+
}
|
|
5088
|
+
try {
|
|
5089
|
+
await execFileAsync(
|
|
5090
|
+
resolveHermesBin(),
|
|
5091
|
+
[
|
|
5092
|
+
...profileArgs(profileName),
|
|
5093
|
+
"sessions",
|
|
5094
|
+
"rename",
|
|
5095
|
+
normalizedSessionId,
|
|
5096
|
+
normalizedTitle
|
|
5097
|
+
],
|
|
5098
|
+
{
|
|
5099
|
+
timeout: 1e4,
|
|
5100
|
+
windowsHide: true
|
|
5101
|
+
}
|
|
5102
|
+
);
|
|
5103
|
+
} catch (error) {
|
|
5104
|
+
throw new LinkHttpError(
|
|
5105
|
+
502,
|
|
5106
|
+
"hermes_session_rename_failed",
|
|
5107
|
+
error instanceof Error ? error.message : "Hermes session rename failed"
|
|
5108
|
+
);
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5071
5111
|
function resolveHermesBin() {
|
|
5072
5112
|
return process.env.HERMES_BIN?.trim() || "hermes";
|
|
5073
5113
|
}
|
|
@@ -5864,22 +5904,22 @@ var ConversationCommandHandlers = class {
|
|
|
5864
5904
|
response: `\u5F53\u524D\u4F1A\u8BDD\u6807\u9898\uFF1A${input.manifest.title}`
|
|
5865
5905
|
});
|
|
5866
5906
|
}
|
|
5867
|
-
await this.deps.
|
|
5868
|
-
|
|
5907
|
+
const result = await this.deps.renameConversation(
|
|
5908
|
+
input.manifest.id,
|
|
5869
5909
|
title,
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5910
|
+
{
|
|
5911
|
+
manifest: input.manifest,
|
|
5912
|
+
snapshot: input.snapshot,
|
|
5913
|
+
source: "manual"
|
|
5914
|
+
}
|
|
5915
|
+
);
|
|
5876
5916
|
return this.deps.appendCommandResult({
|
|
5877
5917
|
manifest: await this.deps.readManifest(input.manifest.id),
|
|
5878
5918
|
snapshot: input.snapshot,
|
|
5879
5919
|
rawContent: input.rawContent,
|
|
5880
5920
|
clientMessageId: input.clientMessageId,
|
|
5881
5921
|
commandName: input.command.name,
|
|
5882
|
-
response: `\u5DF2\u5C06\u5F53\u524D\u4F1A\u8BDD\u6807\u9898\u6539\u4E3A\uFF1A${title}`
|
|
5922
|
+
response: `\u5DF2\u5C06\u5F53\u524D\u4F1A\u8BDD\u6807\u9898\u6539\u4E3A\uFF1A${result.manifest.title}`
|
|
5883
5923
|
});
|
|
5884
5924
|
}
|
|
5885
5925
|
async handleModelCommand(input) {
|
|
@@ -6275,6 +6315,31 @@ function readAttachmentWaveform(attachment) {
|
|
|
6275
6315
|
).filter((item) => item !== void 0).slice(0, 96);
|
|
6276
6316
|
}
|
|
6277
6317
|
|
|
6318
|
+
// src/config/config.ts
|
|
6319
|
+
var defaultLinkConfig = {
|
|
6320
|
+
port: LINK_DEFAULT_PORT,
|
|
6321
|
+
serverBaseUrl: "https://hermes-server.clawpilot.me",
|
|
6322
|
+
relayBaseUrl: "https://hermes-relay.clawpilot.me",
|
|
6323
|
+
appConnectTokenIssuer: "https://hermes-server.clawpilot.me",
|
|
6324
|
+
appConnectTokenAudience: "hermes-link",
|
|
6325
|
+
language: "auto"
|
|
6326
|
+
};
|
|
6327
|
+
async function loadConfig(paths = resolveRuntimePaths()) {
|
|
6328
|
+
const existing = await readJsonFile(paths.configFile);
|
|
6329
|
+
const language = normalizeConfiguredLanguage(existing?.language);
|
|
6330
|
+
return {
|
|
6331
|
+
...defaultLinkConfig,
|
|
6332
|
+
...existing ?? {},
|
|
6333
|
+
language
|
|
6334
|
+
};
|
|
6335
|
+
}
|
|
6336
|
+
function normalizeConfiguredLanguage(language) {
|
|
6337
|
+
if (language === "zh-CN" || language === "en" || language === "auto") {
|
|
6338
|
+
return language;
|
|
6339
|
+
}
|
|
6340
|
+
return defaultLinkConfig.language;
|
|
6341
|
+
}
|
|
6342
|
+
|
|
6278
6343
|
// src/hermes/session-title.ts
|
|
6279
6344
|
import { stat as stat6 } from "fs/promises";
|
|
6280
6345
|
import { createRequire as createRequire2 } from "module";
|
|
@@ -6389,12 +6454,19 @@ function isValidProfileName(value) {
|
|
|
6389
6454
|
}
|
|
6390
6455
|
|
|
6391
6456
|
// src/conversations/conversation-metadata.ts
|
|
6457
|
+
var GENERATED_TITLE_RETRY_DELAYS_MS = [0, 1e3, 3e3];
|
|
6458
|
+
var TITLE_GENERATION_SOURCE_MAX_LENGTH = 12e3;
|
|
6459
|
+
var SESSION_TITLE_MAX_LENGTH = 120;
|
|
6460
|
+
var TITLE_GENERATION_REQUEST_TIMEOUT_MS = 3e4;
|
|
6392
6461
|
var ConversationMetadataCoordinator = class {
|
|
6393
6462
|
constructor(deps) {
|
|
6394
6463
|
this.deps = deps;
|
|
6395
6464
|
}
|
|
6396
6465
|
deps;
|
|
6397
6466
|
async refreshTitleFromHermes(manifest, options = {}) {
|
|
6467
|
+
if (manifest.title_source === "manual" || manifest.title_source === "generated" || manifest.title_source === "temporary_user_message" || manifest.title_source === "temporary_fallback") {
|
|
6468
|
+
return manifest;
|
|
6469
|
+
}
|
|
6398
6470
|
const snapshot = options.snapshot ?? await this.deps.store.readSnapshot(manifest.id).catch(
|
|
6399
6471
|
() => createEmptySnapshot()
|
|
6400
6472
|
);
|
|
@@ -6408,7 +6480,8 @@ var ConversationMetadataCoordinator = class {
|
|
|
6408
6480
|
}
|
|
6409
6481
|
const next = {
|
|
6410
6482
|
...manifest,
|
|
6411
|
-
title
|
|
6483
|
+
title,
|
|
6484
|
+
title_source: "hermes"
|
|
6412
6485
|
};
|
|
6413
6486
|
await this.deps.store.writeManifest(next);
|
|
6414
6487
|
await this.persistConversationStats(manifest.id, snapshot);
|
|
@@ -6421,6 +6494,92 @@ var ConversationMetadataCoordinator = class {
|
|
|
6421
6494
|
});
|
|
6422
6495
|
return { ...next, last_event_seq: event.seq, updated_at: event.created_at };
|
|
6423
6496
|
}
|
|
6497
|
+
scheduleGeneratedTitleRefresh(conversationId) {
|
|
6498
|
+
for (const delay2 of GENERATED_TITLE_RETRY_DELAYS_MS) {
|
|
6499
|
+
setTimeout(() => {
|
|
6500
|
+
void this.generateTitleFromFirstRound(conversationId).catch(
|
|
6501
|
+
() => void 0
|
|
6502
|
+
);
|
|
6503
|
+
}, delay2);
|
|
6504
|
+
}
|
|
6505
|
+
}
|
|
6506
|
+
async renameConversation(conversationId, title, input) {
|
|
6507
|
+
const normalizedTitle = normalizeConversationTitle(title);
|
|
6508
|
+
if (!normalizedTitle) {
|
|
6509
|
+
throw new LinkHttpError(
|
|
6510
|
+
400,
|
|
6511
|
+
"conversation_title_required",
|
|
6512
|
+
"Conversation title is required"
|
|
6513
|
+
);
|
|
6514
|
+
}
|
|
6515
|
+
const sourceChanged = input.manifest.title_source !== input.source;
|
|
6516
|
+
if (normalizedTitle === input.manifest.title) {
|
|
6517
|
+
if (!sourceChanged) {
|
|
6518
|
+
return {
|
|
6519
|
+
manifest: input.manifest,
|
|
6520
|
+
event: {
|
|
6521
|
+
seq: input.manifest.last_event_seq,
|
|
6522
|
+
type: "conversation.updated",
|
|
6523
|
+
conversation_id: conversationId,
|
|
6524
|
+
created_at: input.manifest.updated_at,
|
|
6525
|
+
payload: {
|
|
6526
|
+
title: normalizedTitle,
|
|
6527
|
+
title_source: input.manifest.title_source ?? normalizeConversationTitleSource(input.manifest.title),
|
|
6528
|
+
hermes_synced: false
|
|
6529
|
+
}
|
|
6530
|
+
},
|
|
6531
|
+
hermesSynced: false
|
|
6532
|
+
};
|
|
6533
|
+
}
|
|
6534
|
+
const hermesSynced2 = await this.syncHermesSessionTitle(
|
|
6535
|
+
input.manifest,
|
|
6536
|
+
input.snapshot,
|
|
6537
|
+
normalizedTitle,
|
|
6538
|
+
input.source
|
|
6539
|
+
);
|
|
6540
|
+
const event2 = await this.deps.appendEvent(conversationId, {
|
|
6541
|
+
type: "conversation.updated",
|
|
6542
|
+
payload: {
|
|
6543
|
+
title: normalizedTitle,
|
|
6544
|
+
title_source: input.source,
|
|
6545
|
+
hermes_synced: hermesSynced2
|
|
6546
|
+
}
|
|
6547
|
+
});
|
|
6548
|
+
const next2 = {
|
|
6549
|
+
...input.manifest,
|
|
6550
|
+
title_source: input.source,
|
|
6551
|
+
updated_at: event2.created_at,
|
|
6552
|
+
last_event_seq: event2.seq
|
|
6553
|
+
};
|
|
6554
|
+
await this.deps.store.writeManifest(next2);
|
|
6555
|
+
await this.persistConversationStats(conversationId, input.snapshot);
|
|
6556
|
+
return { manifest: next2, event: event2, hermesSynced: hermesSynced2 };
|
|
6557
|
+
}
|
|
6558
|
+
const hermesSynced = await this.syncHermesSessionTitle(
|
|
6559
|
+
input.manifest,
|
|
6560
|
+
input.snapshot,
|
|
6561
|
+
normalizedTitle,
|
|
6562
|
+
input.source
|
|
6563
|
+
);
|
|
6564
|
+
const event = await this.deps.appendEvent(conversationId, {
|
|
6565
|
+
type: "conversation.updated",
|
|
6566
|
+
payload: {
|
|
6567
|
+
title: normalizedTitle,
|
|
6568
|
+
title_source: input.source,
|
|
6569
|
+
hermes_synced: hermesSynced
|
|
6570
|
+
}
|
|
6571
|
+
});
|
|
6572
|
+
const next = {
|
|
6573
|
+
...input.manifest,
|
|
6574
|
+
title: normalizedTitle,
|
|
6575
|
+
title_source: input.source,
|
|
6576
|
+
updated_at: event.created_at,
|
|
6577
|
+
last_event_seq: event.seq
|
|
6578
|
+
};
|
|
6579
|
+
await this.deps.store.writeManifest(next);
|
|
6580
|
+
await this.persistConversationStats(conversationId, input.snapshot);
|
|
6581
|
+
return { manifest: next, event, hermesSynced };
|
|
6582
|
+
}
|
|
6424
6583
|
async persistConversationStats(conversationId, snapshot, statsOverride) {
|
|
6425
6584
|
const manifest = await this.deps.store.readManifest(conversationId).catch(
|
|
6426
6585
|
() => null
|
|
@@ -6467,16 +6626,152 @@ var ConversationMetadataCoordinator = class {
|
|
|
6467
6626
|
const next = {
|
|
6468
6627
|
...manifest,
|
|
6469
6628
|
title,
|
|
6629
|
+
title_source: title === TEMPORARY_TITLE_FALLBACK ? "temporary_fallback" : "temporary_user_message",
|
|
6470
6630
|
updated_at: event.created_at,
|
|
6471
6631
|
last_event_seq: event.seq
|
|
6472
6632
|
};
|
|
6473
6633
|
await this.deps.store.writeManifest(next);
|
|
6474
6634
|
return next;
|
|
6475
6635
|
}
|
|
6636
|
+
async generateTitleFromFirstRound(conversationId) {
|
|
6637
|
+
const manifest = await this.deps.store.readManifest(conversationId).catch(
|
|
6638
|
+
() => null
|
|
6639
|
+
);
|
|
6640
|
+
if (!manifest || manifest.status !== "active" || !canAutoGenerateTitle(manifest)) {
|
|
6641
|
+
return;
|
|
6642
|
+
}
|
|
6643
|
+
const snapshot = await this.deps.store.readSnapshot(conversationId).catch(
|
|
6644
|
+
() => createEmptySnapshot()
|
|
6645
|
+
);
|
|
6646
|
+
if (snapshot.messages.filter((message) => message.role === "user").length !== 1) {
|
|
6647
|
+
return;
|
|
6648
|
+
}
|
|
6649
|
+
const dialogue = extractFirstRoundTitleSource(snapshot);
|
|
6650
|
+
if (!dialogue) {
|
|
6651
|
+
return;
|
|
6652
|
+
}
|
|
6653
|
+
const title = await requestGeneratedTitleFromServer({
|
|
6654
|
+
paths: this.deps.paths,
|
|
6655
|
+
sessionKey: manifest.id,
|
|
6656
|
+
userMessage: dialogue.userMessage,
|
|
6657
|
+
assistantMessage: dialogue.assistantMessage,
|
|
6658
|
+
attachmentOnly: dialogue.attachmentOnly
|
|
6659
|
+
}).catch(() => null);
|
|
6660
|
+
if (!title) {
|
|
6661
|
+
return;
|
|
6662
|
+
}
|
|
6663
|
+
const latestManifest = await this.deps.store.readManifest(conversationId).catch(
|
|
6664
|
+
() => null
|
|
6665
|
+
);
|
|
6666
|
+
if (!latestManifest || latestManifest.status !== "active" || !canAutoGenerateTitle(latestManifest)) {
|
|
6667
|
+
return;
|
|
6668
|
+
}
|
|
6669
|
+
await this.renameConversation(conversationId, title, {
|
|
6670
|
+
manifest: latestManifest,
|
|
6671
|
+
snapshot,
|
|
6672
|
+
source: "generated"
|
|
6673
|
+
}).catch(() => void 0);
|
|
6674
|
+
}
|
|
6675
|
+
async syncHermesSessionTitle(manifest, snapshot, title, source) {
|
|
6676
|
+
try {
|
|
6677
|
+
await renameHermesSession(
|
|
6678
|
+
manifest.hermes_session_id,
|
|
6679
|
+
title,
|
|
6680
|
+
latestRuntimeRun(snapshot)?.profile ?? manifest.profile
|
|
6681
|
+
);
|
|
6682
|
+
return true;
|
|
6683
|
+
} catch (error) {
|
|
6684
|
+
if (source === "manual") {
|
|
6685
|
+
throw error;
|
|
6686
|
+
}
|
|
6687
|
+
return false;
|
|
6688
|
+
}
|
|
6689
|
+
}
|
|
6476
6690
|
};
|
|
6477
6691
|
function createEmptySnapshot() {
|
|
6478
6692
|
return { schema_version: 1, messages: [], runs: [] };
|
|
6479
6693
|
}
|
|
6694
|
+
function extractFirstRoundTitleSource(snapshot) {
|
|
6695
|
+
const userMessages = snapshot.messages.filter((message) => message.role === "user").sort((left, right) => Date.parse(left.created_at) - Date.parse(right.created_at));
|
|
6696
|
+
if (userMessages.length !== 1) {
|
|
6697
|
+
return null;
|
|
6698
|
+
}
|
|
6699
|
+
const [user] = userMessages;
|
|
6700
|
+
const userText = truncateTitleSourceText(messageText(user));
|
|
6701
|
+
const assistant = snapshot.messages.filter((message) => message.role === "assistant").filter((message) => Date.parse(message.created_at) >= Date.parse(user.created_at)).sort((left, right) => Date.parse(left.created_at) - Date.parse(right.created_at)).find((message) => messageText(message).length > 0);
|
|
6702
|
+
if (!assistant) {
|
|
6703
|
+
return null;
|
|
6704
|
+
}
|
|
6705
|
+
const assistantText = truncateTitleSourceText(messageText(assistant));
|
|
6706
|
+
if (!assistantText) {
|
|
6707
|
+
return null;
|
|
6708
|
+
}
|
|
6709
|
+
const attachmentOnly = !userText && user.parts.some((part) => part.type !== "text");
|
|
6710
|
+
if (!userText && !attachmentOnly) {
|
|
6711
|
+
return null;
|
|
6712
|
+
}
|
|
6713
|
+
return {
|
|
6714
|
+
userMessage: userText,
|
|
6715
|
+
assistantMessage: assistantText,
|
|
6716
|
+
attachmentOnly
|
|
6717
|
+
};
|
|
6718
|
+
}
|
|
6719
|
+
async function requestGeneratedTitleFromServer(input) {
|
|
6720
|
+
const config = await loadConfig(input.paths);
|
|
6721
|
+
const response = await fetch(
|
|
6722
|
+
`${config.serverBaseUrl.replace(/\/+$/u, "")}/api/v1/session-titles/generate`,
|
|
6723
|
+
{
|
|
6724
|
+
method: "POST",
|
|
6725
|
+
signal: AbortSignal.timeout(TITLE_GENERATION_REQUEST_TIMEOUT_MS),
|
|
6726
|
+
headers: {
|
|
6727
|
+
accept: "application/json",
|
|
6728
|
+
"content-type": "application/json"
|
|
6729
|
+
},
|
|
6730
|
+
body: JSON.stringify({
|
|
6731
|
+
sessionKey: input.sessionKey,
|
|
6732
|
+
userMessage: input.userMessage,
|
|
6733
|
+
assistantMessage: input.assistantMessage,
|
|
6734
|
+
attachmentOnly: input.attachmentOnly,
|
|
6735
|
+
scenario: input.attachmentOnly ? "attachment-only" : "first-round"
|
|
6736
|
+
})
|
|
6737
|
+
}
|
|
6738
|
+
);
|
|
6739
|
+
if (!response.ok) {
|
|
6740
|
+
return null;
|
|
6741
|
+
}
|
|
6742
|
+
const payload = await response.json().catch(() => null);
|
|
6743
|
+
return normalizeConversationTitle(payload?.title);
|
|
6744
|
+
}
|
|
6745
|
+
function normalizeConversationTitle(value) {
|
|
6746
|
+
if (typeof value !== "string") {
|
|
6747
|
+
return null;
|
|
6748
|
+
}
|
|
6749
|
+
const normalized = value.replace(/\s+/gu, " ").trim();
|
|
6750
|
+
if (!normalized) {
|
|
6751
|
+
return null;
|
|
6752
|
+
}
|
|
6753
|
+
return Array.from(normalized).slice(0, SESSION_TITLE_MAX_LENGTH).join("");
|
|
6754
|
+
}
|
|
6755
|
+
function truncateTitleSourceText(value) {
|
|
6756
|
+
return Array.from(value.replace(/\s+/gu, " ").trim()).slice(0, TITLE_GENERATION_SOURCE_MAX_LENGTH).join("");
|
|
6757
|
+
}
|
|
6758
|
+
function canAutoGenerateTitle(manifest) {
|
|
6759
|
+
switch (manifest.title_source) {
|
|
6760
|
+
case "manual":
|
|
6761
|
+
case "generated":
|
|
6762
|
+
case "hermes":
|
|
6763
|
+
return false;
|
|
6764
|
+
case "default":
|
|
6765
|
+
case "temporary_user_message":
|
|
6766
|
+
case "temporary_fallback":
|
|
6767
|
+
return true;
|
|
6768
|
+
default:
|
|
6769
|
+
return isDefaultConversationTitle(manifest.title);
|
|
6770
|
+
}
|
|
6771
|
+
}
|
|
6772
|
+
function normalizeConversationTitleSource(title) {
|
|
6773
|
+
return isDefaultConversationTitle(title) ? "default" : "hermes";
|
|
6774
|
+
}
|
|
6480
6775
|
|
|
6481
6776
|
// src/conversations/conversation-turns.ts
|
|
6482
6777
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
@@ -8754,7 +9049,7 @@ var ConversationRunLifecycle = class {
|
|
|
8754
9049
|
const hermesRun = await createHermesRun(
|
|
8755
9050
|
{
|
|
8756
9051
|
input: resolvedInput,
|
|
8757
|
-
instructions:
|
|
9052
|
+
instructions: buildRunInstructions(run),
|
|
8758
9053
|
session_id: hermesSessionId,
|
|
8759
9054
|
model: run.model,
|
|
8760
9055
|
...conversationHistory.messages.length > 0 ? { conversation_history: conversationHistory.messages } : {}
|
|
@@ -9467,6 +9762,16 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
9467
9762
|
});
|
|
9468
9763
|
}
|
|
9469
9764
|
};
|
|
9765
|
+
function buildRunInstructions(run) {
|
|
9766
|
+
return [
|
|
9767
|
+
HERMES_LINK_DELIVERY_INSTRUCTIONS,
|
|
9768
|
+
"",
|
|
9769
|
+
"Current runtime selected by Hermes Link:",
|
|
9770
|
+
`- Model: ${run.model ?? "hermes-agent"}`,
|
|
9771
|
+
`- Provider: ${run.provider ?? "unknown"}`,
|
|
9772
|
+
"If the user asks what model or provider you are currently using, answer from this runtime selection instead of inferring from earlier conversation history."
|
|
9773
|
+
].join("\n");
|
|
9774
|
+
}
|
|
9470
9775
|
async function readdirWithDirs(directory) {
|
|
9471
9776
|
return readdir5(directory, { withFileTypes: true }).catch((error) => {
|
|
9472
9777
|
if (isNodeError10(error, "ENOENT")) {
|
|
@@ -9604,6 +9909,7 @@ var ConversationService = class {
|
|
|
9604
9909
|
hasActiveRuns: () => this.activeRunControllers.size > 0,
|
|
9605
9910
|
appendEvent: (conversationId, input) => this.appendEvent(conversationId, input),
|
|
9606
9911
|
appendCommandResult: (input) => this.orchestration.appendCommandResult(input),
|
|
9912
|
+
renameConversation: (conversationId, title, input) => this.metadata.renameConversation(conversationId, title, input),
|
|
9607
9913
|
readManifest: (conversationId) => this.store.readManifest(conversationId),
|
|
9608
9914
|
writeManifest: (manifest) => this.store.writeManifest(manifest)
|
|
9609
9915
|
});
|
|
@@ -9624,7 +9930,7 @@ var ConversationService = class {
|
|
|
9624
9930
|
isConversationActive: (conversationId) => this.store.isConversationActive(conversationId),
|
|
9625
9931
|
writeBlob: (conversationId, input) => this.maintenance.writeBlob(conversationId, input),
|
|
9626
9932
|
syncCronDeliveries: () => this.syncCronDeliveries(),
|
|
9627
|
-
scheduleTitleRefresh: (conversationId) => this.
|
|
9933
|
+
scheduleTitleRefresh: (conversationId) => this.metadata.scheduleGeneratedTitleRefresh(conversationId)
|
|
9628
9934
|
});
|
|
9629
9935
|
this.orchestration = new ConversationOrchestrationCoordinator({
|
|
9630
9936
|
paths: this.paths,
|
|
@@ -9723,6 +10029,7 @@ var ConversationService = class {
|
|
|
9723
10029
|
schema_version: 1,
|
|
9724
10030
|
kind: "direct",
|
|
9725
10031
|
title,
|
|
10032
|
+
title_source: isDefaultConversationTitle(title) ? "default" : "hermes",
|
|
9726
10033
|
status: "active",
|
|
9727
10034
|
hermes_session_id: `hp_${id}`,
|
|
9728
10035
|
profile_uid: profile.profileUid,
|
|
@@ -9776,6 +10083,10 @@ var ConversationService = class {
|
|
|
9776
10083
|
title: "HermesLink \u5B9A\u65F6\u4EFB\u52A1",
|
|
9777
10084
|
profileName
|
|
9778
10085
|
});
|
|
10086
|
+
await this.store.writeManifest({
|
|
10087
|
+
...await this.store.readActiveManifest(created.id),
|
|
10088
|
+
title_source: "manual"
|
|
10089
|
+
});
|
|
9779
10090
|
return created.id;
|
|
9780
10091
|
}
|
|
9781
10092
|
async appendCronDelivery(input) {
|
|
@@ -9985,6 +10296,34 @@ var ConversationService = class {
|
|
|
9985
10296
|
};
|
|
9986
10297
|
});
|
|
9987
10298
|
}
|
|
10299
|
+
async renameConversation(conversationId, title) {
|
|
10300
|
+
return this.withConversationLock(conversationId, async () => {
|
|
10301
|
+
const manifest = await this.store.readActiveManifest(conversationId);
|
|
10302
|
+
const snapshot = await this.store.readSnapshot(conversationId);
|
|
10303
|
+
return this.renameConversationLocked(conversationId, title, {
|
|
10304
|
+
manifest,
|
|
10305
|
+
snapshot,
|
|
10306
|
+
source: "manual"
|
|
10307
|
+
});
|
|
10308
|
+
});
|
|
10309
|
+
}
|
|
10310
|
+
async renameConversationLocked(conversationId, title, input) {
|
|
10311
|
+
const result = await this.metadata.renameConversation(
|
|
10312
|
+
conversationId,
|
|
10313
|
+
title,
|
|
10314
|
+
input
|
|
10315
|
+
);
|
|
10316
|
+
return {
|
|
10317
|
+
conversation_id: conversationId,
|
|
10318
|
+
title: result.manifest.title,
|
|
10319
|
+
conversation: await this.queries.summarizeConversation(
|
|
10320
|
+
result.manifest,
|
|
10321
|
+
input.snapshot
|
|
10322
|
+
),
|
|
10323
|
+
hermes_synced: result.hermesSynced,
|
|
10324
|
+
last_event_seq: result.event.seq
|
|
10325
|
+
};
|
|
10326
|
+
}
|
|
9988
10327
|
async listEvents(conversationId, after = 0) {
|
|
9989
10328
|
return this.queries.listEvents(conversationId, after);
|
|
9990
10329
|
}
|
|
@@ -10096,32 +10435,6 @@ var ConversationService = class {
|
|
|
10096
10435
|
async deleteUnreferencedBlob(conversationId, blobId) {
|
|
10097
10436
|
return this.maintenance.deleteUnreferencedBlob(conversationId, blobId);
|
|
10098
10437
|
}
|
|
10099
|
-
refreshTitleFromHermesWithRetries(conversationId) {
|
|
10100
|
-
for (const delay2 of [0, 1e3, 3e3]) {
|
|
10101
|
-
setTimeout(() => {
|
|
10102
|
-
void this.withConversationLock(conversationId, async () => {
|
|
10103
|
-
const manifest = await this.store.readManifest(conversationId).catch(
|
|
10104
|
-
() => null
|
|
10105
|
-
);
|
|
10106
|
-
if (!manifest || manifest.status !== "active") {
|
|
10107
|
-
return;
|
|
10108
|
-
}
|
|
10109
|
-
const snapshot = await this.store.readSnapshot(conversationId).catch(
|
|
10110
|
-
() => emptySnapshot2()
|
|
10111
|
-
);
|
|
10112
|
-
await this.refreshTitleFromHermes(manifest, {
|
|
10113
|
-
emitEvent: true,
|
|
10114
|
-
snapshot
|
|
10115
|
-
});
|
|
10116
|
-
}).catch((error) => {
|
|
10117
|
-
void this.logger.warn("conversation_title_sync_failed", {
|
|
10118
|
-
conversation_id: conversationId,
|
|
10119
|
-
error: error instanceof Error ? error.message : String(error)
|
|
10120
|
-
});
|
|
10121
|
-
});
|
|
10122
|
-
}, delay2);
|
|
10123
|
-
}
|
|
10124
|
-
}
|
|
10125
10438
|
async refreshTitleFromHermes(manifest, options = {}) {
|
|
10126
10439
|
return this.metadata.refreshTitleFromHermes(manifest, options);
|
|
10127
10440
|
}
|
|
@@ -10166,31 +10479,6 @@ function conversationMatchesProfile(manifest, profileName, profileUid) {
|
|
|
10166
10479
|
return normalizeProfileName(manifest.profile_name_snapshot ?? manifest.profile) === profileName;
|
|
10167
10480
|
}
|
|
10168
10481
|
|
|
10169
|
-
// src/config/config.ts
|
|
10170
|
-
var defaultLinkConfig = {
|
|
10171
|
-
port: LINK_DEFAULT_PORT,
|
|
10172
|
-
serverBaseUrl: "https://hermes-server.clawpilot.me",
|
|
10173
|
-
relayBaseUrl: "https://hermes-relay.clawpilot.me",
|
|
10174
|
-
appConnectTokenIssuer: "https://hermes-server.clawpilot.me",
|
|
10175
|
-
appConnectTokenAudience: "hermes-link",
|
|
10176
|
-
language: "auto"
|
|
10177
|
-
};
|
|
10178
|
-
async function loadConfig(paths = resolveRuntimePaths()) {
|
|
10179
|
-
const existing = await readJsonFile(paths.configFile);
|
|
10180
|
-
const language = normalizeConfiguredLanguage(existing?.language);
|
|
10181
|
-
return {
|
|
10182
|
-
...defaultLinkConfig,
|
|
10183
|
-
...existing ?? {},
|
|
10184
|
-
language
|
|
10185
|
-
};
|
|
10186
|
-
}
|
|
10187
|
-
function normalizeConfiguredLanguage(language) {
|
|
10188
|
-
if (language === "zh-CN" || language === "en" || language === "auto") {
|
|
10189
|
-
return language;
|
|
10190
|
-
}
|
|
10191
|
-
return defaultLinkConfig.language;
|
|
10192
|
-
}
|
|
10193
|
-
|
|
10194
10482
|
// src/identity/identity.ts
|
|
10195
10483
|
import { generateKeyPairSync, randomUUID as randomUUID7, sign } from "crypto";
|
|
10196
10484
|
import { mkdir as mkdir8, chmod } from "fs/promises";
|
|
@@ -11058,6 +11346,21 @@ function registerConversationRoutes(router, options) {
|
|
|
11058
11346
|
)
|
|
11059
11347
|
};
|
|
11060
11348
|
});
|
|
11349
|
+
router.patch("/api/v1/conversations/:conversationId/title", async (ctx) => {
|
|
11350
|
+
await authenticateRequest(ctx, paths);
|
|
11351
|
+
const body = await readJsonBody(ctx.req);
|
|
11352
|
+
const title = readString11(body, "title") ?? readString11(body, "name") ?? readString11(body, "display_name");
|
|
11353
|
+
if (!title) {
|
|
11354
|
+
throw new LinkHttpError(400, "title_required", "title is required");
|
|
11355
|
+
}
|
|
11356
|
+
ctx.body = {
|
|
11357
|
+
ok: true,
|
|
11358
|
+
...await conversations.renameConversation(
|
|
11359
|
+
ctx.params.conversationId,
|
|
11360
|
+
title
|
|
11361
|
+
)
|
|
11362
|
+
};
|
|
11363
|
+
});
|
|
11061
11364
|
router.post("/api/v1/conversations/:conversationId/ack", async (ctx) => {
|
|
11062
11365
|
await authenticateRequest(ctx, paths);
|
|
11063
11366
|
ctx.body = { ok: true };
|
|
@@ -17141,6 +17444,22 @@ async function preparePairing(paths = resolveRuntimePaths()) {
|
|
|
17141
17444
|
code: created.code,
|
|
17142
17445
|
preferred_urls: qrPreferredUrls(routes)
|
|
17143
17446
|
};
|
|
17447
|
+
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3).toISOString();
|
|
17448
|
+
await writePairingSession(
|
|
17449
|
+
{
|
|
17450
|
+
session_id: created.sessionId,
|
|
17451
|
+
code: created.code,
|
|
17452
|
+
link_id: assigned.linkId,
|
|
17453
|
+
display_name: defaultDisplayName(),
|
|
17454
|
+
local_api_url: `http://127.0.0.1:${config.port}`,
|
|
17455
|
+
server_base_url: config.serverBaseUrl,
|
|
17456
|
+
relay_base_url: relayBaseUrl,
|
|
17457
|
+
preferred_urls: qrPreferredUrls(routes),
|
|
17458
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17459
|
+
expires_at: expiresAt
|
|
17460
|
+
},
|
|
17461
|
+
paths
|
|
17462
|
+
);
|
|
17144
17463
|
return {
|
|
17145
17464
|
sessionId: created.sessionId,
|
|
17146
17465
|
code: created.code,
|
|
@@ -17150,7 +17469,30 @@ async function preparePairing(paths = resolveRuntimePaths()) {
|
|
|
17150
17469
|
displayName: defaultDisplayName(),
|
|
17151
17470
|
linkId: assigned.linkId,
|
|
17152
17471
|
routes,
|
|
17153
|
-
qrPayload
|
|
17472
|
+
qrPayload,
|
|
17473
|
+
expiresAt
|
|
17474
|
+
};
|
|
17475
|
+
}
|
|
17476
|
+
async function writePairingSession(input, paths = resolveRuntimePaths()) {
|
|
17477
|
+
await writeJsonFile(pairingSessionPath(input.session_id, paths), input);
|
|
17478
|
+
return input;
|
|
17479
|
+
}
|
|
17480
|
+
async function readPairingSession(sessionId, paths = resolveRuntimePaths()) {
|
|
17481
|
+
const record = await readJsonFile(pairingSessionPath(sessionId, paths));
|
|
17482
|
+
if (!record || record.session_id !== sessionId || typeof record.code !== "string" || typeof record.link_id !== "string" || typeof record.display_name !== "string" || typeof record.local_api_url !== "string" || typeof record.server_base_url !== "string" || typeof record.relay_base_url !== "string" || !Array.isArray(record.preferred_urls) || typeof record.created_at !== "string" || typeof record.expires_at !== "string") {
|
|
17483
|
+
return null;
|
|
17484
|
+
}
|
|
17485
|
+
return {
|
|
17486
|
+
session_id: record.session_id,
|
|
17487
|
+
code: record.code,
|
|
17488
|
+
link_id: record.link_id,
|
|
17489
|
+
display_name: record.display_name,
|
|
17490
|
+
local_api_url: record.local_api_url,
|
|
17491
|
+
server_base_url: record.server_base_url,
|
|
17492
|
+
relay_base_url: record.relay_base_url,
|
|
17493
|
+
preferred_urls: record.preferred_urls.filter((value) => typeof value === "string"),
|
|
17494
|
+
created_at: record.created_at,
|
|
17495
|
+
expires_at: record.expires_at
|
|
17154
17496
|
};
|
|
17155
17497
|
}
|
|
17156
17498
|
async function recordPairingClaim(input, paths = resolveRuntimePaths()) {
|
|
@@ -17274,6 +17616,9 @@ function defaultDisplayName() {
|
|
|
17274
17616
|
function pairingClaimPath(sessionId, paths) {
|
|
17275
17617
|
return path22.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
|
|
17276
17618
|
}
|
|
17619
|
+
function pairingSessionPath(sessionId, paths) {
|
|
17620
|
+
return path22.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
|
|
17621
|
+
}
|
|
17277
17622
|
function qrPreferredUrls(routes) {
|
|
17278
17623
|
return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
|
|
17279
17624
|
}
|
|
@@ -17326,6 +17671,7 @@ function registerSystemRoutes(router, options) {
|
|
|
17326
17671
|
conversation_delete: true,
|
|
17327
17672
|
conversation_bulk_delete: true,
|
|
17328
17673
|
conversation_cancel: true,
|
|
17674
|
+
conversation_rename: true,
|
|
17329
17675
|
blobs: true,
|
|
17330
17676
|
devices: true,
|
|
17331
17677
|
device_delete: true,
|
|
@@ -17887,6 +18233,435 @@ function writeUpdateSseEvent2(response, status) {
|
|
|
17887
18233
|
`);
|
|
17888
18234
|
}
|
|
17889
18235
|
|
|
18236
|
+
// src/http/routes/pairing.ts
|
|
18237
|
+
import QRCode from "qrcode";
|
|
18238
|
+
function registerPairingRoutes(router, options) {
|
|
18239
|
+
const { paths } = options;
|
|
18240
|
+
router.get("/pair", async (ctx) => {
|
|
18241
|
+
const sessionId = readString11(ctx.query, "session_id");
|
|
18242
|
+
if (!sessionId) {
|
|
18243
|
+
throw new LinkHttpError(400, "pairing_session_required", "session_id is required");
|
|
18244
|
+
}
|
|
18245
|
+
const [session, config, identity] = await Promise.all([
|
|
18246
|
+
readPairingSession(sessionId, paths),
|
|
18247
|
+
loadConfig(paths),
|
|
18248
|
+
loadIdentity(paths)
|
|
18249
|
+
]);
|
|
18250
|
+
if (!session) {
|
|
18251
|
+
throw new LinkHttpError(404, "pairing_session_not_found", "Pairing session was not found");
|
|
18252
|
+
}
|
|
18253
|
+
const state = await readPairingState(sessionId, paths);
|
|
18254
|
+
const page = await renderPairingPage({
|
|
18255
|
+
session,
|
|
18256
|
+
state,
|
|
18257
|
+
version: LINK_VERSION,
|
|
18258
|
+
port: config.port,
|
|
18259
|
+
linkId: identity?.link_id ?? session.link_id
|
|
18260
|
+
});
|
|
18261
|
+
ctx.set("content-type", "text/html; charset=utf-8");
|
|
18262
|
+
ctx.set("cache-control", "no-store");
|
|
18263
|
+
ctx.body = page;
|
|
18264
|
+
});
|
|
18265
|
+
router.get("/api/v1/pairing/session", async (ctx) => {
|
|
18266
|
+
const sessionId = readString11(ctx.query, "session_id");
|
|
18267
|
+
if (!sessionId) {
|
|
18268
|
+
throw new LinkHttpError(400, "pairing_session_required", "session_id is required");
|
|
18269
|
+
}
|
|
18270
|
+
const session = await readPairingSession(sessionId, paths);
|
|
18271
|
+
if (!session) {
|
|
18272
|
+
throw new LinkHttpError(404, "pairing_session_not_found", "Pairing session was not found");
|
|
18273
|
+
}
|
|
18274
|
+
ctx.set("cache-control", "no-store");
|
|
18275
|
+
ctx.body = {
|
|
18276
|
+
ok: true,
|
|
18277
|
+
session: {
|
|
18278
|
+
...session,
|
|
18279
|
+
claimed: Boolean(await readPairingState(sessionId, paths).then((state) => state.claimed))
|
|
18280
|
+
}
|
|
18281
|
+
};
|
|
18282
|
+
});
|
|
18283
|
+
}
|
|
18284
|
+
async function readPairingState(sessionId, paths) {
|
|
18285
|
+
const claim = await readPairingClaim(sessionId, paths);
|
|
18286
|
+
const session = await readPairingSession(sessionId, paths);
|
|
18287
|
+
return {
|
|
18288
|
+
claimed: Boolean(claim),
|
|
18289
|
+
claim,
|
|
18290
|
+
expiresAt: session?.expires_at ?? null
|
|
18291
|
+
};
|
|
18292
|
+
}
|
|
18293
|
+
async function renderPairingPage(input) {
|
|
18294
|
+
const session = input.session;
|
|
18295
|
+
const qrPayload = JSON.stringify({
|
|
18296
|
+
kind: "hermes_link_pairing",
|
|
18297
|
+
version: 1,
|
|
18298
|
+
link_id: session.link_id,
|
|
18299
|
+
display_name: session.display_name,
|
|
18300
|
+
session_id: session.session_id,
|
|
18301
|
+
code: session.code,
|
|
18302
|
+
preferred_urls: session.preferred_urls
|
|
18303
|
+
});
|
|
18304
|
+
const qrSvg = await QRCode.toString(qrPayload, {
|
|
18305
|
+
type: "svg",
|
|
18306
|
+
margin: 1,
|
|
18307
|
+
width: 320,
|
|
18308
|
+
errorCorrectionLevel: "M"
|
|
18309
|
+
});
|
|
18310
|
+
const qrDataUri = `data:image/svg+xml;base64,${Buffer.from(qrSvg).toString("base64")}`;
|
|
18311
|
+
const currentUrl = session.local_api_url.replace(/\/+$/u, "");
|
|
18312
|
+
const linkIdLabel = escapeHtml(input.linkId ?? session.link_id);
|
|
18313
|
+
const expiresLabel = escapeHtml(formatDate(session.expires_at));
|
|
18314
|
+
const statusLabel = input.state.claimed ? "\u5DF2\u5B8C\u6210\u914D\u5BF9" : "\u7B49\u5F85 App \u626B\u7801";
|
|
18315
|
+
const statusHint = input.state.claimed ? "App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002" : "\u6253\u5F00 App \u626B\u7801\uFF0C\u6216\u8005\u590D\u5236\u914D\u5BF9\u7801\u624B\u52A8\u8F93\u5165\u3002";
|
|
18316
|
+
return `<!doctype html>
|
|
18317
|
+
<html lang="zh-CN">
|
|
18318
|
+
<head>
|
|
18319
|
+
<meta charset="utf-8" />
|
|
18320
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
18321
|
+
<meta name="color-scheme" content="light dark" />
|
|
18322
|
+
<title>Hermes Link Pairing</title>
|
|
18323
|
+
<style>
|
|
18324
|
+
:root {
|
|
18325
|
+
color-scheme: light dark;
|
|
18326
|
+
--bg: #f4f5f7;
|
|
18327
|
+
--panel: rgba(255, 255, 255, 0.78);
|
|
18328
|
+
--panel-strong: rgba(255, 255, 255, 0.94);
|
|
18329
|
+
--text: #151922;
|
|
18330
|
+
--muted: #5f6673;
|
|
18331
|
+
--line: rgba(21, 25, 34, 0.12);
|
|
18332
|
+
--accent: #2d5cff;
|
|
18333
|
+
--accent-soft: rgba(45, 92, 255, 0.12);
|
|
18334
|
+
--good: #0b8457;
|
|
18335
|
+
--shadow: 0 24px 90px rgba(18, 24, 38, 0.12);
|
|
18336
|
+
}
|
|
18337
|
+
@media (prefers-color-scheme: dark) {
|
|
18338
|
+
:root {
|
|
18339
|
+
--bg: #0c1017;
|
|
18340
|
+
--panel: rgba(16, 20, 28, 0.78);
|
|
18341
|
+
--panel-strong: rgba(16, 20, 28, 0.94);
|
|
18342
|
+
--text: #eef2f8;
|
|
18343
|
+
--muted: #9ba4b3;
|
|
18344
|
+
--line: rgba(255, 255, 255, 0.12);
|
|
18345
|
+
--accent: #8ab4ff;
|
|
18346
|
+
--accent-soft: rgba(138, 180, 255, 0.12);
|
|
18347
|
+
--good: #67d7a7;
|
|
18348
|
+
--shadow: 0 24px 90px rgba(0, 0, 0, 0.45);
|
|
18349
|
+
}
|
|
18350
|
+
}
|
|
18351
|
+
* { box-sizing: border-box; }
|
|
18352
|
+
body {
|
|
18353
|
+
margin: 0;
|
|
18354
|
+
min-height: 100vh;
|
|
18355
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
18356
|
+
color: var(--text);
|
|
18357
|
+
background: linear-gradient(180deg, var(--bg) 0%, color-mix(in srgb, var(--bg) 88%, var(--accent) 12%) 100%);
|
|
18358
|
+
}
|
|
18359
|
+
.shell {
|
|
18360
|
+
min-height: 100vh;
|
|
18361
|
+
display: grid;
|
|
18362
|
+
place-items: center;
|
|
18363
|
+
padding: 28px 18px;
|
|
18364
|
+
}
|
|
18365
|
+
.panel {
|
|
18366
|
+
width: min(1040px, 100%);
|
|
18367
|
+
border: 1px solid var(--line);
|
|
18368
|
+
border-radius: 28px;
|
|
18369
|
+
background: var(--panel);
|
|
18370
|
+
box-shadow: var(--shadow);
|
|
18371
|
+
backdrop-filter: blur(18px);
|
|
18372
|
+
overflow: hidden;
|
|
18373
|
+
}
|
|
18374
|
+
.hero {
|
|
18375
|
+
display: grid;
|
|
18376
|
+
grid-template-columns: minmax(0, 1.1fr) minmax(320px, 390px);
|
|
18377
|
+
gap: 0;
|
|
18378
|
+
}
|
|
18379
|
+
.copy {
|
|
18380
|
+
padding: 34px 34px 30px;
|
|
18381
|
+
border-right: 1px solid var(--line);
|
|
18382
|
+
}
|
|
18383
|
+
.eyebrow {
|
|
18384
|
+
display: inline-flex;
|
|
18385
|
+
align-items: center;
|
|
18386
|
+
gap: 10px;
|
|
18387
|
+
padding: 8px 12px;
|
|
18388
|
+
border-radius: 999px;
|
|
18389
|
+
background: var(--accent-soft);
|
|
18390
|
+
color: var(--accent);
|
|
18391
|
+
font-size: 13px;
|
|
18392
|
+
font-weight: 600;
|
|
18393
|
+
letter-spacing: 0;
|
|
18394
|
+
}
|
|
18395
|
+
h1 {
|
|
18396
|
+
margin: 18px 0 12px;
|
|
18397
|
+
font-size: clamp(34px, 4vw, 52px);
|
|
18398
|
+
line-height: 1.02;
|
|
18399
|
+
letter-spacing: 0;
|
|
18400
|
+
}
|
|
18401
|
+
.subtitle {
|
|
18402
|
+
max-width: 42ch;
|
|
18403
|
+
margin: 0;
|
|
18404
|
+
color: var(--muted);
|
|
18405
|
+
font-size: 16px;
|
|
18406
|
+
line-height: 1.7;
|
|
18407
|
+
}
|
|
18408
|
+
.meta-grid {
|
|
18409
|
+
display: grid;
|
|
18410
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
18411
|
+
gap: 14px;
|
|
18412
|
+
margin-top: 26px;
|
|
18413
|
+
}
|
|
18414
|
+
.meta {
|
|
18415
|
+
padding: 16px 16px 15px;
|
|
18416
|
+
border-radius: 18px;
|
|
18417
|
+
background: var(--panel-strong);
|
|
18418
|
+
border: 1px solid var(--line);
|
|
18419
|
+
}
|
|
18420
|
+
.meta-label {
|
|
18421
|
+
display: block;
|
|
18422
|
+
color: var(--muted);
|
|
18423
|
+
font-size: 12px;
|
|
18424
|
+
line-height: 1.4;
|
|
18425
|
+
margin-bottom: 8px;
|
|
18426
|
+
}
|
|
18427
|
+
.meta-value {
|
|
18428
|
+
font-size: 15px;
|
|
18429
|
+
line-height: 1.5;
|
|
18430
|
+
word-break: break-word;
|
|
18431
|
+
}
|
|
18432
|
+
.stack {
|
|
18433
|
+
margin-top: 24px;
|
|
18434
|
+
display: grid;
|
|
18435
|
+
gap: 12px;
|
|
18436
|
+
}
|
|
18437
|
+
.steps {
|
|
18438
|
+
display: grid;
|
|
18439
|
+
gap: 10px;
|
|
18440
|
+
margin-top: 18px;
|
|
18441
|
+
}
|
|
18442
|
+
.step {
|
|
18443
|
+
display: flex;
|
|
18444
|
+
gap: 12px;
|
|
18445
|
+
align-items: flex-start;
|
|
18446
|
+
padding: 14px 16px;
|
|
18447
|
+
border: 1px solid var(--line);
|
|
18448
|
+
border-radius: 18px;
|
|
18449
|
+
background: var(--panel-strong);
|
|
18450
|
+
}
|
|
18451
|
+
.step-badge {
|
|
18452
|
+
flex: none;
|
|
18453
|
+
width: 26px;
|
|
18454
|
+
height: 26px;
|
|
18455
|
+
border-radius: 999px;
|
|
18456
|
+
display: grid;
|
|
18457
|
+
place-items: center;
|
|
18458
|
+
background: var(--accent-soft);
|
|
18459
|
+
color: var(--accent);
|
|
18460
|
+
font-size: 13px;
|
|
18461
|
+
font-weight: 600;
|
|
18462
|
+
}
|
|
18463
|
+
.step-title {
|
|
18464
|
+
font-size: 14px;
|
|
18465
|
+
line-height: 1.45;
|
|
18466
|
+
margin: 0;
|
|
18467
|
+
font-weight: 600;
|
|
18468
|
+
}
|
|
18469
|
+
.step-copy {
|
|
18470
|
+
margin: 3px 0 0;
|
|
18471
|
+
color: var(--muted);
|
|
18472
|
+
font-size: 13px;
|
|
18473
|
+
line-height: 1.55;
|
|
18474
|
+
}
|
|
18475
|
+
.qr {
|
|
18476
|
+
padding: 26px 26px 30px;
|
|
18477
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0));
|
|
18478
|
+
}
|
|
18479
|
+
.card {
|
|
18480
|
+
border: 1px solid var(--line);
|
|
18481
|
+
border-radius: 24px;
|
|
18482
|
+
background: var(--panel-strong);
|
|
18483
|
+
padding: 20px;
|
|
18484
|
+
}
|
|
18485
|
+
.status {
|
|
18486
|
+
display: flex;
|
|
18487
|
+
justify-content: space-between;
|
|
18488
|
+
align-items: center;
|
|
18489
|
+
gap: 12px;
|
|
18490
|
+
margin-bottom: 18px;
|
|
18491
|
+
}
|
|
18492
|
+
.status-title {
|
|
18493
|
+
margin: 0;
|
|
18494
|
+
font-size: 18px;
|
|
18495
|
+
line-height: 1.3;
|
|
18496
|
+
}
|
|
18497
|
+
.pill {
|
|
18498
|
+
display: inline-flex;
|
|
18499
|
+
align-items: center;
|
|
18500
|
+
justify-content: center;
|
|
18501
|
+
padding: 7px 11px;
|
|
18502
|
+
border-radius: 999px;
|
|
18503
|
+
background: rgba(11, 132, 87, 0.12);
|
|
18504
|
+
color: var(--good);
|
|
18505
|
+
font-size: 12px;
|
|
18506
|
+
font-weight: 600;
|
|
18507
|
+
white-space: nowrap;
|
|
18508
|
+
}
|
|
18509
|
+
.qr-frame {
|
|
18510
|
+
display: grid;
|
|
18511
|
+
place-items: center;
|
|
18512
|
+
padding: 18px;
|
|
18513
|
+
border-radius: 24px;
|
|
18514
|
+
background: linear-gradient(180deg, rgba(45, 92, 255, 0.06), rgba(45, 92, 255, 0));
|
|
18515
|
+
border: 1px solid var(--line);
|
|
18516
|
+
}
|
|
18517
|
+
.qr-frame img {
|
|
18518
|
+
width: min(100%, 300px);
|
|
18519
|
+
aspect-ratio: 1;
|
|
18520
|
+
display: block;
|
|
18521
|
+
border-radius: 18px;
|
|
18522
|
+
background: #fff;
|
|
18523
|
+
padding: 14px;
|
|
18524
|
+
}
|
|
18525
|
+
.code {
|
|
18526
|
+
margin-top: 16px;
|
|
18527
|
+
padding: 14px 16px;
|
|
18528
|
+
border-radius: 18px;
|
|
18529
|
+
border: 1px solid var(--line);
|
|
18530
|
+
background: rgba(0, 0, 0, 0.03);
|
|
18531
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
18532
|
+
font-size: 22px;
|
|
18533
|
+
letter-spacing: 0.18em;
|
|
18534
|
+
text-align: center;
|
|
18535
|
+
user-select: all;
|
|
18536
|
+
}
|
|
18537
|
+
.footer {
|
|
18538
|
+
display: flex;
|
|
18539
|
+
justify-content: space-between;
|
|
18540
|
+
gap: 18px;
|
|
18541
|
+
flex-wrap: wrap;
|
|
18542
|
+
padding-top: 16px;
|
|
18543
|
+
color: var(--muted);
|
|
18544
|
+
font-size: 13px;
|
|
18545
|
+
line-height: 1.55;
|
|
18546
|
+
}
|
|
18547
|
+
.hint {
|
|
18548
|
+
margin: 10px 0 0;
|
|
18549
|
+
color: var(--muted);
|
|
18550
|
+
font-size: 13px;
|
|
18551
|
+
line-height: 1.55;
|
|
18552
|
+
}
|
|
18553
|
+
@media (max-width: 920px) {
|
|
18554
|
+
.hero { grid-template-columns: 1fr; }
|
|
18555
|
+
.copy { border-right: none; border-bottom: 1px solid var(--line); }
|
|
18556
|
+
}
|
|
18557
|
+
@media (max-width: 640px) {
|
|
18558
|
+
.copy, .qr { padding: 22px 18px; }
|
|
18559
|
+
.meta-grid { grid-template-columns: 1fr; }
|
|
18560
|
+
.status { align-items: flex-start; flex-direction: column; }
|
|
18561
|
+
.code { font-size: 18px; letter-spacing: 0.14em; }
|
|
18562
|
+
}
|
|
18563
|
+
</style>
|
|
18564
|
+
</head>
|
|
18565
|
+
<body>
|
|
18566
|
+
<main class="shell">
|
|
18567
|
+
<section class="panel">
|
|
18568
|
+
<div class="hero">
|
|
18569
|
+
<div class="copy">
|
|
18570
|
+
<span class="eyebrow">Hermes Link \xB7 ${escapeHtml(input.version)}</span>
|
|
18571
|
+
<h1>\u5728 App \u91CC\u5B8C\u6210\u8FD9\u6B21\u914D\u5BF9</h1>
|
|
18572
|
+
<p class="subtitle">\u8FD9\u4E0D\u662F\u4E00\u5F20\u5B64\u96F6\u96F6\u7684\u4E8C\u7EF4\u7801\u3002\u5B83\u540C\u65F6\u7ED9\u4F60\u914D\u5BF9\u72B6\u6001\u3001\u624B\u52A8\u7801\u3001\u6709\u6548\u671F\u548C\u5F53\u524D\u672C\u5730\u5730\u5740\uFF0C\u65B9\u4FBF Windows \u4F20\u7EDF\u63A7\u5236\u53F0\u3001\u8FDC\u7A0B\u7EC8\u7AEF\u548C\u6D4F\u89C8\u5668\u6253\u5F00\u65F6\u90FD\u80FD\u987A\u5229\u5B8C\u6210\u3002</p>
|
|
18573
|
+
<div class="meta-grid">
|
|
18574
|
+
<div class="meta">
|
|
18575
|
+
<span class="meta-label">\u672C\u5730\u5730\u5740</span>
|
|
18576
|
+
<div class="meta-value">${escapeHtml(currentUrl)}</div>
|
|
18577
|
+
</div>
|
|
18578
|
+
<div class="meta">
|
|
18579
|
+
<span class="meta-label">Link ID</span>
|
|
18580
|
+
<div class="meta-value">${linkIdLabel}</div>
|
|
18581
|
+
</div>
|
|
18582
|
+
<div class="meta">
|
|
18583
|
+
<span class="meta-label">\u914D\u5BF9\u7801</span>
|
|
18584
|
+
<div class="meta-value">${escapeHtml(session.code)}</div>
|
|
18585
|
+
</div>
|
|
18586
|
+
<div class="meta">
|
|
18587
|
+
<span class="meta-label">\u8FC7\u671F\u65F6\u95F4</span>
|
|
18588
|
+
<div class="meta-value">${expiresLabel}</div>
|
|
18589
|
+
</div>
|
|
18590
|
+
</div>
|
|
18591
|
+
<div class="stack">
|
|
18592
|
+
<div class="step">
|
|
18593
|
+
<div class="step-badge">1</div>
|
|
18594
|
+
<div>
|
|
18595
|
+
<p class="step-title">\u5728 App \u91CC\u6253\u5F00\u201C\u8FDE\u63A5 Hermes Link\u201D</p>
|
|
18596
|
+
<p class="step-copy">\u5148\u767B\u5F55 HermesPilot \u8D26\u53F7\uFF0C\u518D\u8D70\u626B\u7801\u6216\u624B\u52A8\u7801\u3002\u914D\u5BF9\u6210\u529F\u540E\uFF0CApp \u4F1A\u81EA\u52A8\u5207\u5230\u8FD9\u53F0 Link\u3002</p>
|
|
18597
|
+
</div>
|
|
18598
|
+
</div>
|
|
18599
|
+
<div class="step">
|
|
18600
|
+
<div class="step-badge">2</div>
|
|
18601
|
+
<div>
|
|
18602
|
+
<p class="step-title">\u626B\u4E8C\u7EF4\u7801\uFF0C\u6216\u76F4\u63A5\u8F93\u5165\u914D\u5BF9\u7801</p>
|
|
18603
|
+
<p class="step-copy">\u5982\u679C\u626B\u7801\u4E0D\u65B9\u4FBF\uFF0CApp \u91CC\u4E5F\u53EF\u4EE5\u76F4\u63A5\u8F93\u5165\u8FD9\u4E2A\u9875\u9762\u663E\u793A\u7684\u914D\u5BF9\u7801\u3002</p>
|
|
18604
|
+
</div>
|
|
18605
|
+
</div>
|
|
18606
|
+
<div class="step">
|
|
18607
|
+
<div class="step-badge">3</div>
|
|
18608
|
+
<div>
|
|
18609
|
+
<p class="step-title">\u914D\u5BF9\u6210\u529F\u540E\uFF0C\u8FD9\u4E2A\u9875\u9762\u4F1A\u81EA\u52A8\u53D8\u6210\u5DF2\u5B8C\u6210\u72B6\u6001</p>
|
|
18610
|
+
<p class="step-copy" id="statusHint">${escapeHtml(statusHint)}</p>
|
|
18611
|
+
</div>
|
|
18612
|
+
</div>
|
|
18613
|
+
</div>
|
|
18614
|
+
<p class="hint">\u53EF\u5728\u7EC8\u7AEF\u7EE7\u7EED\u4FDD\u7559\u8FD9\u4E2A\u9875\u9762\uFF0C\u65B9\u4FBF\u7A0D\u540E\u6838\u5BF9\u72B6\u6001\uFF1B\u5982\u679C\u914D\u5BF9\u5DF2\u7ECF\u6210\u529F\uFF0C\u9875\u9762\u4E0D\u4F1A\u518D\u8981\u6C42\u91CD\u65B0\u626B\u7801\u3002</p>
|
|
18615
|
+
</div>
|
|
18616
|
+
<div class="qr">
|
|
18617
|
+
<div class="card">
|
|
18618
|
+
<div class="status">
|
|
18619
|
+
<h2 class="status-title" id="statusTitle">${escapeHtml(statusLabel)}</h2>
|
|
18620
|
+
<span class="pill" id="statusPill">${input.state.claimed ? "\u5DF2\u626B\u7801" : "\u7B49\u5F85\u4E2D"}</span>
|
|
18621
|
+
</div>
|
|
18622
|
+
<div class="qr-frame">
|
|
18623
|
+
<img src="${qrDataUri}" alt="Hermes Link pairing QR code" />
|
|
18624
|
+
</div>
|
|
18625
|
+
<div class="code">${escapeHtml(session.code)}</div>
|
|
18626
|
+
<div class="footer">
|
|
18627
|
+
<span>Local API: ${escapeHtml(currentUrl)}</span>
|
|
18628
|
+
<span>Relay / Server \u4F1A\u540C\u65F6\u51C6\u5907\u53EF\u7528\u8DEF\u7EBF</span>
|
|
18629
|
+
</div>
|
|
18630
|
+
</div>
|
|
18631
|
+
</div>
|
|
18632
|
+
</div>
|
|
18633
|
+
</section>
|
|
18634
|
+
</main>
|
|
18635
|
+
<script>
|
|
18636
|
+
const sessionId = ${JSON.stringify(session.session_id)};
|
|
18637
|
+
const refresh = async () => {
|
|
18638
|
+
try {
|
|
18639
|
+
const response = await fetch('/api/v1/pairing/session?session_id=' + encodeURIComponent(sessionId), {
|
|
18640
|
+
headers: { accept: 'application/json' },
|
|
18641
|
+
});
|
|
18642
|
+
if (!response.ok) return;
|
|
18643
|
+
const payload = await response.json();
|
|
18644
|
+
if (payload?.session?.claimed) {
|
|
18645
|
+
document.querySelector('#statusPill').textContent = '\u5DF2\u626B\u7801';
|
|
18646
|
+
document.querySelector('#statusTitle').textContent = '\u5DF2\u5B8C\u6210\u914D\u5BF9';
|
|
18647
|
+
document.querySelector('#statusHint').textContent = 'App \u5DF2\u5B8C\u6210\u914D\u5BF9\uFF0C\u8FD9\u4E2A\u9875\u9762\u53EF\u4EE5\u5173\u95ED\u3002';
|
|
18648
|
+
}
|
|
18649
|
+
} catch (_) {}
|
|
18650
|
+
};
|
|
18651
|
+
setInterval(refresh, 1500);
|
|
18652
|
+
refresh();
|
|
18653
|
+
</script>
|
|
18654
|
+
</body>
|
|
18655
|
+
</html>`;
|
|
18656
|
+
}
|
|
18657
|
+
function escapeHtml(value) {
|
|
18658
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
18659
|
+
}
|
|
18660
|
+
function formatDate(value) {
|
|
18661
|
+
const date = new Date(value);
|
|
18662
|
+
return Number.isNaN(date.getTime()) ? value : date.toLocaleString("zh-CN", { hour12: false });
|
|
18663
|
+
}
|
|
18664
|
+
|
|
17890
18665
|
// src/http/app.ts
|
|
17891
18666
|
async function createApp(options = {}) {
|
|
17892
18667
|
const paths = options.paths ?? resolveRuntimePaths();
|
|
@@ -17916,6 +18691,7 @@ async function createApp(options = {}) {
|
|
|
17916
18691
|
logger,
|
|
17917
18692
|
onPairingClaimed: options.onPairingClaimed
|
|
17918
18693
|
});
|
|
18694
|
+
registerPairingRoutes(router, { paths });
|
|
17919
18695
|
registerHermesUpdateRoutes(router, { paths, logger });
|
|
17920
18696
|
registerLinkUpdateRoutes(router, { paths, logger });
|
|
17921
18697
|
registerStatisticsRoutes(router, { paths, logger, conversations });
|
|
@@ -17964,4 +18740,4 @@ export {
|
|
|
17964
18740
|
daemonLogFile,
|
|
17965
18741
|
currentCliScriptPath
|
|
17966
18742
|
};
|
|
17967
|
-
//# sourceMappingURL=chunk-
|
|
18743
|
+
//# sourceMappingURL=chunk-HIQBCPY4.js.map
|