ticlawk 0.1.16-dev.6 → 0.1.16-dev.8
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/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +16 -28
- package/src/adapters/ticlawk/index.mjs +0 -24
- package/src/cli/agent-commands.mjs +66 -5
- package/src/core/agent-cli-handlers.mjs +29 -2
- package/src/core/http.mjs +7 -0
- package/src/core/runtime-support.mjs +0 -30
- package/src/runtimes/_shared/standing-prompt.mjs +21 -0
- package/src/runtimes/claude-code/index.mjs +0 -6
- package/src/runtimes/codex/index.mjs +0 -7
- package/src/runtimes/openclaw/index.mjs +5 -17
- package/src/runtimes/opencode/index.mjs +6 -23
- package/src/runtimes/pi/index.mjs +3 -17
- package/src/adapters/ticlawk/cards.mjs +0 -149
- package/src/core/media/outbound.mjs +0 -163
package/package.json
CHANGED
|
@@ -149,18 +149,6 @@ export async function updateChannel(id, updates) {
|
|
|
149
149
|
return updateAgent(id, updates);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
export async function postRuntimeResult(body) {
|
|
153
|
-
// Activity-only: backend records the agent_event for UI trajectory
|
|
154
|
-
// display but does NOT project it into a chat message. The agent CLI's
|
|
155
|
-
// `ticlawk message send` is the only path that produces chat rows.
|
|
156
|
-
const result = await apiFetch('/api/runtime-results', {
|
|
157
|
-
method: 'POST',
|
|
158
|
-
body: JSON.stringify(body),
|
|
159
|
-
timeout: 30000,
|
|
160
|
-
});
|
|
161
|
-
return result;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
152
|
// ── Deliveries (replaces legacy message_jobs poll/ack) ──
|
|
165
153
|
|
|
166
154
|
export async function claimPendingDeliveries(hostId, limit = 5, excludedAgentIds = []) {
|
|
@@ -199,6 +187,7 @@ export async function sendAgentMessage({
|
|
|
199
187
|
replyToMessageId,
|
|
200
188
|
runtimeHostId,
|
|
201
189
|
visibility,
|
|
190
|
+
mediaAssetIds,
|
|
202
191
|
}) {
|
|
203
192
|
const { data } = await apiFetch('/api/agent/messages/send', {
|
|
204
193
|
method: 'POST',
|
|
@@ -210,6 +199,7 @@ export async function sendAgentMessage({
|
|
|
210
199
|
reply_to_message_id: replyToMessageId ?? null,
|
|
211
200
|
runtime_host_id: runtimeHostId ?? null,
|
|
212
201
|
visibility: visibility || null,
|
|
202
|
+
media_asset_ids: Array.isArray(mediaAssetIds) && mediaAssetIds.length > 0 ? mediaAssetIds : undefined,
|
|
213
203
|
}),
|
|
214
204
|
});
|
|
215
205
|
return data || null;
|
|
@@ -436,6 +426,20 @@ export async function uploadAgentAttachment({
|
|
|
436
426
|
});
|
|
437
427
|
}
|
|
438
428
|
|
|
429
|
+
export async function uploadAgentAvatar({
|
|
430
|
+
actingAgentId, filename, contentType, dataBase64,
|
|
431
|
+
}) {
|
|
432
|
+
return apiFetch('/api/agent/profile/avatar', {
|
|
433
|
+
method: 'POST',
|
|
434
|
+
body: JSON.stringify({
|
|
435
|
+
acting_as_agent_id: actingAgentId,
|
|
436
|
+
filename,
|
|
437
|
+
content_type: contentType,
|
|
438
|
+
data_base64: dataBase64,
|
|
439
|
+
}),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
439
443
|
export async function viewAgentAttachment({ actingAgentId, assetId }) {
|
|
440
444
|
const params = new URLSearchParams();
|
|
441
445
|
params.set('acting_as_agent_id', actingAgentId);
|
|
@@ -480,22 +484,6 @@ export async function removeAgentGroupMember({
|
|
|
480
484
|
);
|
|
481
485
|
}
|
|
482
486
|
|
|
483
|
-
// ── Upload ──
|
|
484
|
-
|
|
485
|
-
export async function uploadAsset(fileName, fileData, contentType, kind = 'chat_media') {
|
|
486
|
-
const formData = new FormData();
|
|
487
|
-
formData.append('file', new Blob([fileData], { type: contentType }), fileName);
|
|
488
|
-
formData.append('kind', kind);
|
|
489
|
-
if (contentType) formData.append('content_type', contentType);
|
|
490
|
-
|
|
491
|
-
const { data } = await apiFetch('/api/assets/upload', {
|
|
492
|
-
method: 'POST',
|
|
493
|
-
body: formData,
|
|
494
|
-
timeout: 30000,
|
|
495
|
-
});
|
|
496
|
-
return data;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
487
|
// ── Channel event pipe ──
|
|
500
488
|
|
|
501
489
|
export async function postEvent({ agent, agent_id, runtime_host_id, session_id, cwd, runtime_version, event, required = false }) {
|
|
@@ -10,7 +10,6 @@ import { isTerminalRuntimeFailure } from '../../core/runtime-support.mjs';
|
|
|
10
10
|
import { clearUpdateRequiredState, readUpdateState, setUpdateRequiredState } from '../../core/update-state.mjs';
|
|
11
11
|
import { isManagedInstall, startDetachedSelfUpdate } from '../../core/update.mjs';
|
|
12
12
|
import * as api from './api.mjs';
|
|
13
|
-
import { processAndSaveResult } from './cards.mjs';
|
|
14
13
|
import { persistApiCredential } from './credentials.mjs';
|
|
15
14
|
import { TiclawkWakeClient } from './wake-client.mjs';
|
|
16
15
|
|
|
@@ -205,10 +204,6 @@ export function normalizeInboundMessage(msg) {
|
|
|
205
204
|
};
|
|
206
205
|
}
|
|
207
206
|
|
|
208
|
-
function getBindingSessionId(binding) {
|
|
209
|
-
return binding?.runtimeMeta?.sessionId || binding?.runtimeMeta?.sessionKey || binding?.runtimeMeta?.agentId || binding?.id || null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
207
|
function getRuntimeVersion(bindingOrMeta) {
|
|
213
208
|
const value = bindingOrMeta?.runtimeMeta?.runtimeVersion ?? bindingOrMeta?.runtimeVersion ?? bindingOrMeta?.agent_runtime_version ?? null;
|
|
214
209
|
return Number.isInteger(value) ? Number(value) : null;
|
|
@@ -1341,25 +1336,6 @@ export function createTiclawkAdapter(ctx) {
|
|
|
1341
1336
|
}
|
|
1342
1337
|
},
|
|
1343
1338
|
|
|
1344
|
-
async send(binding, outbound) {
|
|
1345
|
-
const localMediaPaths = (outbound.media || [])
|
|
1346
|
-
.filter((item) => item.kind === 'local_path')
|
|
1347
|
-
.map((item) => item.value);
|
|
1348
|
-
await processAndSaveResult({
|
|
1349
|
-
text: outbound.text || '',
|
|
1350
|
-
mediaUrls: localMediaPaths,
|
|
1351
|
-
}, {
|
|
1352
|
-
agentId: binding.id,
|
|
1353
|
-
hostId,
|
|
1354
|
-
agent: binding.runtime,
|
|
1355
|
-
sessionId: getBindingSessionId(binding),
|
|
1356
|
-
runtimeVersion: getRuntimeVersion(binding),
|
|
1357
|
-
turnId: outbound.turnId || outbound.replyToMessageId || null,
|
|
1358
|
-
type: outbound.type || 'agent_message',
|
|
1359
|
-
replyToMessageId: outbound.replyToMessageId || null,
|
|
1360
|
-
});
|
|
1361
|
-
},
|
|
1362
|
-
|
|
1363
1339
|
// Daemon-driven impersonated reply: the runtime never reached
|
|
1364
1340
|
// `ticlawk message send` (subprocess died, timeout, etc.), so the
|
|
1365
1341
|
// daemon speaks for the agent. Used by reportSubprocessFailure
|
|
@@ -136,12 +136,37 @@ export async function runMessageSendCommand(args) {
|
|
|
136
136
|
console.error(' EOF');
|
|
137
137
|
return 2;
|
|
138
138
|
}
|
|
139
|
+
|
|
140
|
+
// --attach <path> may be passed multiple times. Each file is uploaded
|
|
141
|
+
// through the same daemon endpoint as `ticlawk attachment upload`, then
|
|
142
|
+
// its asset_id is linked to the message via media_asset_ids.
|
|
143
|
+
const attachArg = args.attach;
|
|
144
|
+
const attachPaths = Array.isArray(attachArg)
|
|
145
|
+
? attachArg
|
|
146
|
+
: attachArg
|
|
147
|
+
? [attachArg]
|
|
148
|
+
: [];
|
|
149
|
+
if (attachPaths.length > 10) {
|
|
150
|
+
console.error('at most 10 --attach files per message');
|
|
151
|
+
return 2;
|
|
152
|
+
}
|
|
153
|
+
const mediaAssetIds = [];
|
|
154
|
+
for (const filePath of attachPaths) {
|
|
155
|
+
const upload = await uploadFileViaDaemon(env, String(filePath));
|
|
156
|
+
if (!upload.ok) {
|
|
157
|
+
console.error(`attachment upload failed for ${filePath}: ${upload.error}`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
mediaAssetIds.push(upload.assetId);
|
|
161
|
+
}
|
|
162
|
+
|
|
139
163
|
const body = {
|
|
140
164
|
target,
|
|
141
165
|
conversation_id: conversationId,
|
|
142
166
|
text: text.replace(/\n+$/, ''),
|
|
143
167
|
seen_up_to_seq: getNumberArg(args, 'seen-up-to-seq'),
|
|
144
168
|
reply_to_message_id: getArg(args, 'reply-to'),
|
|
169
|
+
media_asset_ids: mediaAssetIds.length > 0 ? mediaAssetIds : undefined,
|
|
145
170
|
};
|
|
146
171
|
const res = await daemonRequest({
|
|
147
172
|
method: 'POST',
|
|
@@ -545,13 +570,12 @@ export async function runProfileUpdateCommand(args) {
|
|
|
545
570
|
}
|
|
546
571
|
let avatarUrl = null;
|
|
547
572
|
if (avatarFile) {
|
|
548
|
-
|
|
549
|
-
const upload = await uploadFileViaDaemon(env, avatarFile);
|
|
573
|
+
const upload = await uploadAvatarViaDaemon(env, avatarFile);
|
|
550
574
|
if (!upload.ok) {
|
|
551
575
|
console.error(`avatar upload failed: ${upload.error}`);
|
|
552
576
|
return 1;
|
|
553
577
|
}
|
|
554
|
-
avatarUrl = upload.
|
|
578
|
+
avatarUrl = upload.url;
|
|
555
579
|
}
|
|
556
580
|
const res = await daemonRequest({
|
|
557
581
|
method: 'POST',
|
|
@@ -567,6 +591,34 @@ export async function runProfileUpdateCommand(args) {
|
|
|
567
591
|
return exitFromStatus(res.statusCode);
|
|
568
592
|
}
|
|
569
593
|
|
|
594
|
+
async function uploadAvatarViaDaemon(env, filePath) {
|
|
595
|
+
let stat;
|
|
596
|
+
try { stat = statSync(filePath); } catch (err) {
|
|
597
|
+
return { ok: false, error: `cannot stat ${filePath}: ${err.message}` };
|
|
598
|
+
}
|
|
599
|
+
if (!stat.isFile()) return { ok: false, error: `${filePath} is not a regular file` };
|
|
600
|
+
const contentType = inferContentType(filePath);
|
|
601
|
+
if (!contentType.startsWith('image/')) {
|
|
602
|
+
return { ok: false, error: `avatar must be an image (got content_type ${contentType})` };
|
|
603
|
+
}
|
|
604
|
+
const data = readFileSync(filePath);
|
|
605
|
+
const res = await daemonRequest({
|
|
606
|
+
method: 'POST',
|
|
607
|
+
path: '/agent/profile/avatar',
|
|
608
|
+
headers: commonHeaders(env),
|
|
609
|
+
body: {
|
|
610
|
+
filename: basename(filePath),
|
|
611
|
+
content_type: contentType,
|
|
612
|
+
data_base64: data.toString('base64'),
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
616
|
+
return { ok: false, error: res.body?.error || `HTTP ${res.statusCode}` };
|
|
617
|
+
}
|
|
618
|
+
if (!res.body?.url) return { ok: false, error: 'avatar upload returned no url' };
|
|
619
|
+
return { ok: true, url: res.body.url };
|
|
620
|
+
}
|
|
621
|
+
|
|
570
622
|
async function uploadFileViaDaemon(env, filePath) {
|
|
571
623
|
let stat;
|
|
572
624
|
try { stat = statSync(filePath); } catch (err) {
|
|
@@ -588,7 +640,11 @@ async function uploadFileViaDaemon(env, filePath) {
|
|
|
588
640
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
589
641
|
return { ok: false, error: res.body?.error || `HTTP ${res.statusCode}` };
|
|
590
642
|
}
|
|
591
|
-
|
|
643
|
+
const asset = res.body?.data;
|
|
644
|
+
if (!asset?.asset_id || !asset?.url) {
|
|
645
|
+
return { ok: false, error: 'attachment upload returned no asset' };
|
|
646
|
+
}
|
|
647
|
+
return { ok: true, assetId: asset.asset_id, url: asset.url, expiresAt: asset.expires_at };
|
|
592
648
|
}
|
|
593
649
|
|
|
594
650
|
function inferContentType(filePath) {
|
|
@@ -771,12 +827,14 @@ export async function runServerInfoCommand(args) {
|
|
|
771
827
|
|
|
772
828
|
export const AGENT_COMMAND_HELP = {
|
|
773
829
|
message: `ticlawk message <send|read|check|search|react>
|
|
774
|
-
ticlawk message send --target "<target>" [--seen-up-to-seq N] [--reply-to <msg-id>]
|
|
830
|
+
ticlawk message send --target "<target>" [--seen-up-to-seq N] [--reply-to <msg-id>] [--attach <file> ...]
|
|
775
831
|
Body is read from stdin (use <<'EOF' ... EOF for multiline).
|
|
776
832
|
Targets:
|
|
777
833
|
dm:@<user> private message
|
|
778
834
|
#<group> group conversation
|
|
779
835
|
#<group>:<msgid> thread under a top-level message in that group
|
|
836
|
+
--attach <file> uploads a local file and attaches it to the message
|
|
837
|
+
(repeatable; max 10 attachments per message).
|
|
780
838
|
ticlawk message read --target "<target>" [--around <msg-id>] [--before-seq N] [--limit N]
|
|
781
839
|
ticlawk message check [--target "<target>"]
|
|
782
840
|
Non-blocking poll for new/unprocessed messages.
|
|
@@ -792,6 +850,9 @@ export const AGENT_COMMAND_HELP = {
|
|
|
792
850
|
`,
|
|
793
851
|
attachment: `ticlawk attachment <upload|view>
|
|
794
852
|
ticlawk attachment upload <file>
|
|
853
|
+
Upload-only — the user is NOT notified. Almost always you want
|
|
854
|
+
\`ticlawk message send --attach <file>\` instead, which uploads AND
|
|
855
|
+
surfaces the file to the user inside a chat message.
|
|
795
856
|
ticlawk attachment view <asset-id> [--out <path>]
|
|
796
857
|
`,
|
|
797
858
|
reminder: `ticlawk reminder <schedule|list|snooze|update|cancel|log>
|
|
@@ -145,6 +145,10 @@ export async function handleMessageSend(req, body, ctx) {
|
|
|
145
145
|
return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
const mediaAssetIds = Array.isArray(body?.media_asset_ids)
|
|
149
|
+
? body.media_asset_ids.map((v) => String(v).trim()).filter(Boolean)
|
|
150
|
+
: [];
|
|
151
|
+
|
|
148
152
|
try {
|
|
149
153
|
const data = await api.sendAgentMessage({
|
|
150
154
|
actingAgentId,
|
|
@@ -153,6 +157,7 @@ export async function handleMessageSend(req, body, ctx) {
|
|
|
153
157
|
seenUpToSeq: body?.seen_up_to_seq,
|
|
154
158
|
replyToMessageId: body?.reply_to_message_id || threadRootMsgId || null,
|
|
155
159
|
runtimeHostId: getRuntimeHostId(req, body),
|
|
160
|
+
mediaAssetIds,
|
|
156
161
|
});
|
|
157
162
|
debugLog('agent-cli', 'send.ok', {
|
|
158
163
|
actingAgentId,
|
|
@@ -587,8 +592,8 @@ export async function handleAttachmentUpload(req, body, ctx) {
|
|
|
587
592
|
});
|
|
588
593
|
debugLog('agent-cli', 'attachment.upload', {
|
|
589
594
|
actingAgentId,
|
|
590
|
-
asset_id: data?.
|
|
591
|
-
bytes: data?.
|
|
595
|
+
asset_id: data?.data?.asset_id,
|
|
596
|
+
bytes: data?.data?.size_bytes,
|
|
592
597
|
});
|
|
593
598
|
return { status: 200, body: data };
|
|
594
599
|
} catch (err) {
|
|
@@ -596,6 +601,28 @@ export async function handleAttachmentUpload(req, body, ctx) {
|
|
|
596
601
|
}
|
|
597
602
|
}
|
|
598
603
|
|
|
604
|
+
export async function handleProfileAvatarUpload(req, body, ctx) {
|
|
605
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
606
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
607
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
608
|
+
if (!body?.data_base64) return { status: 400, body: { error: 'data_base64 is required' } };
|
|
609
|
+
if (!body?.content_type || !String(body.content_type).startsWith('image/')) {
|
|
610
|
+
return { status: 400, body: { error: 'content_type must be image/*' } };
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const data = await api.uploadAgentAvatar({
|
|
614
|
+
actingAgentId,
|
|
615
|
+
filename: body?.filename || 'avatar.bin',
|
|
616
|
+
contentType: body.content_type,
|
|
617
|
+
dataBase64: body.data_base64,
|
|
618
|
+
});
|
|
619
|
+
debugLog('agent-cli', 'avatar.upload', { actingAgentId, url: data?.url });
|
|
620
|
+
return { status: 200, body: data };
|
|
621
|
+
} catch (err) {
|
|
622
|
+
return { status: err?.status || 500, body: { error: err?.message || 'avatar upload failed' } };
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
599
626
|
export async function handleAttachmentView(req, query, ctx) {
|
|
600
627
|
const actingAgentId = getActingAgentId(req, query);
|
|
601
628
|
const v = validateActingAgent(actingAgentId, ctx);
|
package/src/core/http.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
handleMessageRead,
|
|
12
12
|
handleMessageSearch,
|
|
13
13
|
handleMessageSend,
|
|
14
|
+
handleProfileAvatarUpload,
|
|
14
15
|
handleProfileShow,
|
|
15
16
|
handleProfileUpdate,
|
|
16
17
|
handleReminderCancel,
|
|
@@ -148,6 +149,12 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
|
|
|
148
149
|
const r = await handleProfileUpdate(req, body, cliCtx);
|
|
149
150
|
return writeJson(res, r.status, r.body);
|
|
150
151
|
}
|
|
152
|
+
if (urlNoQuery === '/agent/profile/avatar' && method === 'POST') {
|
|
153
|
+
const body = await readJsonBody(req);
|
|
154
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
155
|
+
const r = await handleProfileAvatarUpload(req, body, cliCtx);
|
|
156
|
+
return writeJson(res, r.status, r.body);
|
|
157
|
+
}
|
|
151
158
|
if (urlNoQuery === '/agent/attachment/upload' && method === 'POST') {
|
|
152
159
|
const body = await readJsonBody(req);
|
|
153
160
|
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
@@ -42,36 +42,6 @@ export function shouldStreamRuntime(runtimeName, runtime) {
|
|
|
42
42
|
return Boolean(runtime?.runTurnStream) && getStreamingMode(runtimeName);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export async function sendAdapterMessage(adapter, binding, payload) {
|
|
46
|
-
await adapter.send(binding, payload);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Record the runtime's final turn output as activity (NOT chat).
|
|
51
|
-
*
|
|
52
|
-
* Previously called `sendResult` and treated as "the chat reply path".
|
|
53
|
-
* After the group-chat upgrade, chat is produced exclusively by the
|
|
54
|
-
* agent invoking `ticlawk message send` via the CLI. The runtime's
|
|
55
|
-
* raw final output is still surfaced for trajectory/debug UI, but it
|
|
56
|
-
* no longer materializes as a `messages` row — the trigger
|
|
57
|
-
* `project_agent_event` was updated in PR-2b to drop the chat
|
|
58
|
-
* projection.
|
|
59
|
-
*
|
|
60
|
-
* Renamed so the call sites read self-evidently: "record activity"
|
|
61
|
-
* never reads as "send a chat message".
|
|
62
|
-
*/
|
|
63
|
-
export async function recordActivity(adapter, binding, inbound, result) {
|
|
64
|
-
if (!result?.text && (!result?.mediaUrls || result.mediaUrls.length === 0)) return;
|
|
65
|
-
await sendAdapterMessage(adapter, binding, {
|
|
66
|
-
type: 'assistant',
|
|
67
|
-
text: result.text || '',
|
|
68
|
-
media: result.media || [],
|
|
69
|
-
turnId: inbound.messageId || result?.turnId || null,
|
|
70
|
-
replyToMessageId: inbound.messageId || null,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
45
|
export async function reportSubprocessFailure({ adapter, binding, inbound, runtimeName, info }) {
|
|
76
46
|
if (!info || info.ok) return;
|
|
77
47
|
const seconds = ((info.durationMs || 0) / 1000).toFixed(1);
|
|
@@ -63,6 +63,27 @@ EOF
|
|
|
63
63
|
|
|
64
64
|
**IMPORTANT**: To reply to any message, always reuse the exact \`target\` from the received message. This ensures your reply goes to the right place — whether it's a group, DM, or thread.
|
|
65
65
|
|
|
66
|
+
### Sending files
|
|
67
|
+
|
|
68
|
+
When you produce a file the user should see (a generated report, a
|
|
69
|
+
screenshot, a downloaded asset, a code patch as a file), attach it
|
|
70
|
+
inline with the chat message:
|
|
71
|
+
|
|
72
|
+
\`\`\`bash
|
|
73
|
+
ticlawk message send --target "#group" --attach /tmp/report.pdf --attach /tmp/chart.png <<'EOF'
|
|
74
|
+
Here's the analysis you asked for — full report attached, plus the
|
|
75
|
+
key chart pulled out for quick reference.
|
|
76
|
+
EOF
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
- \`--attach <path>\` may be repeated; up to 10 files per message.
|
|
80
|
+
- Each file uploads to the secure file layer and surfaces to the user
|
|
81
|
+
inside the chat UI alongside your text.
|
|
82
|
+
- \`ticlawk attachment upload <file>\` exists as a separate command but
|
|
83
|
+
**only uploads** — the user is not notified and cannot see the file.
|
|
84
|
+
Almost never the right command. Only use it if you have a specific
|
|
85
|
+
reason to stash a file for later use without telling the user.
|
|
86
|
+
|
|
66
87
|
### Threads
|
|
67
88
|
|
|
68
89
|
Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main group.
|
|
@@ -23,11 +23,9 @@ import {
|
|
|
23
23
|
} from './session.mjs';
|
|
24
24
|
import { discoverSessions } from './transcripts.mjs';
|
|
25
25
|
import { buildImageMessageFromInbound } from '../../core/media/inbound.mjs';
|
|
26
|
-
import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
|
|
27
26
|
import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
28
27
|
import {
|
|
29
28
|
shouldStreamRuntime,
|
|
30
|
-
recordActivity,
|
|
31
29
|
reportSubprocessFailure,
|
|
32
30
|
terminalRuntimeFailure,
|
|
33
31
|
updateBindingRuntimeMeta,
|
|
@@ -196,10 +194,6 @@ export const claudeCodeRuntime = {
|
|
|
196
194
|
rotatePending: false,
|
|
197
195
|
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
198
196
|
}, { status: 'connected' });
|
|
199
|
-
await recordActivity(adapter, nextBinding, inbound, {
|
|
200
|
-
...result,
|
|
201
|
-
media: normalizeOutboundMedia(result),
|
|
202
|
-
});
|
|
203
197
|
await emitWorkerEvent({
|
|
204
198
|
adapter,
|
|
205
199
|
binding: nextBinding,
|
|
@@ -18,13 +18,10 @@ import {
|
|
|
18
18
|
requireCodexPath,
|
|
19
19
|
} from './session.mjs';
|
|
20
20
|
import { buildCodexInputFromInbound } from '../../core/media/inbound.mjs';
|
|
21
|
-
import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
|
|
22
21
|
import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
23
22
|
import {
|
|
24
23
|
shouldStreamRuntime,
|
|
25
24
|
createDeltaAggregator,
|
|
26
|
-
sendAdapterMessage,
|
|
27
|
-
recordActivity,
|
|
28
25
|
reportSubprocessFailure,
|
|
29
26
|
terminalRuntimeFailure,
|
|
30
27
|
updateBindingRuntimeMeta,
|
|
@@ -215,10 +212,6 @@ export const codexRuntime = {
|
|
|
215
212
|
rotatePending: false,
|
|
216
213
|
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
217
214
|
}, { status: 'connected' });
|
|
218
|
-
await recordActivity(adapter, nextBinding, inbound, {
|
|
219
|
-
...result,
|
|
220
|
-
media: normalizeOutboundMedia(result),
|
|
221
|
-
});
|
|
222
215
|
await emitWorkerEvent({
|
|
223
216
|
adapter,
|
|
224
217
|
binding: nextBinding,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { reportSubprocessFailure, sendAdapterMessage, recordActivity, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
|
|
1
|
+
import { reportSubprocessFailure, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
|
|
3
2
|
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
4
3
|
import { GATEWAY_HOST, GATEWAY_PORT } from './identity.mjs';
|
|
5
4
|
|
|
@@ -192,10 +191,6 @@ export const openClawRuntime = {
|
|
|
192
191
|
});
|
|
193
192
|
},
|
|
194
193
|
});
|
|
195
|
-
await recordActivity(adapter, binding, inbound, {
|
|
196
|
-
...result,
|
|
197
|
-
media: normalizeOutboundMedia(result),
|
|
198
|
-
});
|
|
199
194
|
return true;
|
|
200
195
|
} catch (err) {
|
|
201
196
|
if (typeof adapter.emitEvent === 'function') {
|
|
@@ -233,18 +228,11 @@ export const openClawRuntime = {
|
|
|
233
228
|
}
|
|
234
229
|
},
|
|
235
230
|
|
|
236
|
-
async recoverInFlight(
|
|
231
|
+
async recoverInFlight() {
|
|
232
|
+
// Just drain the persisted in-flight set — the user-facing notice
|
|
233
|
+
// that used to surface here went through the dead chat-projection
|
|
234
|
+
// path. OpenClaw is non-primary; this no-ops cleanly for now.
|
|
237
235
|
const entries = recoverInFlightEntries();
|
|
238
|
-
for (const entry of entries) {
|
|
239
|
-
const binding = ctx.getBinding(entry.bindingId);
|
|
240
|
-
if (!binding) continue;
|
|
241
|
-
await sendAdapterMessage(ctx.adapter, binding, {
|
|
242
|
-
type: 'assistant',
|
|
243
|
-
text: '⚠️ Lost while ticlawk restarted.\n\nThis OpenClaw message was in flight when ticlawk restarted. Please retry your message.',
|
|
244
|
-
media: [],
|
|
245
|
-
replyToMessageId: entry.messageId || null,
|
|
246
|
-
}).catch(() => {});
|
|
247
|
-
}
|
|
248
236
|
return entries.length;
|
|
249
237
|
},
|
|
250
238
|
|
|
@@ -24,13 +24,10 @@ import {
|
|
|
24
24
|
requireOpenCodePath,
|
|
25
25
|
} from './session.mjs';
|
|
26
26
|
import { buildOpenCodeInputFromInbound } from '../../core/media/inbound.mjs';
|
|
27
|
-
import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
|
|
28
27
|
import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
29
28
|
import {
|
|
30
29
|
shouldStreamRuntime,
|
|
31
30
|
createDeltaAggregator,
|
|
32
|
-
sendAdapterMessage,
|
|
33
|
-
recordActivity,
|
|
34
31
|
reportSubprocessFailure,
|
|
35
32
|
terminalRuntimeFailure,
|
|
36
33
|
updateBindingRuntimeMeta,
|
|
@@ -146,24 +143,14 @@ export const openCodeRuntime = {
|
|
|
146
143
|
const captionText = (inbound.text || '').trim();
|
|
147
144
|
|
|
148
145
|
if (files.length === 0 && !captionText) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
replyToMessageId: inbound.messageId || null,
|
|
154
|
-
});
|
|
146
|
+
// Image decode failed and no caption to fall back on — we have
|
|
147
|
+
// nothing meaningful to feed the model. Bail without a user
|
|
148
|
+
// notice; this runtime is non-primary and the dead chat-projection
|
|
149
|
+
// path that used to surface such notices is gone.
|
|
155
150
|
return true;
|
|
156
151
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Downloads all failed; tell the user we're proceeding with the caption alone.
|
|
160
|
-
await sendAdapterMessage(adapter, binding, {
|
|
161
|
-
type: 'assistant',
|
|
162
|
-
text: '⚠️ Could not access the attached image data; acting on the caption text only.',
|
|
163
|
-
media: [],
|
|
164
|
-
replyToMessageId: inbound.messageId || null,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
152
|
+
// If files.length === 0 && captionText, fall through with the
|
|
153
|
+
// caption-only message below — no inline user notice.
|
|
167
154
|
|
|
168
155
|
// If user sent images with no caption, give the model a minimal
|
|
169
156
|
// instruction so it has something to anchor on.
|
|
@@ -238,10 +225,6 @@ export const openCodeRuntime = {
|
|
|
238
225
|
rotatePending: false,
|
|
239
226
|
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
240
227
|
}, { status: 'connected' });
|
|
241
|
-
await recordActivity(adapter, nextBinding, inbound, {
|
|
242
|
-
...result,
|
|
243
|
-
media: normalizeOutboundMedia(result),
|
|
244
|
-
});
|
|
245
228
|
await emitWorkerEvent({
|
|
246
229
|
adapter,
|
|
247
230
|
binding: nextBinding,
|
|
@@ -22,8 +22,6 @@ import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
|
22
22
|
import {
|
|
23
23
|
shouldStreamRuntime,
|
|
24
24
|
createDeltaAggregator,
|
|
25
|
-
sendAdapterMessage,
|
|
26
|
-
recordActivity,
|
|
27
25
|
reportSubprocessFailure,
|
|
28
26
|
terminalRuntimeFailure,
|
|
29
27
|
updateBindingRuntimeMeta,
|
|
@@ -122,22 +120,11 @@ export const piRuntime = {
|
|
|
122
120
|
images = await buildPiImagesFromInbound(inbound);
|
|
123
121
|
const captionText = (inbound.text || '').trim();
|
|
124
122
|
if (images.length === 0 && !captionText) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
media: [],
|
|
129
|
-
replyToMessageId: inbound.messageId || null,
|
|
130
|
-
});
|
|
123
|
+
// Image decode failed and no caption to fall back on. Bail
|
|
124
|
+
// without a user notice; the dead chat-projection path that
|
|
125
|
+
// used to surface such notices is gone.
|
|
131
126
|
return true;
|
|
132
127
|
}
|
|
133
|
-
if (images.length === 0 && captionText) {
|
|
134
|
-
await sendAdapterMessage(adapter, binding, {
|
|
135
|
-
type: 'assistant',
|
|
136
|
-
text: '⚠️ Could not access the attached image data; acting on the caption text only.',
|
|
137
|
-
media: [],
|
|
138
|
-
replyToMessageId: inbound.messageId || null,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
128
|
message = captionText || 'Please analyze the attached image(s).';
|
|
142
129
|
}
|
|
143
130
|
|
|
@@ -209,7 +196,6 @@ export const piRuntime = {
|
|
|
209
196
|
rotatePending: false,
|
|
210
197
|
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
211
198
|
}, { status: 'connected' });
|
|
212
|
-
await recordActivity(adapter, nextBinding, inbound, result);
|
|
213
199
|
await emitWorkerEvent({
|
|
214
200
|
adapter,
|
|
215
201
|
binding: nextBinding,
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ticlawk adapter — final agent message / media write path.
|
|
3
|
-
*
|
|
4
|
-
* Everything here turns an agent result (text + optional media local paths)
|
|
5
|
-
* into a terminal runtime result written back to ticlawk.
|
|
6
|
-
*
|
|
7
|
-
* This module imports from `./api.mjs` (HTTP client) and from
|
|
8
|
-
* `../../core/logger.mjs` (structured logging). No runtime dependencies.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
-
import { extname } from 'node:path';
|
|
13
|
-
import { randomUUID } from 'node:crypto';
|
|
14
|
-
import * as api from './api.mjs';
|
|
15
|
-
import { extractMediaPaths } from '../../core/media/outbound.mjs';
|
|
16
|
-
import { debugLog, debugError } from '../../core/logger.mjs';
|
|
17
|
-
import { TICLAWK_CONNECTOR_API_KEY } from '../../core/config.mjs';
|
|
18
|
-
|
|
19
|
-
const MIME_MAP = {
|
|
20
|
-
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
21
|
-
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
22
|
-
'.mp4': 'video/mp4', '.webm': 'video/webm',
|
|
23
|
-
'.opus': 'audio/opus', '.mp3': 'audio/mpeg', '.wav': 'audio/wav',
|
|
24
|
-
'.pdf': 'application/pdf',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
async function uploadLocalAsset(localPath) {
|
|
28
|
-
if (!process.env[TICLAWK_CONNECTOR_API_KEY]) {
|
|
29
|
-
debugError('relay', 'upload.skipped', {
|
|
30
|
-
localPath,
|
|
31
|
-
reason: 'missing connector api key',
|
|
32
|
-
});
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
if (!existsSync(localPath)) {
|
|
36
|
-
debugError('relay', 'upload.skipped', {
|
|
37
|
-
localPath,
|
|
38
|
-
reason: 'file not found',
|
|
39
|
-
});
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const ext = extname(localPath).toLowerCase();
|
|
44
|
-
const contentType = MIME_MAP[ext] || 'application/octet-stream';
|
|
45
|
-
const fileName = `${Date.now()}-${randomUUID().slice(0, 8)}${ext}`;
|
|
46
|
-
const fileData = readFileSync(localPath);
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const asset = await api.uploadAsset(fileName, fileData, contentType);
|
|
50
|
-
if (!asset?.asset_id) {
|
|
51
|
-
debugError('relay', 'upload.failed', {
|
|
52
|
-
localPath,
|
|
53
|
-
fileName,
|
|
54
|
-
error: 'missing asset_id in upload response',
|
|
55
|
-
});
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
debugLog('relay', 'upload.ok', {
|
|
59
|
-
localPath,
|
|
60
|
-
fileName,
|
|
61
|
-
assetId: asset.asset_id,
|
|
62
|
-
contentType: asset.content_type || contentType,
|
|
63
|
-
sizeBytes: asset.size_bytes ?? null,
|
|
64
|
-
});
|
|
65
|
-
return asset;
|
|
66
|
-
} catch (err) {
|
|
67
|
-
debugError('relay', 'upload.failed', {
|
|
68
|
-
localPath,
|
|
69
|
-
fileName,
|
|
70
|
-
error: err.message,
|
|
71
|
-
});
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function uploadMediaAssets(localPaths) {
|
|
77
|
-
const assets = [];
|
|
78
|
-
for (const p of localPaths) {
|
|
79
|
-
const asset = await uploadLocalAsset(p);
|
|
80
|
-
if (asset) assets.push(asset);
|
|
81
|
-
}
|
|
82
|
-
return assets;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function processAndSaveResult(result, opts) {
|
|
86
|
-
const { agentId: explicitAgentId, sessionKey, hostId, type, replyToMessageId, agent, sessionId, turnId, runtimeVersion } = opts;
|
|
87
|
-
const agentId = explicitAgentId || sessionKey;
|
|
88
|
-
const startedAt = Date.now();
|
|
89
|
-
|
|
90
|
-
// Collect media: from agent mediaUrls + parsed from text
|
|
91
|
-
const allLocalPaths = [...new Set([
|
|
92
|
-
...(result.mediaUrls || []),
|
|
93
|
-
...extractMediaPaths(result.text || ''),
|
|
94
|
-
])];
|
|
95
|
-
|
|
96
|
-
debugLog('relay', 'process-result.begin', {
|
|
97
|
-
agentId,
|
|
98
|
-
type,
|
|
99
|
-
parentMessageId: replyToMessageId || null,
|
|
100
|
-
textLength: result.text?.length || 0,
|
|
101
|
-
localMediaCount: allLocalPaths.length,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// Upload local media to Ticlawk private chat assets.
|
|
105
|
-
const uploadedAssets = await uploadMediaAssets(allLocalPaths);
|
|
106
|
-
const uploadedAssetIds = uploadedAssets
|
|
107
|
-
.map((asset) => asset?.asset_id)
|
|
108
|
-
.filter(Boolean);
|
|
109
|
-
|
|
110
|
-
const updateId = randomUUID();
|
|
111
|
-
debugLog('relay', 'post-final.begin', {
|
|
112
|
-
agentId,
|
|
113
|
-
updateId,
|
|
114
|
-
durationMs: Date.now() - startedAt,
|
|
115
|
-
uploadedMediaCount: uploadedAssetIds.length,
|
|
116
|
-
});
|
|
117
|
-
try {
|
|
118
|
-
await api.postRuntimeResult({
|
|
119
|
-
agent,
|
|
120
|
-
agent_id: agentId,
|
|
121
|
-
runtime_host_id: hostId,
|
|
122
|
-
session_id: sessionId || null,
|
|
123
|
-
cwd: '',
|
|
124
|
-
runtime_version: runtimeVersion ?? null,
|
|
125
|
-
result_id: updateId,
|
|
126
|
-
turn_id: turnId || replyToMessageId || null,
|
|
127
|
-
reply_to_message_id: replyToMessageId || null,
|
|
128
|
-
origin_ts: new Date().toISOString(),
|
|
129
|
-
text: result.text || '',
|
|
130
|
-
media_asset_ids: uploadedAssetIds,
|
|
131
|
-
output_type: type || 'agent_message',
|
|
132
|
-
});
|
|
133
|
-
} catch (err) {
|
|
134
|
-
debugError('relay', 'post-final.failed', {
|
|
135
|
-
agentId,
|
|
136
|
-
updateId,
|
|
137
|
-
durationMs: Date.now() - startedAt,
|
|
138
|
-
error: err.message,
|
|
139
|
-
});
|
|
140
|
-
throw err;
|
|
141
|
-
}
|
|
142
|
-
debugLog('relay', 'process-result.ok', {
|
|
143
|
-
agentId,
|
|
144
|
-
updateId,
|
|
145
|
-
durationMs: Date.now() - startedAt,
|
|
146
|
-
uploadedMediaCount: uploadedAssetIds.length,
|
|
147
|
-
});
|
|
148
|
-
return { id: updateId, agentId };
|
|
149
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { extname } from 'node:path';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
-
|
|
5
|
-
export const MIME_MAP = {
|
|
6
|
-
'.png': 'image/png',
|
|
7
|
-
'.jpg': 'image/jpeg',
|
|
8
|
-
'.jpeg': 'image/jpeg',
|
|
9
|
-
'.gif': 'image/gif',
|
|
10
|
-
'.webp': 'image/webp',
|
|
11
|
-
'.svg': 'image/svg+xml',
|
|
12
|
-
'.mp4': 'video/mp4',
|
|
13
|
-
'.webm': 'video/webm',
|
|
14
|
-
'.pdf': 'application/pdf',
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
function mediaExtensionPattern() {
|
|
18
|
-
return Object.keys(MIME_MAP).map((entry) => entry.replace('.', '\\.')).join('|');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function stripUrlSuffix(value) {
|
|
22
|
-
return String(value || '').replace(/[?#].*$/, '');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function trimCandidate(value) {
|
|
26
|
-
return String(value || '')
|
|
27
|
-
.trim()
|
|
28
|
-
.replace(/^<|>$/g, '')
|
|
29
|
-
.replace(/^["']|["']$/g, '')
|
|
30
|
-
.replace(/\\\//g, '/');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function hasKnownMediaExtension(value) {
|
|
34
|
-
return Boolean(MIME_MAP[extname(stripUrlSuffix(value)).toLowerCase()]);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function normalizeMediaPath(value) {
|
|
38
|
-
let candidate = trimCandidate(value);
|
|
39
|
-
if (!candidate) return null;
|
|
40
|
-
|
|
41
|
-
candidate = candidate.replace(/^about:/i, '');
|
|
42
|
-
|
|
43
|
-
if (/^file:/i.test(candidate)) {
|
|
44
|
-
try {
|
|
45
|
-
candidate = fileURLToPath(candidate);
|
|
46
|
-
} catch {
|
|
47
|
-
candidate = candidate.replace(/^file:(?:\/\/)?/i, '');
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
candidate = stripUrlSuffix(candidate);
|
|
52
|
-
|
|
53
|
-
if (candidate.startsWith('~')) {
|
|
54
|
-
candidate = candidate.replace(/^~/, homedir());
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (candidate.startsWith('/')) {
|
|
58
|
-
candidate = candidate.replace(/^\/{2,}/, '/');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!candidate.startsWith('/')) return null;
|
|
62
|
-
if (!hasKnownMediaExtension(candidate)) return null;
|
|
63
|
-
return candidate;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function addMediaCandidate(paths, value) {
|
|
67
|
-
const normalized = normalizeMediaPath(value);
|
|
68
|
-
if (normalized) paths.add(normalized);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function extractMediaPaths(text) {
|
|
72
|
-
const extPattern = mediaExtensionPattern();
|
|
73
|
-
const paths = new Set();
|
|
74
|
-
const candidates = [];
|
|
75
|
-
|
|
76
|
-
const body = String(text || '');
|
|
77
|
-
for (const match of body.matchAll(/<img[^>]+src=["']([^"']+)["'][^>]*>/gi)) {
|
|
78
|
-
candidates.push({ index: match.index || 0, value: match[1] });
|
|
79
|
-
}
|
|
80
|
-
for (const match of body.matchAll(/!?\[[^\]]*\]\(\s*<([^>\n]+)>/gi)) {
|
|
81
|
-
candidates.push({ index: match.index || 0, value: match[1] });
|
|
82
|
-
}
|
|
83
|
-
for (const match of body.matchAll(/!?\[[^\]]*\]\(\s*([^\s)>]+)>?/gi)) {
|
|
84
|
-
candidates.push({ index: match.index || 0, value: match[1] });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const re = new RegExp(`((?:file:(?://)?|about:(?://)?)?(?:~|/{1,3})[^\\s"'<>)]*?(?:${extPattern})(?:[?#][^\\s"'<>)]*)?)`, 'gi');
|
|
88
|
-
for (const match of body.matchAll(re)) {
|
|
89
|
-
candidates.push({ index: match.index || 0, value: match[1] });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
candidates
|
|
93
|
-
.sort((a, b) => a.index - b.index)
|
|
94
|
-
.forEach((candidate) => addMediaCandidate(paths, candidate.value));
|
|
95
|
-
|
|
96
|
-
return [...paths];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function escapeRegExp(value) {
|
|
100
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function referencesPath(value, normalizedPaths) {
|
|
104
|
-
const normalized = normalizeMediaPath(value);
|
|
105
|
-
return Boolean(normalized && normalizedPaths.has(normalized));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function pathReferenceVariants(path) {
|
|
109
|
-
const normalized = normalizeMediaPath(path) || path;
|
|
110
|
-
const variants = new Set([normalized]);
|
|
111
|
-
if (normalized.startsWith('/')) {
|
|
112
|
-
variants.add(pathToFileURL(normalized).href);
|
|
113
|
-
variants.add(`about:${normalized}`);
|
|
114
|
-
variants.add(`about://${normalized}`);
|
|
115
|
-
}
|
|
116
|
-
return [...variants].sort((a, b) => b.length - a.length);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function stripMediaPathReferences(text, explicitPaths = []) {
|
|
120
|
-
let next = String(text || '');
|
|
121
|
-
const paths = [...new Set([
|
|
122
|
-
...explicitPaths,
|
|
123
|
-
...extractMediaPaths(next),
|
|
124
|
-
].map((path) => normalizeMediaPath(path)).filter(Boolean))];
|
|
125
|
-
const normalizedPaths = new Set(paths);
|
|
126
|
-
|
|
127
|
-
next = next
|
|
128
|
-
.replace(/<img[^>]+src=["']([^"']+)["'][^>]*>/gi, (match, src) => (
|
|
129
|
-
referencesPath(src, normalizedPaths) ? '' : match
|
|
130
|
-
))
|
|
131
|
-
.replace(/!\[[^\]]*\]\(\s*<([^>\n]+)>(?:\s+["'][^"']*["'])?\s*\)/gi, (match, target) => (
|
|
132
|
-
referencesPath(target, normalizedPaths) ? '' : match
|
|
133
|
-
))
|
|
134
|
-
.replace(/!\[[^\]]*\]\(\s*([^\s)>]+)>?(?:\s+["'][^"']*["'])?\s*\)/gi, (match, target) => (
|
|
135
|
-
referencesPath(target, normalizedPaths) ? '' : match
|
|
136
|
-
));
|
|
137
|
-
|
|
138
|
-
for (const path of paths) {
|
|
139
|
-
for (const variant of pathReferenceVariants(path)) {
|
|
140
|
-
const escapedPath = escapeRegExp(variant);
|
|
141
|
-
next = next
|
|
142
|
-
.replace(new RegExp(`\\[media attached[^\\]]*${escapedPath}[^\\]]*\\]\\s*`, 'gi'), '')
|
|
143
|
-
.replace(new RegExp(`^\\s*${escapedPath}\\s*$`, 'gim'), '')
|
|
144
|
-
.replace(new RegExp(escapedPath, 'g'), '');
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return next
|
|
149
|
-
.replace(/<img[^>]+src=["'](?:file:|about:)(?:\/){0,3}(?:tmp|private|var|Users|home)[^"']*["'][^>]*>/gi, '')
|
|
150
|
-
.replace(/!\[[^\]]*\]\(\s*<?(?:file:|about:)(?:\/){0,3}(?:tmp|private|var|Users|home)[^)]*\)/gi, '')
|
|
151
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
152
|
-
.trim();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function normalizeOutboundMedia(result) {
|
|
156
|
-
const explicit = Array.isArray(result?.mediaUrls) ? result.mediaUrls : [];
|
|
157
|
-
const inferred = extractMediaPaths(result?.text || '');
|
|
158
|
-
return [...new Set([...explicit, ...inferred])].map((path) => ({
|
|
159
|
-
kind: 'local_path',
|
|
160
|
-
value: path,
|
|
161
|
-
mime: MIME_MAP[extname(path).toLowerCase()] || 'application/octet-stream',
|
|
162
|
-
}));
|
|
163
|
-
}
|