clawconnect-agent 0.1.11 → 0.1.13
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/README.md +16 -0
- package/README.zh-CN.md +16 -0
- package/dist/commands/install.js +3 -2
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/pair.js +2 -1
- package/dist/commands/pair.js.map +1 -1
- package/dist/commands/profile-hints.d.ts +7 -0
- package/dist/commands/profile-hints.js +71 -0
- package/dist/commands/profile-hints.js.map +1 -0
- package/dist/commands/send-file.js +25 -5
- package/dist/commands/send-file.js.map +1 -1
- package/dist/core/relay/timeline-event-builder.d.ts +5 -1
- package/dist/core/relay/timeline-event-builder.js +4 -0
- package/dist/core/relay/timeline-event-builder.js.map +1 -1
- package/dist/core/relay/timeline-event-log.d.ts +3 -0
- package/dist/core/relay/timeline-event-log.js +4 -0
- package/dist/core/relay/timeline-event-log.js.map +1 -1
- package/dist/hermes/hermes-relay-manager.d.ts +27 -24
- package/dist/hermes/hermes-relay-manager.js +126 -343
- package/dist/hermes/hermes-relay-manager.js.map +1 -1
- package/dist/hermes/hermes-runtime-test-support.d.ts +27 -0
- package/dist/hermes/hermes-runtime-test-support.js +703 -0
- package/dist/hermes/hermes-runtime-test-support.js.map +1 -0
- package/dist/hermes/hermes-runtime.d.ts +1 -2
- package/dist/hermes/hermes-runtime.js +1 -2
- package/dist/hermes/hermes-runtime.js.map +1 -1
- package/dist/hermes/relay/active-hermes-chat-runs.d.ts +12 -0
- package/dist/hermes/relay/active-hermes-chat-runs.js +42 -0
- package/dist/hermes/relay/active-hermes-chat-runs.js.map +1 -0
- package/dist/hermes/relay/hermes-slash-command-catalog.d.ts +22 -0
- package/dist/hermes/relay/hermes-slash-command-catalog.js +248 -0
- package/dist/hermes/relay/hermes-slash-command-catalog.js.map +1 -0
- package/dist/hermes/relay/hermes-state-db-realtime.d.ts +69 -0
- package/dist/hermes/relay/hermes-state-db-realtime.js +444 -0
- package/dist/hermes/relay/hermes-state-db-realtime.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.d.ts +15 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.js +510 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-chat.d.ts +4 -14
- package/dist/hermes/runtime/hermes-runtime-chat.js +205 -436
- package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-command-utils.d.ts +1 -0
- package/dist/hermes/runtime/hermes-runtime-command-utils.js +10 -1
- package/dist/hermes/runtime/hermes-runtime-command-utils.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history-completion.d.ts +12 -0
- package/dist/hermes/runtime/hermes-runtime-history-completion.js +162 -0
- package/dist/hermes/runtime/hermes-runtime-history-completion.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-history.js +85 -22
- package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-preloaded-skills.d.ts +11 -0
- package/dist/hermes/runtime/hermes-runtime-preloaded-skills.js +67 -0
- package/dist/hermes/runtime/hermes-runtime-preloaded-skills.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-process.d.ts +3 -1
- package/dist/hermes/runtime/hermes-runtime-process.js +35 -6
- package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-sessions.js +28 -3
- package/dist/hermes/runtime/hermes-runtime-sessions.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-slash-command.d.ts +8 -0
- package/dist/hermes/runtime/hermes-runtime-slash-command.js +144 -0
- package/dist/hermes/runtime/hermes-runtime-slash-command.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-state-db.d.ts +4 -0
- package/dist/hermes/runtime/hermes-runtime-state-db.js +193 -0
- package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.d.ts +8 -0
- package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js +121 -0
- package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-usage.d.ts +2 -0
- package/dist/hermes/runtime/hermes-runtime-usage.js +59 -5
- package/dist/hermes/runtime/hermes-runtime-usage.js.map +1 -1
- package/dist/i18n/index.js +4 -0
- package/dist/i18n/index.js.map +1 -1
- package/dist/index.js +22 -3
- package/dist/index.js.map +1 -1
- package/dist/openclaw/handlers/local-handlers.js +66 -20
- package/dist/openclaw/handlers/local-handlers.js.map +1 -1
- package/dist/openclaw/relay/chat-send-dedupe-coordinator.d.ts +20 -0
- package/dist/openclaw/relay/chat-send-dedupe-coordinator.js +161 -0
- package/dist/openclaw/relay/chat-send-dedupe-coordinator.js.map +1 -0
- package/dist/openclaw/relay/relay-manager-hello.d.ts +6 -0
- package/dist/openclaw/relay/relay-manager-hello.js +11 -0
- package/dist/openclaw/relay/relay-manager-hello.js.map +1 -0
- package/dist/openclaw/relay/relay-manager-history-timing.d.ts +5 -0
- package/dist/openclaw/relay/relay-manager-history-timing.js +6 -0
- package/dist/openclaw/relay/relay-manager-history-timing.js.map +1 -0
- package/dist/openclaw/relay/relay-manager-office-events.d.ts +2 -0
- package/dist/openclaw/relay/relay-manager-office-events.js +9 -0
- package/dist/openclaw/relay/relay-manager-office-events.js.map +1 -0
- package/dist/openclaw/relay/relay-manager-payload-helpers.d.ts +18 -0
- package/dist/openclaw/relay/relay-manager-payload-helpers.js +213 -0
- package/dist/openclaw/relay/relay-manager-payload-helpers.js.map +1 -0
- package/dist/openclaw/relay/relay-manager-protocol.d.ts +49 -0
- package/dist/openclaw/relay/relay-manager-protocol.js +2 -0
- package/dist/openclaw/relay/relay-manager-protocol.js.map +1 -0
- package/dist/openclaw/relay-manager.d.ts +3 -27
- package/dist/openclaw/relay-manager.js +19 -402
- package/dist/openclaw/relay-manager.js.map +1 -1
- package/dist/openclaw/session-store.js +42 -1
- package/dist/openclaw/session-store.js.map +1 -1
- package/package.json +1 -1
- package/dist/hermes/runtime/hermes-runtime-artifacts.d.ts +0 -3
- package/dist/hermes/runtime/hermes-runtime-artifacts.js +0 -75
- package/dist/hermes/runtime/hermes-runtime-artifacts.js.map +0 -1
|
@@ -1,94 +1,35 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
-
import { readFileSync, statSync } from "fs";
|
|
3
2
|
import { mkdir, writeFile } from "fs/promises";
|
|
4
3
|
import { join } from "path";
|
|
5
4
|
import { spawn } from "child_process";
|
|
6
5
|
import { buildMobileAssistantDeltaPayload, } from "../../core/relay/mobile-chat-run-bridge.js";
|
|
7
6
|
import { buildToolInvocationUpdatedEvent, } from "../../core/relay/timeline-event-builder.js";
|
|
8
7
|
import { forgetHermesSession, getMappedHermesSessionId, rememberHermesSession, } from "../hermes-session-store.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
8
|
+
import { CHAT_TIMEOUT_MS, CLAWCONNECT_MOBILE_BRIDGE_HINT, HERMES_INBOX_DIR, SUBPROCESS_ENV, resolveHermesBin, isHermesCommandDeniedTimeoutLine, isHermesMissingSessionError, runHermes, sanitizeHermesChatOutput, stripAnsi, stripHermesSessionResumeNotices, } from "./hermes-runtime-process.js";
|
|
9
|
+
import { collectHermesUsageSnapshot, listHermesSessions, readCachedHermesStatusSnapshot, readHermesStatusSnapshotAsync, } from "./hermes-runtime-usage.js";
|
|
10
|
+
import { compactStringArray, sanitizeFileName } from "./hermes-runtime-values.js";
|
|
11
|
+
import { detectHermesHistoryCompletion, selectHermesSessionForCompletedChat, } from "./hermes-runtime-history-completion.js";
|
|
12
|
+
import { tryRunHermesApiChat } from "./hermes-runtime-api-client.js";
|
|
13
|
+
import { resolveHermesPreloadedSkillContext } from "./hermes-runtime-preloaded-skills.js";
|
|
14
|
+
import { createHermesToolLogWatcher, hermesToolState, } from "./hermes-runtime-tool-log-watcher.js";
|
|
15
|
+
import { isHermesSlashCommandMessage, runHermesSlashCommand, } from "./hermes-runtime-slash-command.js";
|
|
16
|
+
export { selectHermesSessionForCompletedChat } from "./hermes-runtime-history-completion.js";
|
|
17
|
+
export { parseHermesToolLogLine } from "./hermes-runtime-tool-log-watcher.js";
|
|
18
|
+
export { isHermesSlashCommandMessage } from "./hermes-runtime-slash-command.js";
|
|
14
19
|
const HERMES_ASSISTANT_DELTA_FLUSH_MS = 120;
|
|
15
20
|
const HERMES_ASSISTANT_DELTA_MAX_BYTES = 4096;
|
|
16
21
|
const HERMES_HISTORY_COMPLETION_GRACE_MS = 2_000;
|
|
17
22
|
const HERMES_HISTORY_COMPLETION_POLL_MS = 1_000;
|
|
23
|
+
const HERMES_API_EMPTY_OUTPUT_HISTORY_COMPLETION_TIMEOUT_MS = 12_000;
|
|
24
|
+
const HERMES_API_EMPTY_OUTPUT_HISTORY_COMPLETION_POLL_MS = 500;
|
|
25
|
+
const HERMES_RUNTIME_CONTEXT_CACHE_MAX_AGE_MS = 5 * 60_000;
|
|
18
26
|
const HERMES_COMMAND_DENIED_TIMEOUT_MESSAGE = "Timeout – denying command";
|
|
19
27
|
const hermesChatQueues = new Map();
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import sys
|
|
26
|
-
|
|
27
|
-
from rich.console import Console
|
|
28
|
-
|
|
29
|
-
import cli as cli_mod
|
|
30
|
-
from cli import HermesCLI
|
|
31
|
-
|
|
32
|
-
command = os.environ.get("CLAWCONNECT_HERMES_SLASH_COMMAND", "").strip()
|
|
33
|
-
resume = os.environ.get("CLAWCONNECT_HERMES_SLASH_RESUME", "").strip() or None
|
|
34
|
-
if command and not command.startswith("/"):
|
|
35
|
-
command = "/" + command
|
|
36
|
-
|
|
37
|
-
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
|
38
|
-
cli = HermesCLI(model=None, compact=True, resume=resume, verbose=False)
|
|
39
|
-
|
|
40
|
-
buf = io.StringIO()
|
|
41
|
-
cli.console = Console(file=buf, force_terminal=True, width=120)
|
|
42
|
-
|
|
43
|
-
def approve_once(prompt=""):
|
|
44
|
-
if prompt:
|
|
45
|
-
print(prompt, end="")
|
|
46
|
-
return "1"
|
|
47
|
-
|
|
48
|
-
def approve_once_modal(*args, **kwargs):
|
|
49
|
-
return "once"
|
|
50
|
-
|
|
51
|
-
try:
|
|
52
|
-
cli._prompt_text_input = approve_once
|
|
53
|
-
cli._prompt_text_input_modal = approve_once_modal
|
|
54
|
-
except Exception:
|
|
55
|
-
pass
|
|
56
|
-
|
|
57
|
-
old_cprint = getattr(cli_mod, "_cprint", None)
|
|
58
|
-
if old_cprint is not None:
|
|
59
|
-
cli_mod._cprint = lambda text: print(text)
|
|
60
|
-
|
|
61
|
-
try:
|
|
62
|
-
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
|
|
63
|
-
keep_going = cli.process_command(command)
|
|
64
|
-
pending_inputs = []
|
|
65
|
-
pending_queue = getattr(cli, "_pending_input", None)
|
|
66
|
-
if pending_queue is not None:
|
|
67
|
-
while True:
|
|
68
|
-
try:
|
|
69
|
-
pending_inputs.append(pending_queue.get_nowait())
|
|
70
|
-
except Exception:
|
|
71
|
-
break
|
|
72
|
-
payload = {
|
|
73
|
-
"ok": True,
|
|
74
|
-
"output": buf.getvalue().rstrip(),
|
|
75
|
-
"sessionId": getattr(cli, "session_id", None),
|
|
76
|
-
"keepGoing": bool(keep_going),
|
|
77
|
-
"pendingInputs": pending_inputs,
|
|
78
|
-
}
|
|
79
|
-
except Exception as exc:
|
|
80
|
-
payload = {
|
|
81
|
-
"ok": False,
|
|
82
|
-
"output": buf.getvalue().rstrip(),
|
|
83
|
-
"error": str(exc),
|
|
84
|
-
"sessionId": getattr(cli, "session_id", None),
|
|
85
|
-
}
|
|
86
|
-
finally:
|
|
87
|
-
if old_cprint is not None:
|
|
88
|
-
cli_mod._cprint = old_cprint
|
|
89
|
-
|
|
90
|
-
sys.stdout.write(json.dumps(payload, ensure_ascii=False))
|
|
91
|
-
`;
|
|
28
|
+
const EMPTY_PRELOADED_SKILL_CONTEXT = {
|
|
29
|
+
cliArgs: [],
|
|
30
|
+
requiredToolsets: [],
|
|
31
|
+
skillNames: [],
|
|
32
|
+
};
|
|
92
33
|
export async function runHermesChat(params, context = {}) {
|
|
93
34
|
const record = params && typeof params === "object" && !Array.isArray(params)
|
|
94
35
|
? params
|
|
@@ -98,19 +39,33 @@ export async function runHermesChat(params, context = {}) {
|
|
|
98
39
|
? record.sessionKey.trim()
|
|
99
40
|
: "main";
|
|
100
41
|
if (isHermesSlashCommandMessage(rawMessage)) {
|
|
101
|
-
return await runSerializedHermesChat(sessionKey, () => (runHermesSlashCommand({
|
|
42
|
+
return await runSerializedHermesChat(sessionKey, () => (runHermesSlashCommand({
|
|
43
|
+
message: rawMessage,
|
|
44
|
+
sessionKey,
|
|
45
|
+
hermesSessionId: record.hermesSessionId,
|
|
46
|
+
runQueuedChat: (queuedMessage, hermesSessionId) => runHermesChat({
|
|
47
|
+
message: queuedMessage,
|
|
48
|
+
sessionKey,
|
|
49
|
+
...(hermesSessionId ? { hermesSessionId } : {}),
|
|
50
|
+
}),
|
|
51
|
+
})));
|
|
102
52
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
53
|
+
const sourceRunId = typeof context.requestId === "string" && context.requestId.trim().length > 0
|
|
54
|
+
? context.requestId.trim()
|
|
55
|
+
: undefined;
|
|
56
|
+
const preparationPlan = planHermesChatPreparation(rawMessage);
|
|
57
|
+
const preparedMessage = await prepareHermesMessage(rawMessage, record.attachments, sessionKey, sourceRunId, preparationPlan);
|
|
58
|
+
if (!preparedMessage.cliMessage.trim()) {
|
|
105
59
|
throw new Error("message_required");
|
|
106
60
|
}
|
|
107
61
|
return await runSerializedHermesChat(sessionKey, async () => {
|
|
108
62
|
return await runHermesChatPrepared({
|
|
109
63
|
rawMessage,
|
|
110
|
-
|
|
64
|
+
preparedMessage,
|
|
111
65
|
sessionKey,
|
|
112
66
|
hermesSessionId: record.hermesSessionId,
|
|
113
67
|
context,
|
|
68
|
+
preparationPlan,
|
|
114
69
|
});
|
|
115
70
|
});
|
|
116
71
|
}
|
|
@@ -135,14 +90,64 @@ async function runHermesChatPrepared(params) {
|
|
|
135
90
|
: undefined;
|
|
136
91
|
const mappedResume = explicitResume ? undefined : await getMappedHermesSessionId(params.sessionKey);
|
|
137
92
|
let resume = explicitResume ?? mappedResume;
|
|
93
|
+
const preloadedSkillContext = params.preparationPlan.preloadFileTransferSkill
|
|
94
|
+
? await resolveHermesPreloadedSkillContext({ forceFileTransfer: true })
|
|
95
|
+
: EMPTY_PRELOADED_SKILL_CONTEXT;
|
|
96
|
+
try {
|
|
97
|
+
const apiChat = await tryRunHermesApiChat({
|
|
98
|
+
message: params.preparedMessage.apiMessage,
|
|
99
|
+
instructions: params.preparedMessage.apiInstructions,
|
|
100
|
+
sessionKey: params.sessionKey,
|
|
101
|
+
resume,
|
|
102
|
+
preloadedSkillNames: preloadedSkillContext.skillNames,
|
|
103
|
+
requiredToolsets: preloadedSkillContext.requiredToolsets,
|
|
104
|
+
context: params.context,
|
|
105
|
+
});
|
|
106
|
+
if (apiChat) {
|
|
107
|
+
const recoveredOutput = await recoverEmptyHermesApiOutputFromHistory({
|
|
108
|
+
output: apiChat.output,
|
|
109
|
+
hermesSessionId: apiChat.hermesSessionId,
|
|
110
|
+
sessionKey: params.sessionKey,
|
|
111
|
+
userMessage: params.rawMessage,
|
|
112
|
+
abortSignal: params.context.abortSignal,
|
|
113
|
+
});
|
|
114
|
+
return recoveredOutput ? { ...apiChat, output: recoveredOutput } : apiChat;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (!mappedResume || !isHermesMissingSessionError(error)) {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
await forgetHermesSession(params.sessionKey, mappedResume);
|
|
122
|
+
resume = undefined;
|
|
123
|
+
const retryApiChat = await tryRunHermesApiChat({
|
|
124
|
+
message: params.preparedMessage.apiMessage,
|
|
125
|
+
instructions: params.preparedMessage.apiInstructions,
|
|
126
|
+
sessionKey: params.sessionKey,
|
|
127
|
+
preloadedSkillNames: preloadedSkillContext.skillNames,
|
|
128
|
+
requiredToolsets: preloadedSkillContext.requiredToolsets,
|
|
129
|
+
context: params.context,
|
|
130
|
+
});
|
|
131
|
+
if (retryApiChat) {
|
|
132
|
+
const recoveredOutput = await recoverEmptyHermesApiOutputFromHistory({
|
|
133
|
+
output: retryApiChat.output,
|
|
134
|
+
hermesSessionId: retryApiChat.hermesSessionId,
|
|
135
|
+
sessionKey: params.sessionKey,
|
|
136
|
+
userMessage: params.rawMessage,
|
|
137
|
+
abortSignal: params.context.abortSignal,
|
|
138
|
+
});
|
|
139
|
+
return recoveredOutput ? { ...retryApiChat, output: recoveredOutput } : retryApiChat;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
138
142
|
const beforeSessions = await listHermesSessions();
|
|
139
143
|
let rawOutput;
|
|
140
144
|
try {
|
|
141
145
|
rawOutput = await runHermesChatOnce({
|
|
142
|
-
message: params.
|
|
146
|
+
message: params.preparedMessage.cliMessage,
|
|
143
147
|
sessionKey: params.sessionKey,
|
|
144
148
|
resume,
|
|
145
149
|
context: params.context,
|
|
150
|
+
preloadedSkillArgs: preloadedSkillContext.cliArgs,
|
|
146
151
|
historyCompletion: () => detectHermesHistoryCompletion({
|
|
147
152
|
beforeSessions,
|
|
148
153
|
resume,
|
|
@@ -158,9 +163,10 @@ async function runHermesChatPrepared(params) {
|
|
|
158
163
|
await forgetHermesSession(params.sessionKey, mappedResume);
|
|
159
164
|
resume = undefined;
|
|
160
165
|
rawOutput = await runHermesChatOnce({
|
|
161
|
-
message: params.
|
|
166
|
+
message: params.preparedMessage.cliMessage,
|
|
162
167
|
sessionKey: params.sessionKey,
|
|
163
168
|
context: params.context,
|
|
169
|
+
preloadedSkillArgs: preloadedSkillContext.cliArgs,
|
|
164
170
|
historyCompletion: () => detectHermesHistoryCompletion({
|
|
165
171
|
beforeSessions,
|
|
166
172
|
sessionKey: params.sessionKey,
|
|
@@ -180,257 +186,72 @@ async function runHermesChatPrepared(params) {
|
|
|
180
186
|
}
|
|
181
187
|
const usage = mappedSession?.hermesSessionId
|
|
182
188
|
? await collectHermesUsageSnapshot(mappedSession.hermesSessionId)
|
|
183
|
-
:
|
|
189
|
+
: await readHermesStatusSnapshotAsync();
|
|
184
190
|
return {
|
|
185
191
|
output,
|
|
186
192
|
sessionKey: params.sessionKey,
|
|
187
|
-
artifactPaths:
|
|
193
|
+
artifactPaths: [],
|
|
188
194
|
usage,
|
|
189
195
|
};
|
|
190
196
|
}
|
|
191
|
-
async function
|
|
192
|
-
|
|
193
|
-
if (params.resume) {
|
|
194
|
-
args.push("--resume", params.resume);
|
|
195
|
-
}
|
|
196
|
-
return params.context.publishEvent
|
|
197
|
-
? await runHermesChatStreaming(args, params.sessionKey, params.context, params.historyCompletion)
|
|
198
|
-
: runHermes(args, CHAT_TIMEOUT_MS);
|
|
199
|
-
}
|
|
200
|
-
async function detectHermesHistoryCompletion(params) {
|
|
201
|
-
const sessions = await listHermesSessions();
|
|
202
|
-
const mappedSession = selectHermesSessionForCompletedChat(sessions, {
|
|
203
|
-
beforeSessions: params.beforeSessions,
|
|
204
|
-
resume: params.resume,
|
|
205
|
-
userMessage: params.userMessage,
|
|
206
|
-
});
|
|
207
|
-
if (!mappedSession?.hermesSessionId) {
|
|
208
|
-
return undefined;
|
|
209
|
-
}
|
|
210
|
-
const exportResult = await runHermesSessionExport({
|
|
211
|
-
sessionKey: params.sessionKey,
|
|
212
|
-
hermesSessionId: mappedSession.hermesSessionId,
|
|
213
|
-
output: "-",
|
|
214
|
-
});
|
|
215
|
-
if (!exportResult.ok) {
|
|
197
|
+
async function recoverEmptyHermesApiOutputFromHistory(params) {
|
|
198
|
+
if (sanitizeHermesChatOutput(params.output).trim()) {
|
|
216
199
|
return undefined;
|
|
217
200
|
}
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
function latestAssistantReplyFromHermesExport(payload, userMessage) {
|
|
221
|
-
const output = toRecord(payload).output;
|
|
222
|
-
const parsed = parseHermesChatExportOutput(output);
|
|
223
|
-
const record = toRecord(parsed);
|
|
224
|
-
const rawMessages = Array.isArray(record.messages)
|
|
225
|
-
? record.messages
|
|
226
|
-
: Array.isArray(record.items)
|
|
227
|
-
? record.items
|
|
228
|
-
: Array.isArray(parsed)
|
|
229
|
-
? parsed
|
|
230
|
-
: [];
|
|
231
|
-
if (rawMessages.length === 0) {
|
|
232
|
-
return undefined;
|
|
233
|
-
}
|
|
234
|
-
const normalizedUser = normalizeSessionSelectionText(userMessage);
|
|
235
|
-
if (!normalizedUser) {
|
|
201
|
+
if (!params.hermesSessionId?.trim()) {
|
|
236
202
|
return undefined;
|
|
237
203
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (normalizeHistoryRoleValue(message.role) !== "user") {
|
|
243
|
-
continue;
|
|
204
|
+
const deadline = Date.now() + HERMES_API_EMPTY_OUTPUT_HISTORY_COMPLETION_TIMEOUT_MS;
|
|
205
|
+
while (Date.now() <= deadline) {
|
|
206
|
+
if (params.abortSignal?.aborted) {
|
|
207
|
+
throw new Error("hermes_chat_aborted");
|
|
244
208
|
}
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
for (let index = rawMessages.length - 1; index >= latestUserIndex + 1; index -= 1) {
|
|
255
|
-
const message = toRecord(rawMessages[index]);
|
|
256
|
-
if (normalizeHistoryRoleValue(message.role) !== "assistant") {
|
|
257
|
-
continue;
|
|
209
|
+
const detectedOutput = await detectHermesHistoryCompletion({
|
|
210
|
+
beforeSessions: [],
|
|
211
|
+
resume: params.hermesSessionId,
|
|
212
|
+
sessionKey: params.sessionKey,
|
|
213
|
+
userMessage: params.userMessage,
|
|
214
|
+
});
|
|
215
|
+
if (detectedOutput) {
|
|
216
|
+
return detectedOutput;
|
|
258
217
|
}
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
261
|
-
|
|
218
|
+
const remainingMs = deadline - Date.now();
|
|
219
|
+
if (remainingMs <= 0) {
|
|
220
|
+
break;
|
|
262
221
|
}
|
|
222
|
+
await sleep(Math.min(HERMES_API_EMPTY_OUTPUT_HISTORY_COMPLETION_POLL_MS, remainingMs));
|
|
263
223
|
}
|
|
264
224
|
return undefined;
|
|
265
225
|
}
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
return value;
|
|
269
|
-
}
|
|
270
|
-
const trimmed = value.trim();
|
|
271
|
-
if (!trimmed) {
|
|
272
|
-
return {};
|
|
273
|
-
}
|
|
274
|
-
try {
|
|
275
|
-
return JSON.parse(trimmed);
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
278
|
-
const firstObjectBrace = trimmed.indexOf("{");
|
|
279
|
-
const lastObjectBrace = trimmed.lastIndexOf("}");
|
|
280
|
-
if (firstObjectBrace >= 0 && lastObjectBrace > firstObjectBrace) {
|
|
281
|
-
try {
|
|
282
|
-
return JSON.parse(trimmed.slice(firstObjectBrace, lastObjectBrace + 1));
|
|
283
|
-
}
|
|
284
|
-
catch {
|
|
285
|
-
return {};
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return {};
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
function normalizeHistoryRoleValue(value) {
|
|
292
|
-
return typeof value === "string" ? value.trim().toLowerCase().replace("_", "") : "";
|
|
293
|
-
}
|
|
294
|
-
function extractHermesHistoryText(record) {
|
|
295
|
-
const content = record.content;
|
|
296
|
-
if (typeof content === "string") {
|
|
297
|
-
return content;
|
|
298
|
-
}
|
|
299
|
-
if (Array.isArray(content)) {
|
|
300
|
-
return content
|
|
301
|
-
.flatMap((block) => {
|
|
302
|
-
const blockRecord = toRecord(block);
|
|
303
|
-
const type = typeof blockRecord.type === "string" ? blockRecord.type.trim().toLowerCase() : "";
|
|
304
|
-
if (type && type !== "text" && type !== "output_text" && type !== "input_text") {
|
|
305
|
-
return [];
|
|
306
|
-
}
|
|
307
|
-
return typeof blockRecord.text === "string" ? [blockRecord.text] : [];
|
|
308
|
-
})
|
|
309
|
-
.filter((text) => text.trim().length > 0)
|
|
310
|
-
.join("\n\n");
|
|
311
|
-
}
|
|
312
|
-
for (const key of ["text", "message", "output"]) {
|
|
313
|
-
const value = record[key];
|
|
314
|
-
if (typeof value === "string" && value.trim()) {
|
|
315
|
-
return value;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return "";
|
|
226
|
+
async function sleep(ms) {
|
|
227
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
319
228
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const normalizedUserMessage = normalizeSessionSelectionText(options.userMessage);
|
|
334
|
-
if (normalizedUserMessage) {
|
|
335
|
-
const matched = newSessions.find((session) => {
|
|
336
|
-
const haystack = normalizeSessionSelectionText([
|
|
337
|
-
session.displayName,
|
|
338
|
-
session.derivedTitle,
|
|
339
|
-
session.label,
|
|
340
|
-
].filter(Boolean).join(" "));
|
|
341
|
-
return haystack.length > 0
|
|
342
|
-
&& (haystack.includes(normalizedUserMessage) || normalizedUserMessage.includes(haystack));
|
|
343
|
-
});
|
|
344
|
-
if (matched) {
|
|
345
|
-
return matched;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return newSessions[0];
|
|
349
|
-
}
|
|
350
|
-
function normalizeSessionSelectionText(value) {
|
|
351
|
-
return value
|
|
352
|
-
?.replace(/\[Hermes runtime context][\s\S]*$/i, "")
|
|
353
|
-
.replace(/\s+/g, " ")
|
|
354
|
-
.trim()
|
|
355
|
-
.slice(0, 80)
|
|
356
|
-
.toLowerCase() ?? "";
|
|
357
|
-
}
|
|
358
|
-
export async function runHermesSlashCommand(params) {
|
|
359
|
-
const command = params.message.trim();
|
|
360
|
-
const resume = typeof params.hermesSessionId === "string" && params.hermesSessionId.trim().length > 0
|
|
361
|
-
? params.hermesSessionId.trim()
|
|
362
|
-
: await getMappedHermesSessionId(params.sessionKey);
|
|
363
|
-
const raw = runHermesPython(HERMES_SLASH_COMMAND_SCRIPT, {
|
|
364
|
-
CLAWCONNECT_HERMES_SLASH_COMMAND: command,
|
|
365
|
-
CLAWCONNECT_HERMES_SLASH_RESUME: resume ?? "",
|
|
366
|
-
});
|
|
367
|
-
const payload = parseHermesSlashCommandPayload(raw);
|
|
368
|
-
const output = sanitizeHermesChatOutput(payload.output ?? "").trim();
|
|
369
|
-
if (!payload.ok) {
|
|
370
|
-
const message = [output, payload.error].filter(Boolean).join("\n").trim() || "hermes_slash_command_failed";
|
|
371
|
-
throw new Error(message);
|
|
372
|
-
}
|
|
373
|
-
const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim().length > 0
|
|
374
|
-
? payload.sessionId.trim()
|
|
375
|
-
: undefined;
|
|
376
|
-
if (sessionId) {
|
|
377
|
-
await rememberHermesSession(params.sessionKey, {
|
|
378
|
-
sessionKey: params.sessionKey,
|
|
379
|
-
hermesSessionId: sessionId,
|
|
380
|
-
displayName: sessionId,
|
|
381
|
-
label: command,
|
|
382
|
-
lastActivityAt: new Date().toISOString(),
|
|
383
|
-
kind: "hermes",
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
if (/^\/(?:new|reset)\b/i.test(command) && resume && sessionId && sessionId !== resume) {
|
|
387
|
-
await forgetHermesSession(resume, resume);
|
|
388
|
-
}
|
|
389
|
-
if (payload.pendingInputs && payload.pendingInputs.length > 0) {
|
|
390
|
-
const queuedMessage = payload.pendingInputs.join("\n\n").trim();
|
|
391
|
-
if (queuedMessage) {
|
|
392
|
-
const queued = await runHermesChat({
|
|
393
|
-
message: queuedMessage,
|
|
394
|
-
sessionKey: params.sessionKey,
|
|
395
|
-
...(sessionId ? { hermesSessionId: sessionId } : {}),
|
|
396
|
-
});
|
|
397
|
-
return {
|
|
398
|
-
...queued,
|
|
399
|
-
output: [output, queued.output].filter((part) => part && part !== "(no output)").join("\n\n") || queued.output,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
const usage = await collectHermesUsageSnapshot(sessionId);
|
|
404
|
-
return {
|
|
405
|
-
output: output || "(no output)",
|
|
406
|
-
sessionKey: params.sessionKey,
|
|
407
|
-
artifactPaths: [],
|
|
408
|
-
usage,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
function parseHermesSlashCommandPayload(raw) {
|
|
412
|
-
const parsed = JSON.parse(raw);
|
|
413
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
414
|
-
throw new Error("invalid_hermes_slash_command_payload");
|
|
229
|
+
async function runHermesChatOnce(params) {
|
|
230
|
+
const args = [
|
|
231
|
+
"chat",
|
|
232
|
+
"--query",
|
|
233
|
+
params.message,
|
|
234
|
+
"--quiet",
|
|
235
|
+
"--source",
|
|
236
|
+
"pocketclaw",
|
|
237
|
+
...(params.preloadedSkillArgs ?? []),
|
|
238
|
+
"--yolo",
|
|
239
|
+
];
|
|
240
|
+
if (params.resume) {
|
|
241
|
+
args.push("--resume", params.resume);
|
|
415
242
|
}
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
sessionId: typeof record.sessionId === "string" ? record.sessionId : undefined,
|
|
422
|
-
pendingInputs: Array.isArray(record.pendingInputs)
|
|
423
|
-
? record.pendingInputs.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
424
|
-
: undefined,
|
|
425
|
-
};
|
|
243
|
+
const runId = params.context.requestId ?? `hermes-${Date.now()}`;
|
|
244
|
+
const env = hermesChatSubprocessEnv(runId, params.sessionKey);
|
|
245
|
+
return params.context.publishEvent
|
|
246
|
+
? await runHermesChatStreaming(args, params.sessionKey, params.context, runId, env, params.historyCompletion)
|
|
247
|
+
: runHermes(args, CHAT_TIMEOUT_MS, env);
|
|
426
248
|
}
|
|
427
|
-
async function runHermesChatStreaming(args, sessionKey, context, historyCompletion) {
|
|
249
|
+
async function runHermesChatStreaming(args, sessionKey, context, runId, env, historyCompletion) {
|
|
428
250
|
const child = spawn(resolveHermesBin(), args, {
|
|
429
|
-
env
|
|
251
|
+
env,
|
|
430
252
|
stdio: ["ignore", "pipe", "pipe"],
|
|
431
253
|
windowsHide: true,
|
|
432
254
|
});
|
|
433
|
-
const runId = context.requestId ?? `hermes-${Date.now()}`;
|
|
434
255
|
let output = "";
|
|
435
256
|
let stderr = "";
|
|
436
257
|
let seq = 0;
|
|
@@ -552,7 +373,8 @@ async function runHermesChatStreaming(args, sessionKey, context, historyCompleti
|
|
|
552
373
|
sessionKey,
|
|
553
374
|
seq: seq += 1,
|
|
554
375
|
timestampMs,
|
|
555
|
-
delta
|
|
376
|
+
// canonical message.part.delta 是同一 part 的绝对状态;Hermes CLI stdout 这里 flush 的只是增量片段。
|
|
377
|
+
delta: output,
|
|
556
378
|
}),
|
|
557
379
|
});
|
|
558
380
|
}
|
|
@@ -797,125 +619,27 @@ export function buildHermesAssistantDeltaPayload(params) {
|
|
|
797
619
|
includeTimelineEvents: true,
|
|
798
620
|
});
|
|
799
621
|
}
|
|
800
|
-
function
|
|
801
|
-
if (event.phase === "completed") {
|
|
802
|
-
return "success";
|
|
803
|
-
}
|
|
804
|
-
if (event.phase === "failed") {
|
|
805
|
-
return "failed";
|
|
806
|
-
}
|
|
807
|
-
return "streaming_output";
|
|
808
|
-
}
|
|
809
|
-
function createHermesToolLogWatcher(onEvent) {
|
|
810
|
-
let offset = 0;
|
|
811
|
-
let timer;
|
|
812
|
-
try {
|
|
813
|
-
offset = statSync(HERMES_AGENT_LOG_FILE).size;
|
|
814
|
-
}
|
|
815
|
-
catch {
|
|
816
|
-
offset = 0;
|
|
817
|
-
}
|
|
818
|
-
const poll = () => {
|
|
819
|
-
let content = "";
|
|
820
|
-
try {
|
|
821
|
-
const bytes = readFileSync(HERMES_AGENT_LOG_FILE);
|
|
822
|
-
if (bytes.length < offset) {
|
|
823
|
-
offset = 0;
|
|
824
|
-
}
|
|
825
|
-
if (bytes.length === offset) {
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
content = bytes.subarray(offset).toString("utf8");
|
|
829
|
-
offset = bytes.length;
|
|
830
|
-
}
|
|
831
|
-
catch {
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
for (const line of content.split(/\r?\n/)) {
|
|
835
|
-
const event = parseHermesToolLogLine(line);
|
|
836
|
-
if (event) {
|
|
837
|
-
onEvent(event);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
};
|
|
622
|
+
function hermesChatSubprocessEnv(runId, sessionKey) {
|
|
841
623
|
return {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
timer = setInterval(poll, 250);
|
|
847
|
-
timer.unref?.();
|
|
848
|
-
},
|
|
849
|
-
stop: () => {
|
|
850
|
-
poll();
|
|
851
|
-
if (timer) {
|
|
852
|
-
clearInterval(timer);
|
|
853
|
-
timer = undefined;
|
|
854
|
-
}
|
|
855
|
-
},
|
|
624
|
+
...SUBPROCESS_ENV,
|
|
625
|
+
CLAWCONNECT_SOURCE_RUN_ID: runId,
|
|
626
|
+
CLAWCONNECT_SESSION_KEY: sessionKey,
|
|
627
|
+
CLAWCONNECT_CHAT_SESSION_KEY: sessionKey,
|
|
856
628
|
};
|
|
857
629
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
if (!
|
|
861
|
-
return
|
|
862
|
-
}
|
|
863
|
-
const executor = clean.match(/\bagent\.tool_executor:\s*tool\s+([A-Za-z0-9_.-]+)\s+(.+)$/i);
|
|
864
|
-
if (executor) {
|
|
865
|
-
const toolName = normalizeHermesToolName(executor[1] ?? "tool");
|
|
866
|
-
const detail = (executor[2] ?? "").trim();
|
|
867
|
-
if (/\b(?:running|started|executing|start)\b/i.test(detail)) {
|
|
868
|
-
return {
|
|
869
|
-
toolName,
|
|
870
|
-
phase: "streaming",
|
|
871
|
-
text: `${toolName} ${detail}`.trim(),
|
|
872
|
-
isError: false,
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
const failed = /\b(?:failed|error|errored|denied|aborted)\b/i.test(detail);
|
|
876
|
-
return {
|
|
877
|
-
toolName,
|
|
878
|
-
phase: failed ? "failed" : "completed",
|
|
879
|
-
text: `${toolName} ${detail}`.trim(),
|
|
880
|
-
isError: failed,
|
|
881
|
-
};
|
|
882
|
-
}
|
|
883
|
-
const toolLogger = clean.match(/\btools\.([A-Za-z0-9_.-]+):\s*(.+)$/i);
|
|
884
|
-
if (!toolLogger) {
|
|
885
|
-
return null;
|
|
886
|
-
}
|
|
887
|
-
const loggerName = toolLogger[1] ?? "tool";
|
|
888
|
-
if (!/(?:_tool|_tools)$/i.test(loggerName)) {
|
|
889
|
-
return null;
|
|
890
|
-
}
|
|
891
|
-
let toolName = normalizeHermesToolName(loggerName);
|
|
892
|
-
let detail = (toolLogger[2] ?? "").trim();
|
|
893
|
-
if (/\b(?:Shutting down \d+ remaining sandbox(?:\(es\))?|Manually cleaned up environment|Cleaned \d+ environments?)\b/i.test(detail)) {
|
|
894
|
-
return null;
|
|
895
|
-
}
|
|
896
|
-
const nestedTool = detail.match(/^([A-Za-z0-9_.-]+):\s*(.+)$/);
|
|
897
|
-
if (nestedTool) {
|
|
898
|
-
toolName = normalizeHermesToolName(nestedTool[1] ?? toolName);
|
|
899
|
-
detail = (nestedTool[2] ?? "").trim();
|
|
900
|
-
}
|
|
901
|
-
if (!detail) {
|
|
902
|
-
return null;
|
|
630
|
+
const CLAWCONNECT_MOBILE_TURN_INSTRUCTION = "Use [ClawConnect mobile turn] metadata only for ClawConnect file-transfer attribution and message identity. Do not mention it in the answer.";
|
|
631
|
+
function buildClawConnectMobileTurnMetadata(sourceRunId, sessionKey) {
|
|
632
|
+
if (!sourceRunId) {
|
|
633
|
+
return undefined;
|
|
903
634
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
function normalizeHermesToolName(rawName) {
|
|
911
|
-
const normalized = rawName
|
|
912
|
-
.trim()
|
|
913
|
-
.replace(/[^A-Za-z0-9_.-]+/g, "_")
|
|
914
|
-
.replace(/_tools$/i, "")
|
|
915
|
-
.replace(/_tool$/i, "");
|
|
916
|
-
return normalized || "tool";
|
|
635
|
+
// 这个块是 ClawConnect 和 Hermes history 的稳定身份合同;客户端展示时会剥离它。
|
|
636
|
+
return [
|
|
637
|
+
"[ClawConnect mobile turn]",
|
|
638
|
+
`sourceRunId: ${sourceRunId}`,
|
|
639
|
+
`sessionKey: ${sessionKey}`,
|
|
640
|
+
].join("\n");
|
|
917
641
|
}
|
|
918
|
-
async function prepareHermesMessage(message, attachments, sessionKey) {
|
|
642
|
+
async function prepareHermesMessage(message, attachments, sessionKey, sourceRunId, preparationPlan = planHermesChatPreparation(message)) {
|
|
919
643
|
const refs = [];
|
|
920
644
|
if (Array.isArray(attachments)) {
|
|
921
645
|
const safeSession = sessionKey.replace(/[^\w.-]/g, "_") || "main";
|
|
@@ -938,16 +662,61 @@ async function prepareHermesMessage(message, attachments, sessionKey) {
|
|
|
938
662
|
refs.push(`[file attached: ${filePath} (${mimeType})]`);
|
|
939
663
|
}
|
|
940
664
|
}
|
|
941
|
-
const
|
|
665
|
+
const userSections = [message.trim()];
|
|
942
666
|
if (refs.length > 0) {
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
const runtimeHint = buildHermesRuntimeContextHint(
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
667
|
+
userSections.push(refs.join("\n"));
|
|
668
|
+
}
|
|
669
|
+
const runtimeHint = buildHermesRuntimeContextHint(readCachedHermesStatusSnapshot(HERMES_RUNTIME_CONTEXT_CACHE_MAX_AGE_MS));
|
|
670
|
+
const turnMetadata = buildClawConnectMobileTurnMetadata(sourceRunId, sessionKey);
|
|
671
|
+
const apiMessageSections = [...userSections];
|
|
672
|
+
if (turnMetadata) {
|
|
673
|
+
apiMessageSections.push(turnMetadata);
|
|
674
|
+
}
|
|
675
|
+
const apiInstructionSections = [
|
|
676
|
+
runtimeHint,
|
|
677
|
+
CLAWCONNECT_MOBILE_BRIDGE_HINT,
|
|
678
|
+
turnMetadata ? CLAWCONNECT_MOBILE_TURN_INSTRUCTION : undefined,
|
|
679
|
+
];
|
|
680
|
+
const cliMessageSections = [
|
|
681
|
+
...userSections,
|
|
682
|
+
runtimeHint,
|
|
683
|
+
CLAWCONNECT_MOBILE_BRIDGE_HINT,
|
|
684
|
+
turnMetadata
|
|
685
|
+
? [turnMetadata, CLAWCONNECT_MOBILE_TURN_INSTRUCTION].join("\n")
|
|
686
|
+
: undefined,
|
|
687
|
+
];
|
|
688
|
+
return {
|
|
689
|
+
apiMessage: apiMessageSections.filter(Boolean).join("\n\n").trim(),
|
|
690
|
+
apiInstructions: apiInstructionSections.filter(Boolean).join("\n\n").trim() || undefined,
|
|
691
|
+
cliMessage: cliMessageSections.filter(Boolean).join("\n\n").trim(),
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function planHermesChatPreparation(message) {
|
|
695
|
+
return {
|
|
696
|
+
preloadFileTransferSkill: shouldPreloadHermesFileTransferSkill(message),
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function shouldPreloadHermesFileTransferSkill(message) {
|
|
700
|
+
const text = message.trim();
|
|
701
|
+
const lower = text.toLowerCase();
|
|
702
|
+
if (!text) {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
if (/\bfile-transfer\b|clawconnect\s+send-file/.test(lower)) {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
const pathOrExtension = /(?:^|\s|["'“”‘’])(?:~|\/|[a-z]:\\)|\.(?:png|jpe?g|gif|webp|heic|pdf|docx?|xlsx?|pptx?|zip|txt|md)\b/i.test(text);
|
|
709
|
+
const englishSend = /\b(send|transfer|upload|attach|share|copy)\b/i.test(text);
|
|
710
|
+
const englishFile = /\b(file|image|photo|picture|screenshot|document|attachment)\b/i.test(text) || pathOrExtension;
|
|
711
|
+
const englishTarget = /\b(phone|iphone|mobile|clawlink|this device|me)\b/i.test(text);
|
|
712
|
+
if (englishSend && englishFile && englishTarget) {
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
const chineseSend = /发送|发给|发到|发过来|传到|传给|传输|上传|分享|转发|给我|发/.test(text);
|
|
716
|
+
const chineseFile = /文件|图片|照片|截图|文档|附件|桌面|本机|路径|微信图片/.test(text) || pathOrExtension;
|
|
717
|
+
const chineseTarget = /手机|移动端|这台|给我|发过来|iPhone|ClawLink/i.test(text);
|
|
718
|
+
// 文件传输技能会触发 Hermes CLI 的 skills list;普通文本必须不走这条慢路径。
|
|
719
|
+
return chineseSend && chineseFile && chineseTarget;
|
|
951
720
|
}
|
|
952
721
|
export function buildHermesRuntimeContextHint(snapshot) {
|
|
953
722
|
const details = compactStringArray([
|