clawconnect-agent 0.2.6 → 0.2.7
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 +7 -3
- package/README.zh-CN.md +7 -3
- package/dist/commands/hermes-file-transfer-outcome.d.ts +18 -0
- package/dist/commands/hermes-file-transfer-outcome.js +40 -0
- package/dist/commands/hermes-file-transfer-outcome.js.map +1 -0
- package/dist/commands/send-file.js +39 -33
- package/dist/commands/send-file.js.map +1 -1
- package/dist/core/command-types.d.ts +5 -0
- package/dist/core/relay/chat-payload.js +32 -22
- package/dist/core/relay/chat-payload.js.map +1 -1
- package/dist/core/relay/file-upload.d.ts +1 -0
- package/dist/core/relay/file-upload.js +3 -0
- package/dist/core/relay/file-upload.js.map +1 -1
- package/dist/core/relay/mobile-chat-run-bridge.d.ts +53 -8
- package/dist/core/relay/mobile-chat-run-bridge.js +45 -0
- package/dist/core/relay/mobile-chat-run-bridge.js.map +1 -1
- package/dist/core/relay/office-payload.js +54 -22
- package/dist/core/relay/office-payload.js.map +1 -1
- package/dist/core/relay/source-commit.d.ts +41 -0
- package/dist/core/relay/source-commit.js +118 -0
- package/dist/core/relay/source-commit.js.map +1 -0
- package/dist/core/relay/timeline-event-builder.d.ts +6 -0
- package/dist/core/relay/timeline-event-builder.js +3 -0
- package/dist/core/relay/timeline-event-builder.js.map +1 -1
- package/dist/core/relay/timeline-event-log.d.ts +29 -0
- package/dist/core/relay/timeline-event-log.js +37 -0
- package/dist/core/relay/timeline-event-log.js.map +1 -1
- package/dist/core/relay/timeline-projection-v3.d.ts +63 -0
- package/dist/core/relay/timeline-projection-v3.js +87 -0
- package/dist/core/relay/timeline-projection-v3.js.map +1 -0
- package/dist/hermes/hermes-relay-manager.js +3 -0
- package/dist/hermes/hermes-relay-manager.js.map +1 -1
- package/dist/hermes/hermes-runtime-test-support.d.ts +3 -3
- package/dist/hermes/hermes-runtime-test-support.js +12 -6
- package/dist/hermes/hermes-runtime-test-support.js.map +1 -1
- package/dist/hermes/runtime/hermes-file-transfer-state.d.ts +33 -0
- package/dist/hermes/runtime/hermes-file-transfer-state.js +139 -0
- package/dist/hermes/runtime/hermes-file-transfer-state.js.map +1 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.d.ts +17 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.js +103 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.d.ts +1 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.js +16 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-chat.d.ts +35 -4
- package/dist/hermes/runtime/hermes-runtime-chat.js +426 -51
- package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-command-router.js +9 -1
- package/dist/hermes/runtime/hermes-runtime-command-router.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-cron.d.ts +1 -0
- package/dist/hermes/runtime/hermes-runtime-cron.js +34 -66
- package/dist/hermes/runtime/hermes-runtime-cron.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history-completion.d.ts +8 -1
- package/dist/hermes/runtime/hermes-runtime-history-completion.js +112 -41
- package/dist/hermes/runtime/hermes-runtime-history-completion.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history.js +92 -17
- package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-models.js +4 -1
- package/dist/hermes/runtime/hermes-runtime-models.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-process.js +4 -3
- package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-state-db.d.ts +6 -0
- package/dist/hermes/runtime/hermes-runtime-state-db.js +20 -1
- package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js +12 -20
- package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/openclaw/config/provider-config.js +4 -1
- package/dist/openclaw/config/provider-config.js.map +1 -1
- package/dist/openclaw/relay/agent-identity-helpers.d.ts +9 -0
- package/dist/openclaw/relay/agent-identity-helpers.js +118 -0
- package/dist/openclaw/relay/agent-identity-helpers.js.map +1 -0
- package/dist/openclaw/relay/assistant-media-sidecar.d.ts +19 -2
- package/dist/openclaw/relay/assistant-media-sidecar.js +131 -15
- package/dist/openclaw/relay/assistant-media-sidecar.js.map +1 -1
- package/dist/openclaw/relay/chat-history.d.ts +37 -2
- package/dist/openclaw/relay/chat-history.js +565 -51
- package/dist/openclaw/relay/chat-history.js.map +1 -1
- package/dist/openclaw/relay/chat-run-identity.js +3 -1
- package/dist/openclaw/relay/chat-run-identity.js.map +1 -1
- package/dist/openclaw/relay/gateway-history-projection.d.ts +4 -0
- package/dist/openclaw/relay/gateway-history-projection.js +67 -0
- package/dist/openclaw/relay/gateway-history-projection.js.map +1 -0
- package/dist/openclaw/relay/openclaw-active-run-state.d.ts +67 -0
- package/dist/openclaw/relay/openclaw-active-run-state.js +283 -0
- package/dist/openclaw/relay/openclaw-active-run-state.js.map +1 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.d.ts +12 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js +48 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js.map +1 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.d.ts +22 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.js +64 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.js.map +1 -0
- package/dist/openclaw/relay/outgoing-media-relay.d.ts +30 -3
- package/dist/openclaw/relay/outgoing-media-relay.js +407 -118
- package/dist/openclaw/relay/outgoing-media-relay.js.map +1 -1
- package/dist/openclaw/relay/relay-manager-history-timing.d.ts +1 -4
- package/dist/openclaw/relay/relay-manager-history-timing.js +4 -4
- package/dist/openclaw/relay/relay-manager-history-timing.js.map +1 -1
- package/dist/openclaw/relay/relay-manager-payload-helpers.d.ts +28 -1
- package/dist/openclaw/relay/relay-manager-payload-helpers.js +219 -11
- package/dist/openclaw/relay/relay-manager-payload-helpers.js.map +1 -1
- package/dist/openclaw/relay/session-context.d.ts +15 -0
- package/dist/openclaw/relay/session-context.js +48 -4
- package/dist/openclaw/relay/session-context.js.map +1 -1
- package/dist/openclaw/relay/source-commit-projection.d.ts +25 -0
- package/dist/openclaw/relay/source-commit-projection.js +140 -0
- package/dist/openclaw/relay/source-commit-projection.js.map +1 -0
- package/dist/openclaw/relay/transcript-dedupe.js +11 -42
- package/dist/openclaw/relay/transcript-dedupe.js.map +1 -1
- package/dist/openclaw/relay-manager.js +323 -135
- package/dist/openclaw/relay-manager.js.map +1 -1
- package/dist/openclaw/runtime/local-runtime.js +4 -2
- package/dist/openclaw/runtime/local-runtime.js.map +1 -1
- package/package.json +1 -1
- package/dist/openclaw/session-store.d.ts +0 -6
- package/dist/openclaw/session-store.js +0 -231
- package/dist/openclaw/session-store.js.map +0 -1
|
@@ -7,13 +7,15 @@ import { buildToolInvocationUpdatedEvent, } from "../../core/relay/timeline-even
|
|
|
7
7
|
import { forgetHermesSession, getMappedHermesSessionId, rememberHermesSession, } from "../hermes-session-store.js";
|
|
8
8
|
import { CHAT_TIMEOUT_MS, CLAWCONNECT_MOBILE_BRIDGE_HINT, HERMES_INBOX_DIR, SUBPROCESS_ENV, hermesInvocation, isHermesCommandDeniedTimeoutLine, isHermesMissingSessionError, runHermes, sanitizeHermesChatOutput, stripAnsi, stripHermesSessionResumeNotices, } from "./hermes-runtime-process.js";
|
|
9
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 { captureHermesSessionActiveHead, rewindHermesSessionAfterActiveHead, } from "./hermes-runtime-state-db.js";
|
|
13
|
-
import { tryRunHermesApiChat } from "./hermes-runtime-api-client.js";
|
|
10
|
+
import { compactStringArray, sanitizeFileName, stringValue } from "./hermes-runtime-values.js";
|
|
11
|
+
import { detectHermesHistoryCompletion, hermesSourceRunIdFromMessage, selectHermesSessionForCompletedChat, } from "./hermes-runtime-history-completion.js";
|
|
12
|
+
import { captureHermesSessionActiveHead, readHermesSessionMessages, rewindHermesSessionAfterActiveHead, } from "./hermes-runtime-state-db.js";
|
|
13
|
+
import { ensureHermesApiSessionForMobileRoute, tryRunHermesApiChat } from "./hermes-runtime-api-client.js";
|
|
14
14
|
import { resolveHermesPreloadedSkillContext } from "./hermes-runtime-preloaded-skills.js";
|
|
15
15
|
import { createHermesToolLogWatcher, hermesToolState, } from "./hermes-runtime-tool-log-watcher.js";
|
|
16
16
|
import { isHermesSlashCommandMessage, runHermesSlashCommand, } from "./hermes-runtime-slash-command.js";
|
|
17
|
+
import { clearPendingHermesFileTransfer, getPendingHermesFileTransfer, recordPendingHermesFileTransfer, } from "./hermes-file-transfer-state.js";
|
|
18
|
+
import { clearHermesMobileFileRoute, registerHermesMobileFileRoute, } from "./hermes-mobile-file-route-store.js";
|
|
17
19
|
export { selectHermesSessionForCompletedChat } from "./hermes-runtime-history-completion.js";
|
|
18
20
|
export { latestTerminalAssistantReplyFromHermesExport } from "./hermes-runtime-history-completion.js";
|
|
19
21
|
export { parseHermesToolLogLine } from "./hermes-runtime-tool-log-watcher.js";
|
|
@@ -33,6 +35,9 @@ const EMPTY_PRELOADED_SKILL_CONTEXT = {
|
|
|
33
35
|
requiredToolsets: [],
|
|
34
36
|
skillNames: [],
|
|
35
37
|
};
|
|
38
|
+
const HERMES_FILE_TRANSFER_NOT_SENT_MESSAGE = "文件尚未发送:没有检测到可验证的 clawconnect send-file 成功结果,请补充或确认要发送的文件后重试。";
|
|
39
|
+
const HERMES_FILE_TRANSFER_CONTENT_MISSING_MESSAGE = "本轮没有可显示的结构化回答。";
|
|
40
|
+
const HERMES_FILE_TRANSFER_CANCELLED_MESSAGE = "文件发送已取消。";
|
|
36
41
|
export async function runHermesChat(params, context = {}) {
|
|
37
42
|
const record = params && typeof params === "object" && !Array.isArray(params)
|
|
38
43
|
? params
|
|
@@ -56,8 +61,7 @@ export async function runHermesChat(params, context = {}) {
|
|
|
56
61
|
const sourceRunId = typeof context.requestId === "string" && context.requestId.trim().length > 0
|
|
57
62
|
? context.requestId.trim()
|
|
58
63
|
: undefined;
|
|
59
|
-
const
|
|
60
|
-
const preparedMessage = await prepareHermesMessage(rawMessage, record.attachments, sessionKey, sourceRunId, preparationPlan);
|
|
64
|
+
const preparedMessage = await prepareHermesMessage(rawMessage, record.attachments, sessionKey, sourceRunId);
|
|
61
65
|
if (!preparedMessage.cliMessage.trim()) {
|
|
62
66
|
throw new Error("message_required");
|
|
63
67
|
}
|
|
@@ -69,7 +73,6 @@ export async function runHermesChat(params, context = {}) {
|
|
|
69
73
|
sourceRunId,
|
|
70
74
|
hermesSessionId: record.hermesSessionId,
|
|
71
75
|
context,
|
|
72
|
-
preparationPlan,
|
|
73
76
|
});
|
|
74
77
|
});
|
|
75
78
|
}
|
|
@@ -94,30 +97,66 @@ async function runHermesChatPrepared(params) {
|
|
|
94
97
|
: undefined;
|
|
95
98
|
const mappedResume = explicitResume ? undefined : await getMappedHermesSessionId(params.sessionKey);
|
|
96
99
|
let resume = explicitResume ?? mappedResume;
|
|
97
|
-
const
|
|
100
|
+
const preparationPlan = await planHermesChatPreparation({
|
|
101
|
+
message: params.rawMessage,
|
|
102
|
+
gatewayId: params.context.gatewayId ?? "clawconnect",
|
|
103
|
+
sessionKey: params.sessionKey,
|
|
104
|
+
sessionId: resume,
|
|
105
|
+
sourceRunId: params.sourceRunId,
|
|
106
|
+
fileTransferCapability: params.context.hermesFileTransferCapability,
|
|
107
|
+
});
|
|
108
|
+
const preloadedSkillContext = preparationPlan.preloadFileTransferSkill
|
|
98
109
|
? await resolveHermesPreloadedSkillContext({ forceFileTransfer: true })
|
|
99
110
|
: EMPTY_PRELOADED_SKILL_CONTEXT;
|
|
111
|
+
if (preparationPlan.preloadFileTransferSkill && !resume && params.sourceRunId) {
|
|
112
|
+
const ensuredSessionId = await ensureHermesApiSessionForMobileRoute(params.sessionKey);
|
|
113
|
+
if (ensuredSessionId) {
|
|
114
|
+
resume = ensuredSessionId;
|
|
115
|
+
await rememberHermesSession(params.sessionKey, {
|
|
116
|
+
sessionKey: params.sessionKey,
|
|
117
|
+
hermesSessionId: ensuredSessionId,
|
|
118
|
+
displayName: params.sessionKey,
|
|
119
|
+
kind: "hermes",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
100
123
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
124
|
+
// File delivery is fail-closed and must be backed by the local
|
|
125
|
+
// clawconnect send-file tool. The API stream publishes assistant deltas
|
|
126
|
+
// before its final tool evidence is persisted, so route these turns
|
|
127
|
+
// through the CLI path where the answer is buffered until verification.
|
|
128
|
+
const apiChat = preparationPlan.preloadFileTransferSkill
|
|
129
|
+
? undefined
|
|
130
|
+
: await tryRunHermesApiChat({
|
|
131
|
+
message: params.preparedMessage.apiMessage,
|
|
132
|
+
instructions: params.preparedMessage.apiInstructions,
|
|
133
|
+
sessionKey: params.sessionKey,
|
|
134
|
+
resume,
|
|
135
|
+
preloadedSkillNames: preloadedSkillContext.skillNames,
|
|
136
|
+
requiredToolsets: preloadedSkillContext.requiredToolsets,
|
|
137
|
+
context: params.context,
|
|
138
|
+
});
|
|
110
139
|
if (apiChat) {
|
|
111
140
|
const recoveredOutput = await recoverEmptyHermesApiOutputFromHistory({
|
|
112
141
|
output: apiChat.output,
|
|
113
142
|
hermesSessionId: apiChat.hermesSessionId,
|
|
114
143
|
sessionKey: params.sessionKey,
|
|
115
144
|
userMessage: params.rawMessage,
|
|
145
|
+
sourceRunId: params.sourceRunId,
|
|
116
146
|
abortSignal: params.context.abortSignal,
|
|
117
147
|
});
|
|
148
|
+
const visibleOutput = requireVisibleHermesOutput(recoveredOutput || apiChat.output);
|
|
118
149
|
return {
|
|
119
150
|
...apiChat,
|
|
120
|
-
output:
|
|
151
|
+
output: visibleOutput,
|
|
152
|
+
...(await verifyHermesFileTransferIfRequired({
|
|
153
|
+
plan: preparationPlan,
|
|
154
|
+
gatewayId: params.context.gatewayId ?? "clawconnect",
|
|
155
|
+
sessionKey: params.sessionKey,
|
|
156
|
+
sessionId: apiChat.hermesSessionId,
|
|
157
|
+
sourceRunId: params.sourceRunId,
|
|
158
|
+
output: visibleOutput,
|
|
159
|
+
})),
|
|
121
160
|
};
|
|
122
161
|
}
|
|
123
162
|
}
|
|
@@ -132,25 +171,37 @@ async function runHermesChatPrepared(params) {
|
|
|
132
171
|
}
|
|
133
172
|
await forgetHermesSession(params.sessionKey, mappedResume);
|
|
134
173
|
resume = undefined;
|
|
135
|
-
const retryApiChat =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
174
|
+
const retryApiChat = preparationPlan.preloadFileTransferSkill
|
|
175
|
+
? undefined
|
|
176
|
+
: await tryRunHermesApiChat({
|
|
177
|
+
message: params.preparedMessage.apiMessage,
|
|
178
|
+
instructions: params.preparedMessage.apiInstructions,
|
|
179
|
+
sessionKey: params.sessionKey,
|
|
180
|
+
preloadedSkillNames: preloadedSkillContext.skillNames,
|
|
181
|
+
requiredToolsets: preloadedSkillContext.requiredToolsets,
|
|
182
|
+
context: params.context,
|
|
183
|
+
});
|
|
143
184
|
if (retryApiChat) {
|
|
144
185
|
const recoveredOutput = await recoverEmptyHermesApiOutputFromHistory({
|
|
145
186
|
output: retryApiChat.output,
|
|
146
187
|
hermesSessionId: retryApiChat.hermesSessionId,
|
|
147
188
|
sessionKey: params.sessionKey,
|
|
148
189
|
userMessage: params.rawMessage,
|
|
190
|
+
sourceRunId: params.sourceRunId,
|
|
149
191
|
abortSignal: params.context.abortSignal,
|
|
150
192
|
});
|
|
193
|
+
const visibleOutput = requireVisibleHermesOutput(recoveredOutput || retryApiChat.output);
|
|
151
194
|
return {
|
|
152
195
|
...retryApiChat,
|
|
153
|
-
output:
|
|
196
|
+
output: visibleOutput,
|
|
197
|
+
...(await verifyHermesFileTransferIfRequired({
|
|
198
|
+
plan: preparationPlan,
|
|
199
|
+
gatewayId: params.context.gatewayId ?? "clawconnect",
|
|
200
|
+
sessionKey: params.sessionKey,
|
|
201
|
+
sessionId: retryApiChat.hermesSessionId,
|
|
202
|
+
sourceRunId: params.sourceRunId,
|
|
203
|
+
output: visibleOutput,
|
|
204
|
+
})),
|
|
154
205
|
};
|
|
155
206
|
}
|
|
156
207
|
}
|
|
@@ -160,17 +211,21 @@ async function runHermesChatPrepared(params) {
|
|
|
160
211
|
: undefined;
|
|
161
212
|
let rawOutput;
|
|
162
213
|
try {
|
|
163
|
-
rawOutput = await
|
|
214
|
+
rawOutput = await runHermesChatOnceWithMobileFileRoute({
|
|
164
215
|
message: params.preparedMessage.cliMessage,
|
|
165
216
|
sessionKey: params.sessionKey,
|
|
166
217
|
resume,
|
|
167
218
|
context: params.context,
|
|
168
219
|
preloadedSkillArgs: preloadedSkillContext.cliArgs,
|
|
220
|
+
enableFileTransferRoute: preparationPlan.preloadFileTransferSkill,
|
|
221
|
+
gatewayId: params.context.gatewayId ?? "clawconnect",
|
|
222
|
+
sourceRunId: params.sourceRunId,
|
|
169
223
|
historyCompletion: () => detectHermesHistoryCompletion({
|
|
170
224
|
beforeSessions,
|
|
171
225
|
resume,
|
|
172
226
|
sessionKey: params.sessionKey,
|
|
173
227
|
userMessage: params.rawMessage,
|
|
228
|
+
sourceRunId: params.sourceRunId,
|
|
174
229
|
}),
|
|
175
230
|
});
|
|
176
231
|
}
|
|
@@ -191,15 +246,19 @@ async function runHermesChatPrepared(params) {
|
|
|
191
246
|
await forgetHermesSession(params.sessionKey, mappedResume);
|
|
192
247
|
resume = undefined;
|
|
193
248
|
cliActiveHead = undefined;
|
|
194
|
-
rawOutput = await
|
|
249
|
+
rawOutput = await runHermesChatOnceWithMobileFileRoute({
|
|
195
250
|
message: params.preparedMessage.cliMessage,
|
|
196
251
|
sessionKey: params.sessionKey,
|
|
197
252
|
context: params.context,
|
|
198
253
|
preloadedSkillArgs: preloadedSkillContext.cliArgs,
|
|
254
|
+
enableFileTransferRoute: preparationPlan.preloadFileTransferSkill,
|
|
255
|
+
gatewayId: params.context.gatewayId ?? "clawconnect",
|
|
256
|
+
sourceRunId: params.sourceRunId,
|
|
199
257
|
historyCompletion: () => detectHermesHistoryCompletion({
|
|
200
258
|
beforeSessions,
|
|
201
259
|
sessionKey: params.sessionKey,
|
|
202
260
|
userMessage: params.rawMessage,
|
|
261
|
+
sourceRunId: params.sourceRunId,
|
|
203
262
|
}),
|
|
204
263
|
});
|
|
205
264
|
}
|
|
@@ -214,6 +273,14 @@ async function runHermesChatPrepared(params) {
|
|
|
214
273
|
if (mappedSession) {
|
|
215
274
|
await rememberHermesSession(params.sessionKey, mappedSession);
|
|
216
275
|
}
|
|
276
|
+
const verifiedFileTransfer = await verifyHermesFileTransferIfRequired({
|
|
277
|
+
plan: preparationPlan,
|
|
278
|
+
gatewayId: params.context.gatewayId ?? "clawconnect",
|
|
279
|
+
sessionKey: params.sessionKey,
|
|
280
|
+
sessionId: resume ?? mappedSession?.hermesSessionId,
|
|
281
|
+
sourceRunId: params.sourceRunId,
|
|
282
|
+
output,
|
|
283
|
+
});
|
|
217
284
|
const usage = mappedSession?.hermesSessionId
|
|
218
285
|
? await collectHermesUsageSnapshot(mappedSession.hermesSessionId)
|
|
219
286
|
: await readHermesStatusSnapshotAsync();
|
|
@@ -222,6 +289,7 @@ async function runHermesChatPrepared(params) {
|
|
|
222
289
|
sessionKey: params.sessionKey,
|
|
223
290
|
artifactPaths: [],
|
|
224
291
|
usage,
|
|
292
|
+
...verifiedFileTransfer,
|
|
225
293
|
};
|
|
226
294
|
}
|
|
227
295
|
function isHermesChatAbortedError(error) {
|
|
@@ -289,6 +357,7 @@ async function recoverEmptyHermesApiOutputFromHistory(params) {
|
|
|
289
357
|
resume: params.hermesSessionId,
|
|
290
358
|
sessionKey: params.sessionKey,
|
|
291
359
|
userMessage: params.userMessage,
|
|
360
|
+
sourceRunId: params.sourceRunId,
|
|
292
361
|
});
|
|
293
362
|
if (detectedOutput) {
|
|
294
363
|
return detectedOutput;
|
|
@@ -327,6 +396,25 @@ async function runHermesChatOnce(params) {
|
|
|
327
396
|
? await runHermesChatStreaming(args, params.sessionKey, params.context, runId, env, params.historyCompletion)
|
|
328
397
|
: runHermes(args, CHAT_TIMEOUT_MS, env);
|
|
329
398
|
}
|
|
399
|
+
async function runHermesChatOnceWithMobileFileRoute(params) {
|
|
400
|
+
const hermesSessionId = params.resume?.trim();
|
|
401
|
+
const sourceRunId = params.sourceRunId?.trim();
|
|
402
|
+
if (!params.enableFileTransferRoute || !hermesSessionId || !sourceRunId) {
|
|
403
|
+
return await runHermesChatOnce(params);
|
|
404
|
+
}
|
|
405
|
+
await registerHermesMobileFileRoute({
|
|
406
|
+
hermesSessionId,
|
|
407
|
+
gatewayId: params.gatewayId,
|
|
408
|
+
sessionKey: params.sessionKey,
|
|
409
|
+
sourceRunId,
|
|
410
|
+
});
|
|
411
|
+
try {
|
|
412
|
+
return await runHermesChatOnce(params);
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
await clearHermesMobileFileRoute(hermesSessionId, sourceRunId);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
330
418
|
async function runHermesChatStreaming(args, sessionKey, context, runId, env, historyCompletion) {
|
|
331
419
|
const invocation = hermesInvocation(args);
|
|
332
420
|
const child = spawn(invocation.command, invocation.args, {
|
|
@@ -653,7 +741,7 @@ function buildClawConnectMobileTurnMetadata(sourceRunId, sessionKey) {
|
|
|
653
741
|
`sessionKey: ${sessionKey}`,
|
|
654
742
|
].join("\n");
|
|
655
743
|
}
|
|
656
|
-
async function prepareHermesMessage(message, attachments, sessionKey, sourceRunId
|
|
744
|
+
async function prepareHermesMessage(message, attachments, sessionKey, sourceRunId) {
|
|
657
745
|
const refs = [];
|
|
658
746
|
if (Array.isArray(attachments)) {
|
|
659
747
|
await cleanupExpiredHermesInbox();
|
|
@@ -742,32 +830,319 @@ export async function cleanupExpiredHermesInbox(inboxDir = HERMES_INBOX_DIR, now
|
|
|
742
830
|
}));
|
|
743
831
|
}));
|
|
744
832
|
}
|
|
745
|
-
function planHermesChatPreparation(
|
|
833
|
+
export async function planHermesChatPreparation(params) {
|
|
834
|
+
// A continuation is enabled only by a durable host-side pending state from
|
|
835
|
+
// the same gateway/mobile/Hermes session. The user's wording is deliberately
|
|
836
|
+
// opaque to this router; Hermes receives the full history and decides what it
|
|
837
|
+
// means (including any language, selection, or clarification).
|
|
838
|
+
const pending = params.sessionId
|
|
839
|
+
? await getPendingHermesFileTransfer({
|
|
840
|
+
gatewayId: params.gatewayId,
|
|
841
|
+
sessionKey: params.sessionKey,
|
|
842
|
+
hermesSessionId: params.sessionId,
|
|
843
|
+
sourceRunId: params.sourceRunId,
|
|
844
|
+
})
|
|
845
|
+
: undefined;
|
|
746
846
|
return {
|
|
747
|
-
|
|
847
|
+
// The mobile relay explicitly grants the typed CLI file-transfer
|
|
848
|
+
// capability on every turn. The model/tool protocol, rather than a
|
|
849
|
+
// language-specific intent table, decides whether a send is needed. This
|
|
850
|
+
// deliberately trades the API fast path for a buffered CLI path on mobile
|
|
851
|
+
// turns, where send-file receipts are persisted atomically.
|
|
852
|
+
preloadFileTransferSkill: params.fileTransferCapability === "cli" || pending !== undefined,
|
|
853
|
+
fileTransferMode: pending ? "continuation" : undefined,
|
|
854
|
+
...(pending ? { pendingFileTransfer: pending } : {}),
|
|
748
855
|
};
|
|
749
856
|
}
|
|
750
|
-
function
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
857
|
+
function normalizeHermesRole(value) {
|
|
858
|
+
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
859
|
+
}
|
|
860
|
+
export async function verifyHermesFileTransferIfRequired(params) {
|
|
861
|
+
if (!params.plan.preloadFileTransferSkill) {
|
|
862
|
+
return {};
|
|
863
|
+
}
|
|
864
|
+
const messages = params.sessionId ? await readHermesSessionMessages(params.sessionId) : undefined;
|
|
865
|
+
const evidence = params.sourceRunId
|
|
866
|
+
? collectHermesFileTransferEvidence(messages, params.sourceRunId)
|
|
867
|
+
: [];
|
|
868
|
+
if (evidence.length > 0) {
|
|
869
|
+
await clearPendingHermesFileTransfer(params.gatewayId, params.sessionKey, params.plan.pendingFileTransfer?.sourceRunId);
|
|
870
|
+
return {
|
|
871
|
+
verifiedFileTransferCount: evidence.length,
|
|
872
|
+
output: `已发送 ${evidence.length} 个文件,请查收。`,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
const outcome = params.sourceRunId
|
|
876
|
+
? collectHermesFileTransferOutcome(messages, params.sourceRunId)
|
|
877
|
+
: undefined;
|
|
878
|
+
if (outcome?.kind === "clarification") {
|
|
879
|
+
if (params.sessionId && params.sourceRunId) {
|
|
880
|
+
await recordPendingHermesFileTransfer({
|
|
881
|
+
gatewayId: params.gatewayId,
|
|
882
|
+
sessionKey: params.sessionKey,
|
|
883
|
+
hermesSessionId: params.sessionId,
|
|
884
|
+
sourceRunId: params.sourceRunId,
|
|
885
|
+
continuation: false,
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
// Only the typed content field is displayable. Raw assistant prose is
|
|
889
|
+
// deliberately discarded, and this outcome is never delivery evidence.
|
|
890
|
+
return {
|
|
891
|
+
output: outcome.assistantText ?? HERMES_FILE_TRANSFER_CONTENT_MISSING_MESSAGE,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
if (outcome?.kind === "cancelled") {
|
|
895
|
+
await clearPendingHermesFileTransfer(params.gatewayId, params.sessionKey, params.plan.pendingFileTransfer?.sourceRunId);
|
|
896
|
+
return {
|
|
897
|
+
output: outcome.assistantText ?? HERMES_FILE_TRANSFER_CANCELLED_MESSAGE,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
if (outcome?.kind === "ordinary") {
|
|
901
|
+
await clearPendingHermesFileTransfer(params.gatewayId, params.sessionKey, params.plan.pendingFileTransfer?.sourceRunId);
|
|
902
|
+
// Ordinary chat is shown only through the typed assistantText field. The
|
|
903
|
+
// model's ordinary label is routing metadata, never proof of delivery.
|
|
904
|
+
return {
|
|
905
|
+
output: outcome.assistantText ?? HERMES_FILE_TRANSFER_CONTENT_MISSING_MESSAGE,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
if (outcome?.kind === "attempted") {
|
|
909
|
+
await clearPendingHermesFileTransfer(params.gatewayId, params.sessionKey, params.plan.pendingFileTransfer?.sourceRunId);
|
|
910
|
+
return {
|
|
911
|
+
verifiedFileTransferCount: 0,
|
|
912
|
+
output: HERMES_FILE_TRANSFER_NOT_SENT_MESSAGE,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
if (params.sourceRunId && hasHermesSendFileAttempt(messages, params.sourceRunId)) {
|
|
916
|
+
return {
|
|
917
|
+
verifiedFileTransferCount: 0,
|
|
918
|
+
output: HERMES_FILE_TRANSFER_NOT_SENT_MESSAGE,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
// File-transfer capability is available on every mobile turn, but capability
|
|
922
|
+
// alone does not turn ordinary chat into a file operation. Preserve the Host
|
|
923
|
+
// answer unless this exact turn emitted a typed outcome or actually invoked
|
|
924
|
+
// the first-party terminal command. This keeps failure closed around a real
|
|
925
|
+
// send attempt without replacing unrelated Hermes replies.
|
|
926
|
+
return {};
|
|
927
|
+
}
|
|
928
|
+
function hasHermesSendFileAttempt(messages, sourceRunId) {
|
|
929
|
+
const currentIndex = currentHermesTurnIndex(messages, sourceRunId);
|
|
930
|
+
if (currentIndex < 0 || !messages)
|
|
754
931
|
return false;
|
|
932
|
+
for (const message of messages.slice(currentIndex + 1)) {
|
|
933
|
+
if (normalizeHermesRole(message.role) === "user")
|
|
934
|
+
break;
|
|
935
|
+
if (normalizeHermesRole(message.role) !== "assistant")
|
|
936
|
+
continue;
|
|
937
|
+
const rawToolCalls = typeof message.tool_calls === "string"
|
|
938
|
+
? parseJsonValue(message.tool_calls)
|
|
939
|
+
: message.tool_calls;
|
|
940
|
+
if (!Array.isArray(rawToolCalls))
|
|
941
|
+
continue;
|
|
942
|
+
for (const rawToolCall of rawToolCalls) {
|
|
943
|
+
const toolCall = rawToolCall && typeof rawToolCall === "object" && !Array.isArray(rawToolCall)
|
|
944
|
+
? rawToolCall
|
|
945
|
+
: undefined;
|
|
946
|
+
const fn = toolCall?.function && typeof toolCall.function === "object" && !Array.isArray(toolCall.function)
|
|
947
|
+
? toolCall.function
|
|
948
|
+
: undefined;
|
|
949
|
+
const functionName = normalizeHermesRole(fn?.name);
|
|
950
|
+
if (functionName === "clawconnect_send_file")
|
|
951
|
+
return true;
|
|
952
|
+
if (functionName !== "terminal")
|
|
953
|
+
continue;
|
|
954
|
+
const rawArguments = typeof fn?.arguments === "string"
|
|
955
|
+
? parseJsonValue(fn.arguments)
|
|
956
|
+
: fn?.arguments;
|
|
957
|
+
const args = rawArguments && typeof rawArguments === "object" && !Array.isArray(rawArguments)
|
|
958
|
+
? rawArguments
|
|
959
|
+
: undefined;
|
|
960
|
+
if (typeof args?.command === "string" && isClawConnectSendFileCommand(args.command)) {
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
function isClawConnectSendFileCommand(command) {
|
|
968
|
+
return /(?:^|[\s;&|])(?:["'][^"']*clawconnect["']|[^\s;&|]*clawconnect)\s+send-file(?:\s|$)/i.test(command);
|
|
969
|
+
}
|
|
970
|
+
function parseJsonValue(value) {
|
|
971
|
+
try {
|
|
972
|
+
return JSON.parse(value);
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
return undefined;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/** Read only first-party terminal outcome records for the exact current turn. */
|
|
979
|
+
export function collectHermesFileTransferOutcome(messages, sourceRunId) {
|
|
980
|
+
const currentIndex = currentHermesTurnIndex(messages, sourceRunId);
|
|
981
|
+
if (currentIndex < 0 || !messages)
|
|
982
|
+
return undefined;
|
|
983
|
+
const outcomes = [];
|
|
984
|
+
for (const message of messages.slice(currentIndex + 1)) {
|
|
985
|
+
if (normalizeHermesRole(message.role) === "user")
|
|
986
|
+
break;
|
|
987
|
+
if (normalizeHermesRole(message.role) !== "tool" || normalizeHermesRole(message.tool_name) !== "terminal")
|
|
988
|
+
continue;
|
|
989
|
+
for (const record of terminalResultRecords(message)) {
|
|
990
|
+
if (record.protocol !== "clawconnect.hermes-file-transfer-outcome.v1"
|
|
991
|
+
|| record.sourceRunId !== sourceRunId
|
|
992
|
+
|| record.sourceRole !== "assistant"
|
|
993
|
+
|| record.status !== "completed"
|
|
994
|
+
|| record.exit_code !== 0
|
|
995
|
+
|| (record.kind !== "ordinary" && record.kind !== "clarification" && record.kind !== "attempted" && record.kind !== "cancelled"))
|
|
996
|
+
continue;
|
|
997
|
+
const assistantText = typeof record.assistantText === "string" ? record.assistantText.trim() : "";
|
|
998
|
+
if (record.kind !== "attempted" && !assistantText)
|
|
999
|
+
continue;
|
|
1000
|
+
outcomes.push({
|
|
1001
|
+
protocol: "clawconnect.hermes-file-transfer-outcome.v1",
|
|
1002
|
+
kind: record.kind,
|
|
1003
|
+
sourceRunId,
|
|
1004
|
+
sourceRole: "assistant",
|
|
1005
|
+
status: "completed",
|
|
1006
|
+
...(assistantText ? { assistantText } : {}),
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
if (outcomes.length !== 1)
|
|
1011
|
+
return undefined;
|
|
1012
|
+
return outcomes[0];
|
|
1013
|
+
}
|
|
1014
|
+
export function collectHermesFileTransferEvidence(messages, sourceRunId) {
|
|
1015
|
+
if (!messages) {
|
|
1016
|
+
return [];
|
|
1017
|
+
}
|
|
1018
|
+
const currentIndex = currentHermesTurnIndex(messages, sourceRunId);
|
|
1019
|
+
if (currentIndex < 0) {
|
|
1020
|
+
return [];
|
|
1021
|
+
}
|
|
1022
|
+
const ids = new Set();
|
|
1023
|
+
for (const message of messages.slice(currentIndex + 1)) {
|
|
1024
|
+
if (normalizeHermesRole(message.role) === "user") {
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
if (normalizeHermesRole(message.role) !== "tool")
|
|
1028
|
+
continue;
|
|
1029
|
+
const toolName = normalizeHermesRole(message.tool_name);
|
|
1030
|
+
if (toolName === "clawconnect_send_file") {
|
|
1031
|
+
const receipt = parseJsonRecord(stringValue(message.content) ?? "");
|
|
1032
|
+
if (receipt?.ok === true
|
|
1033
|
+
&& receipt.sourceRunId === sourceRunId
|
|
1034
|
+
&& receipt.status === "completed"
|
|
1035
|
+
&& typeof receipt.fileId === "string"
|
|
1036
|
+
&& /^file_[a-z0-9]+$/i.test(receipt.fileId)) {
|
|
1037
|
+
ids.add(receipt.fileId);
|
|
1038
|
+
}
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
// Only the legacy first-party terminal result is additional evidence. A
|
|
1042
|
+
// delegate summary, wrapper output, or assistant claim cannot manufacture
|
|
1043
|
+
// an attachment by repeating the command name and a success word.
|
|
1044
|
+
if (toolName !== "terminal")
|
|
1045
|
+
continue;
|
|
1046
|
+
for (const record of terminalResultRecords(message)) {
|
|
1047
|
+
if (record.sourceRunId !== sourceRunId
|
|
1048
|
+
|| record.sourceRole !== "assistant"
|
|
1049
|
+
|| record.status !== "completed"
|
|
1050
|
+
|| record.exit_code !== 0
|
|
1051
|
+
|| typeof record.fileId !== "string"
|
|
1052
|
+
|| !/^file_[a-z0-9]+$/i.test(record.fileId)) {
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
ids.add(record.fileId);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return [...ids];
|
|
1059
|
+
}
|
|
1060
|
+
function currentHermesTurnIndex(messages, sourceRunId) {
|
|
1061
|
+
if (!messages || !sourceRunId.trim())
|
|
1062
|
+
return -1;
|
|
1063
|
+
let currentIndex = -1;
|
|
1064
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
1065
|
+
const message = messages[index];
|
|
1066
|
+
if (normalizeHermesRole(message?.role) === "user" && hermesSourceRunIdFromMessage(message) === sourceRunId) {
|
|
1067
|
+
currentIndex = index;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return currentIndex;
|
|
1071
|
+
}
|
|
1072
|
+
function terminalResultRecords(message) {
|
|
1073
|
+
const content = stringValue(message.content);
|
|
1074
|
+
if (!content) {
|
|
1075
|
+
return [];
|
|
1076
|
+
}
|
|
1077
|
+
const outer = parseJsonRecord(content);
|
|
1078
|
+
if (!outer) {
|
|
1079
|
+
return [];
|
|
1080
|
+
}
|
|
1081
|
+
const records = [outer];
|
|
1082
|
+
const output = stringValue(outer.output);
|
|
1083
|
+
if (output) {
|
|
1084
|
+
for (const fragment of extractJsonObjects(output)) {
|
|
1085
|
+
const record = parseJsonRecord(fragment);
|
|
1086
|
+
if (record) {
|
|
1087
|
+
// Hermes stores the command envelope's exit_code beside the textual
|
|
1088
|
+
// send-file JSON payload; the payload itself carries the Relay fields.
|
|
1089
|
+
records.push(record.exit_code === undefined && outer.exit_code !== undefined
|
|
1090
|
+
? { ...record, exit_code: outer.exit_code }
|
|
1091
|
+
: record);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return records;
|
|
1096
|
+
}
|
|
1097
|
+
function parseJsonRecord(value) {
|
|
1098
|
+
try {
|
|
1099
|
+
const parsed = JSON.parse(value);
|
|
1100
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
1101
|
+
? parsed
|
|
1102
|
+
: undefined;
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
return undefined;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function extractJsonObjects(value) {
|
|
1109
|
+
const fragments = [];
|
|
1110
|
+
let start = -1;
|
|
1111
|
+
let depth = 0;
|
|
1112
|
+
let inString = false;
|
|
1113
|
+
let escaped = false;
|
|
1114
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1115
|
+
const character = value[index];
|
|
1116
|
+
if (inString) {
|
|
1117
|
+
if (escaped) {
|
|
1118
|
+
escaped = false;
|
|
1119
|
+
}
|
|
1120
|
+
else if (character === "\\") {
|
|
1121
|
+
escaped = true;
|
|
1122
|
+
}
|
|
1123
|
+
else if (character === '"') {
|
|
1124
|
+
inString = false;
|
|
1125
|
+
}
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
if (character === '"') {
|
|
1129
|
+
inString = true;
|
|
1130
|
+
}
|
|
1131
|
+
else if (character === "{") {
|
|
1132
|
+
if (depth === 0) {
|
|
1133
|
+
start = index;
|
|
1134
|
+
}
|
|
1135
|
+
depth += 1;
|
|
1136
|
+
}
|
|
1137
|
+
else if (character === "}" && depth > 0) {
|
|
1138
|
+
depth -= 1;
|
|
1139
|
+
if (depth === 0 && start >= 0) {
|
|
1140
|
+
fragments.push(value.slice(start, index + 1));
|
|
1141
|
+
start = -1;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
755
1144
|
}
|
|
756
|
-
|
|
757
|
-
return true;
|
|
758
|
-
}
|
|
759
|
-
const pathOrExtension = /(?:^|\s|["'“”‘’])(?:~|\/|[a-z]:\\)|\.(?:png|jpe?g|gif|webp|heic|pdf|docx?|xlsx?|pptx?|zip|txt|md)\b/i.test(text);
|
|
760
|
-
const englishSend = /\b(send|transfer|upload|attach|share|copy)\b/i.test(text);
|
|
761
|
-
const englishFile = /\b(file|image|photo|picture|screenshot|document|attachment)\b/i.test(text) || pathOrExtension;
|
|
762
|
-
const englishTarget = /\b(phone|iphone|mobile|clawlink|this device|me)\b/i.test(text);
|
|
763
|
-
if (englishSend && englishFile && englishTarget) {
|
|
764
|
-
return true;
|
|
765
|
-
}
|
|
766
|
-
const chineseSend = /发送|发给|发到|发过来|传到|传给|传输|上传|分享|转发|给我|发/.test(text);
|
|
767
|
-
const chineseFile = /文件|图片|照片|截图|文档|附件|桌面|本机|路径|微信图片/.test(text) || pathOrExtension;
|
|
768
|
-
const chineseTarget = /手机|移动端|这台|给我|发过来|iPhone|ClawLink/i.test(text);
|
|
769
|
-
// 文件传输技能会触发 Hermes CLI 的 skills list;普通文本必须不走这条慢路径。
|
|
770
|
-
return chineseSend && chineseFile && chineseTarget;
|
|
1145
|
+
return fragments;
|
|
771
1146
|
}
|
|
772
1147
|
export function buildHermesRuntimeContextHint(snapshot) {
|
|
773
1148
|
const details = compactStringArray([
|