@rethinkingstudio/clawpilot 2.1.7-internal.1 → 2.1.7-internal.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ccconnect/api-client.d.ts +13 -0
- package/dist/ccconnect/api-client.js +3 -0
- package/dist/ccconnect/api-client.js.map +1 -1
- package/dist/ccconnect/project-config.d.ts +25 -0
- package/dist/ccconnect/project-config.js +309 -0
- package/dist/ccconnect/project-config.js.map +1 -0
- package/dist/media/assistant-attachments.d.ts +2 -0
- package/dist/media/assistant-attachments.js +18 -18
- package/dist/media/assistant-attachments.js.map +1 -1
- package/dist/relay/ccconnect-relay-manager.js +522 -15
- package/dist/relay/ccconnect-relay-manager.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,8 @@ import { handleLocalCommand } from "../commands/local-handlers.js";
|
|
|
4
4
|
import { handleProviderCommand } from "../commands/provider-handlers.js";
|
|
5
5
|
import { getServicePlatform } from "../platform/service-manager.js";
|
|
6
6
|
import { CCConnectManagementClient, decodeCCConnectSessionKey, encodeCCConnectSessionKey, timestampMs, } from "../ccconnect/api-client.js";
|
|
7
|
+
import { createCCConnectProject, discoverCCConnectProjects, restartCCConnect } from "../ccconnect/project-config.js";
|
|
8
|
+
import { uploadMediaBlocks } from "../media/assistant-attachments.js";
|
|
7
9
|
const DEFAULT_CONTEXT_TOKENS = 200_000;
|
|
8
10
|
export async function runCCConnectRelayManager(opts) {
|
|
9
11
|
const wsUrl = buildRelayUrl(opts.relayServerUrl, opts.gatewayId, opts.relaySecret);
|
|
@@ -19,6 +21,7 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
19
21
|
let bridgePingTimer = null;
|
|
20
22
|
let bridgeReconnectAttempts = 0;
|
|
21
23
|
const rawToEncoded = new Map();
|
|
24
|
+
const rawToProject = new Map();
|
|
22
25
|
const activeRunByEncoded = new Map();
|
|
23
26
|
try {
|
|
24
27
|
relayWs = new WebSocket(wsUrl);
|
|
@@ -106,7 +109,19 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
106
109
|
ws.send(JSON.stringify({
|
|
107
110
|
type: "register",
|
|
108
111
|
platform: "pocketclaw",
|
|
109
|
-
capabilities: [
|
|
112
|
+
capabilities: [
|
|
113
|
+
"text",
|
|
114
|
+
"typing",
|
|
115
|
+
"preview",
|
|
116
|
+
"update_message",
|
|
117
|
+
"delete_message",
|
|
118
|
+
"reconstruct_reply",
|
|
119
|
+
"buttons",
|
|
120
|
+
"card",
|
|
121
|
+
"image",
|
|
122
|
+
"file",
|
|
123
|
+
"audio",
|
|
124
|
+
],
|
|
110
125
|
metadata: { version: "1.0.0", description: "PocketClaw relay adapter" },
|
|
111
126
|
}));
|
|
112
127
|
});
|
|
@@ -142,6 +157,9 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
142
157
|
});
|
|
143
158
|
}
|
|
144
159
|
function handleBridgeMessage(msg) {
|
|
160
|
+
void handleBridgeMessageAsync(msg);
|
|
161
|
+
}
|
|
162
|
+
async function handleBridgeMessageAsync(msg) {
|
|
145
163
|
const record = msg;
|
|
146
164
|
if (msg.type === "register_ack") {
|
|
147
165
|
bridgeReady = Boolean(msg.ok);
|
|
@@ -157,8 +175,10 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
157
175
|
}
|
|
158
176
|
return;
|
|
159
177
|
}
|
|
178
|
+
if (msg.type === "pong")
|
|
179
|
+
return;
|
|
160
180
|
const rawSessionKey = stringValue(record.session_key) ?? "";
|
|
161
|
-
const encodedSessionKey =
|
|
181
|
+
const encodedSessionKey = await resolveEncodedBridgeSession(record);
|
|
162
182
|
if (!encodedSessionKey)
|
|
163
183
|
return;
|
|
164
184
|
const runId = ensureRunId(encodedSessionKey);
|
|
@@ -185,27 +205,69 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
185
205
|
emitChat(runId, encodedSessionKey, "delta", stringValue(record.content) ?? "", false);
|
|
186
206
|
return;
|
|
187
207
|
}
|
|
208
|
+
if (msg.type === "delete_message") {
|
|
209
|
+
send({
|
|
210
|
+
type: "event",
|
|
211
|
+
event: "message.delete",
|
|
212
|
+
payload: {
|
|
213
|
+
runId,
|
|
214
|
+
sessionKey: encodedSessionKey,
|
|
215
|
+
previewHandle: stringValue(record.preview_handle),
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
activeRunByEncoded.delete(encodedSessionKey);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
188
221
|
if (msg.type === "reply_stream") {
|
|
189
222
|
const text = stringValue(record.full_text) ?? stringValue(record.delta) ?? "";
|
|
190
223
|
const done = Boolean(record.done);
|
|
191
|
-
|
|
192
|
-
|
|
224
|
+
const state = done && !isIntermediateReply(text) ? "final" : "delta";
|
|
225
|
+
emitChat(runId, encodedSessionKey, state, text, false);
|
|
226
|
+
if (state === "final")
|
|
193
227
|
activeRunByEncoded.delete(encodedSessionKey);
|
|
194
228
|
return;
|
|
195
229
|
}
|
|
196
230
|
if (msg.type === "reply") {
|
|
197
|
-
|
|
198
|
-
|
|
231
|
+
const content = stringValue(record.content) ?? "";
|
|
232
|
+
const permissionFallback = permissionHintActions(content, rawSessionKey, stringValue(record.reply_ctx) ?? rawSessionKey);
|
|
233
|
+
const state = isIntermediateReply(content) ? "delta" : "final";
|
|
234
|
+
if (permissionFallback) {
|
|
235
|
+
emitStructuredChat(runId, encodedSessionKey, state, {
|
|
236
|
+
text: content,
|
|
237
|
+
blocks: [textBlock(content), permissionFallback].filter(Boolean),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
emitChat(runId, encodedSessionKey, state, content, false);
|
|
242
|
+
}
|
|
243
|
+
if (state === "final")
|
|
244
|
+
activeRunByEncoded.delete(encodedSessionKey);
|
|
199
245
|
return;
|
|
200
246
|
}
|
|
201
247
|
if (msg.type === "buttons") {
|
|
202
|
-
|
|
203
|
-
|
|
248
|
+
const actions = normalizeBridgeButtons(record.buttons);
|
|
249
|
+
console.log(`[runtime:ccconnect] bridge buttons session=${rawSessionKey} actions=${actions.length}`);
|
|
250
|
+
emitStructuredChat(runId, encodedSessionKey, "delta", {
|
|
251
|
+
text: stringValue(record.content) ?? "",
|
|
252
|
+
blocks: [
|
|
253
|
+
textBlock(stringValue(record.content) ?? ""),
|
|
254
|
+
actionsBlock(actions, rawSessionKey, stringValue(record.reply_ctx) ?? rawSessionKey),
|
|
255
|
+
].filter(Boolean),
|
|
256
|
+
});
|
|
204
257
|
return;
|
|
205
258
|
}
|
|
206
259
|
if (msg.type === "card") {
|
|
207
|
-
|
|
208
|
-
|
|
260
|
+
console.log(`[runtime:ccconnect] bridge card session=${rawSessionKey}`);
|
|
261
|
+
emitStructuredChat(runId, encodedSessionKey, "delta", normalizeBridgeCard(record.card, rawSessionKey, stringValue(record.reply_ctx) ?? rawSessionKey));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (msg.type === "image" || msg.type === "file" || msg.type === "audio") {
|
|
265
|
+
const attachment = await uploadBridgeMedia(msg);
|
|
266
|
+
emitStructuredChat(runId, encodedSessionKey, "final", {
|
|
267
|
+
text: attachment.text,
|
|
268
|
+
attachments: attachment.attachments,
|
|
269
|
+
blocks: attachment.text ? [textBlock(attachment.text)] : [],
|
|
270
|
+
});
|
|
209
271
|
return;
|
|
210
272
|
}
|
|
211
273
|
if (msg.type === "error") {
|
|
@@ -213,6 +275,37 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
213
275
|
activeRunByEncoded.delete(encodedSessionKey);
|
|
214
276
|
}
|
|
215
277
|
}
|
|
278
|
+
async function resolveEncodedBridgeSession(record) {
|
|
279
|
+
const rawSessionKey = stringValue(record.session_key);
|
|
280
|
+
if (!rawSessionKey) {
|
|
281
|
+
if (activeRunByEncoded.size === 1) {
|
|
282
|
+
return activeRunByEncoded.keys().next().value;
|
|
283
|
+
}
|
|
284
|
+
console.warn(`[runtime:ccconnect] dropped bridge ${String(record.type)} without session_key`);
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
const existing = rawToEncoded.get(rawSessionKey);
|
|
288
|
+
if (existing)
|
|
289
|
+
return existing;
|
|
290
|
+
const explicitProject = stringValue(record.project) ?? rawToProject.get(rawSessionKey);
|
|
291
|
+
if (explicitProject) {
|
|
292
|
+
const encoded = encodeCCConnectSessionKey(explicitProject, rawSessionKey, rawSessionKey);
|
|
293
|
+
rawToEncoded.set(rawSessionKey, encoded);
|
|
294
|
+
rawToProject.set(rawSessionKey, explicitProject);
|
|
295
|
+
console.warn(`[runtime:ccconnect] recovered bridge session mapping project=${explicitProject} rawSessionKey=${rawSessionKey}`);
|
|
296
|
+
return encoded;
|
|
297
|
+
}
|
|
298
|
+
if (activeRunByEncoded.size === 1) {
|
|
299
|
+
const encoded = activeRunByEncoded.keys().next().value;
|
|
300
|
+
if (encoded) {
|
|
301
|
+
rawToEncoded.set(rawSessionKey, encoded);
|
|
302
|
+
console.warn(`[runtime:ccconnect] inferred bridge session mapping rawSessionKey=${rawSessionKey}`);
|
|
303
|
+
return encoded;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
console.warn(`[runtime:ccconnect] dropped bridge ${String(record.type)} for unmapped session_key=${rawSessionKey}`);
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
216
309
|
function ensureRunId(encodedSessionKey, preferred) {
|
|
217
310
|
const existing = activeRunByEncoded.get(encodedSessionKey);
|
|
218
311
|
if (existing)
|
|
@@ -232,6 +325,37 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
232
325
|
payload.errorMessage = text ?? "cc-connect error";
|
|
233
326
|
send({ type: "event", event: "chat", payload });
|
|
234
327
|
}
|
|
328
|
+
function emitStructuredChat(runId, sessionKey, state, data) {
|
|
329
|
+
const blocks = data.blocks?.length ? data.blocks : data.text !== undefined ? [textBlock(data.text)] : undefined;
|
|
330
|
+
const payload = { runId, sessionKey, state };
|
|
331
|
+
if (blocks)
|
|
332
|
+
payload.message = { content: blocks };
|
|
333
|
+
if (data.attachments)
|
|
334
|
+
payload.attachments = data.attachments;
|
|
335
|
+
if (state === "error")
|
|
336
|
+
payload.errorMessage = data.text ?? "cc-connect error";
|
|
337
|
+
send({ type: "event", event: "chat", payload });
|
|
338
|
+
}
|
|
339
|
+
async function uploadBridgeMedia(msg) {
|
|
340
|
+
const data = stringValue(msg.data);
|
|
341
|
+
const fileName = stringValue(msg.file_name);
|
|
342
|
+
const mimeType = bridgeMediaMimeType(msg);
|
|
343
|
+
const label = fileName || (msg.type === "audio" ? "Audio message" : msg.type === "image" ? "Image" : "File");
|
|
344
|
+
if (!data) {
|
|
345
|
+
return { text: `[${label} missing data]`, attachments: [] };
|
|
346
|
+
}
|
|
347
|
+
const block = {
|
|
348
|
+
mimeType,
|
|
349
|
+
fileName,
|
|
350
|
+
data: Buffer.from(data.replace(/^data:[^;]+;base64,/, ""), "base64"),
|
|
351
|
+
};
|
|
352
|
+
const uploaded = await uploadMediaBlocks([block], opts.relayServerUrl, opts.gatewayId, opts.relaySecret);
|
|
353
|
+
const attachments = uploaded.map((att) => ({
|
|
354
|
+
...att,
|
|
355
|
+
fileName: fileName ?? undefined,
|
|
356
|
+
}));
|
|
357
|
+
return { text: label, attachments };
|
|
358
|
+
}
|
|
235
359
|
async function respondSessionsList(requestId) {
|
|
236
360
|
const projects = await client.listProjects();
|
|
237
361
|
const sessions = [];
|
|
@@ -248,18 +372,80 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
248
372
|
count: sessions.length,
|
|
249
373
|
});
|
|
250
374
|
}
|
|
375
|
+
async function respondProjectsList(requestId) {
|
|
376
|
+
const projects = await client.listProjects();
|
|
377
|
+
const detailed = await Promise.all(projects.map(async (project) => {
|
|
378
|
+
try {
|
|
379
|
+
const detail = await client.getProject(project.name);
|
|
380
|
+
return {
|
|
381
|
+
name: detail.name,
|
|
382
|
+
agentType: detail.agent_type,
|
|
383
|
+
workDir: detail.work_dir,
|
|
384
|
+
mode: detail.agent_mode || detail.mode,
|
|
385
|
+
sessionsCount: detail.sessions_count ?? project.sessions_count ?? 0,
|
|
386
|
+
platforms: detail.platforms ?? project.platforms ?? [],
|
|
387
|
+
heartbeatEnabled: detail.heartbeat_enabled ?? project.heartbeat_enabled ?? false,
|
|
388
|
+
activeSessionKeys: detail.active_session_keys ?? [],
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return {
|
|
393
|
+
name: project.name,
|
|
394
|
+
agentType: project.agent_type,
|
|
395
|
+
sessionsCount: project.sessions_count ?? 0,
|
|
396
|
+
platforms: project.platforms ?? [],
|
|
397
|
+
heartbeatEnabled: project.heartbeat_enabled ?? false,
|
|
398
|
+
activeSessionKeys: [],
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}));
|
|
402
|
+
sendResponse(requestId, { projects: detailed, count: detailed.length });
|
|
403
|
+
}
|
|
404
|
+
async function respondProjectsDiscover(requestId) {
|
|
405
|
+
const projects = await client.listProjects();
|
|
406
|
+
const detailed = await Promise.all(projects.map(async (project) => {
|
|
407
|
+
try {
|
|
408
|
+
const detail = await client.getProject(project.name);
|
|
409
|
+
return { name: detail.name, workDir: detail.work_dir };
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return { name: project.name, workDir: undefined };
|
|
413
|
+
}
|
|
414
|
+
}));
|
|
415
|
+
const directories = discoverCCConnectProjects(detailed);
|
|
416
|
+
sendResponse(requestId, { directories, count: directories.length });
|
|
417
|
+
}
|
|
418
|
+
async function respondProjectCreate(requestId, params) {
|
|
419
|
+
const p = params;
|
|
420
|
+
const created = createCCConnectProject(p);
|
|
421
|
+
restartCCConnect(created.configPath);
|
|
422
|
+
sendResponse(requestId, {
|
|
423
|
+
project: {
|
|
424
|
+
name: created.name,
|
|
425
|
+
agentType: created.agentType,
|
|
426
|
+
workDir: created.workDir,
|
|
427
|
+
mode: created.mode,
|
|
428
|
+
sessionsCount: 0,
|
|
429
|
+
platforms: ["line", "bridge"],
|
|
430
|
+
heartbeatEnabled: false,
|
|
431
|
+
activeSessionKeys: [],
|
|
432
|
+
},
|
|
433
|
+
restarted: true,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
251
436
|
async function respondMessagesHistory(requestId, params) {
|
|
252
437
|
const p = params;
|
|
253
438
|
if (!p.sessionKey)
|
|
254
439
|
throw new Error("sessionKey required");
|
|
255
440
|
const decoded = decodeCCConnectSessionKey(p.sessionKey);
|
|
256
|
-
const
|
|
441
|
+
const sessionId = await resolveCCConnectSessionId(decoded.project, decoded.sessionKey, decoded.sessionId);
|
|
442
|
+
const detail = await client.getSession(decoded.project, sessionId, Math.min(p.limit ?? 50, 200));
|
|
257
443
|
const history = detail.history ?? [];
|
|
258
444
|
const messages = history.map((entry, index) => ({
|
|
259
445
|
id: index + 1,
|
|
260
446
|
role: entry.role === "user" ? "user" : "assistant",
|
|
261
447
|
content: entry.content ?? "",
|
|
262
|
-
runId: entry.role === "user" ? undefined : `${
|
|
448
|
+
runId: entry.role === "user" ? undefined : `${sessionId}-${index + 1}`,
|
|
263
449
|
state: "final",
|
|
264
450
|
createdAt: timestampMs(entry.timestamp),
|
|
265
451
|
updatedAt: timestampMs(entry.timestamp),
|
|
@@ -292,11 +478,14 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
292
478
|
throw new Error("sessionKey required");
|
|
293
479
|
if (!p.message)
|
|
294
480
|
throw new Error("message required");
|
|
295
|
-
const decoded =
|
|
481
|
+
const decoded = await resolveCCConnectSendTarget(p.sessionKey);
|
|
296
482
|
rawToEncoded.set(decoded.sessionKey, p.sessionKey);
|
|
483
|
+
rawToProject.set(decoded.sessionKey, decoded.project);
|
|
484
|
+
activeRunByEncoded.delete(p.sessionKey);
|
|
297
485
|
const runId = ensureRunId(p.sessionKey, p.idempotencyKey);
|
|
298
486
|
emitChat(runId, p.sessionKey, "delta", undefined, true);
|
|
299
487
|
if (bridgeReady && bridgeWs?.readyState === WebSocket.OPEN) {
|
|
488
|
+
console.log(`[runtime:ccconnect] chat.send via bridge project=${decoded.project} rawSessionKey=${decoded.sessionKey}`);
|
|
300
489
|
bridgeWs.send(JSON.stringify({
|
|
301
490
|
type: "message",
|
|
302
491
|
msg_id: `pocketclaw-${Date.now()}`,
|
|
@@ -306,17 +495,68 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
306
495
|
content: p.message,
|
|
307
496
|
reply_ctx: decoded.sessionKey,
|
|
308
497
|
project: decoded.project,
|
|
498
|
+
...bridgeAttachments(params),
|
|
309
499
|
}));
|
|
310
500
|
return;
|
|
311
501
|
}
|
|
312
502
|
scheduleBridgeReconnect("bridge not ready during chat.send");
|
|
503
|
+
console.log(`[runtime:ccconnect] chat.send via management fallback project=${decoded.project} rawSessionKey=${decoded.sessionKey}`);
|
|
313
504
|
await client.send(decoded.project, decoded.sessionKey, p.message);
|
|
314
505
|
}
|
|
506
|
+
async function handleChatAction(params) {
|
|
507
|
+
const p = params;
|
|
508
|
+
if (!p.sessionKey)
|
|
509
|
+
throw new Error("sessionKey required");
|
|
510
|
+
if (!p.action)
|
|
511
|
+
throw new Error("action required");
|
|
512
|
+
if (!bridgeReady || bridgeWs?.readyState !== WebSocket.OPEN) {
|
|
513
|
+
throw new Error("cc-connect bridge not ready");
|
|
514
|
+
}
|
|
515
|
+
const decoded = await resolveCCConnectSendTarget(p.sessionKey);
|
|
516
|
+
bridgeWs.send(JSON.stringify({
|
|
517
|
+
type: "card_action",
|
|
518
|
+
session_key: decoded.sessionKey,
|
|
519
|
+
action: p.action,
|
|
520
|
+
reply_ctx: p.replyCtx || decoded.sessionKey,
|
|
521
|
+
project: decoded.project,
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
async function resolveCCConnectSendTarget(sessionKey) {
|
|
525
|
+
try {
|
|
526
|
+
return decodeCCConnectSessionKey(sessionKey);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
const projects = await client.listProjects();
|
|
530
|
+
for (const project of projects) {
|
|
531
|
+
const sessions = await client.listSessions(project.name);
|
|
532
|
+
const preferred = sessions.find((session) => session.active || session.live) ?? sessions[0];
|
|
533
|
+
if (preferred?.session_key) {
|
|
534
|
+
console.warn(`[runtime:ccconnect] non-ccconnect sessionKey received (${sessionKey}); using ${project.name}/${preferred.session_key}`);
|
|
535
|
+
return {
|
|
536
|
+
project: project.name,
|
|
537
|
+
sessionKey: preferred.session_key,
|
|
538
|
+
sessionId: preferred.id || preferred.session_key,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
throw new Error(`invalid cc-connect session key: ${sessionKey}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
315
545
|
function rememberSession(project, session) {
|
|
316
|
-
const encoded = encodeCCConnectSessionKey(project, session.session_key, session.
|
|
317
|
-
rawToEncoded.
|
|
546
|
+
const encoded = encodeCCConnectSessionKey(project, session.session_key, session.session_key);
|
|
547
|
+
if (!rawToEncoded.has(session.session_key)) {
|
|
548
|
+
rawToEncoded.set(session.session_key, encoded);
|
|
549
|
+
rawToProject.set(session.session_key, project);
|
|
550
|
+
}
|
|
318
551
|
return encoded;
|
|
319
552
|
}
|
|
553
|
+
async function resolveCCConnectSessionId(project, sessionKey, sessionId) {
|
|
554
|
+
if (sessionId && sessionId !== sessionKey)
|
|
555
|
+
return sessionId;
|
|
556
|
+
const sessions = await client.listSessions(project);
|
|
557
|
+
const matched = sessions.find((session) => session.session_key === sessionKey);
|
|
558
|
+
return matched?.id || sessionId || sessionKey;
|
|
559
|
+
}
|
|
320
560
|
function toRuntimeSession(project, session, encoded) {
|
|
321
561
|
const platform = session.platform || session.session_key.split(":")[0] || "cc-connect";
|
|
322
562
|
const title = session.name || session.chat_name || session.user_name || session.session_key || session.id;
|
|
@@ -383,6 +623,18 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
383
623
|
}
|
|
384
624
|
try {
|
|
385
625
|
switch (msg.method) {
|
|
626
|
+
case "projects.list":
|
|
627
|
+
if (requestId)
|
|
628
|
+
await respondProjectsList(requestId);
|
|
629
|
+
return;
|
|
630
|
+
case "projects.discover":
|
|
631
|
+
if (requestId)
|
|
632
|
+
await respondProjectsDiscover(requestId);
|
|
633
|
+
return;
|
|
634
|
+
case "projects.create":
|
|
635
|
+
if (requestId)
|
|
636
|
+
await respondProjectCreate(requestId, msg.params ?? {});
|
|
637
|
+
return;
|
|
386
638
|
case "sessions.list":
|
|
387
639
|
if (requestId)
|
|
388
640
|
await respondSessionsList(requestId);
|
|
@@ -400,6 +652,11 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
400
652
|
if (requestId)
|
|
401
653
|
sendResponse(requestId, { ok: true });
|
|
402
654
|
return;
|
|
655
|
+
case "chat.action":
|
|
656
|
+
await handleChatAction(msg.params ?? {});
|
|
657
|
+
if (requestId)
|
|
658
|
+
sendResponse(requestId, { ok: true });
|
|
659
|
+
return;
|
|
403
660
|
case "chat.abort":
|
|
404
661
|
if (requestId)
|
|
405
662
|
sendResponse(requestId, { ok: true });
|
|
@@ -445,4 +702,254 @@ function buildRelayUrl(base, gatewayId, relaySecret) {
|
|
|
445
702
|
function stringValue(value) {
|
|
446
703
|
return typeof value === "string" ? value : undefined;
|
|
447
704
|
}
|
|
705
|
+
function textBlock(text) {
|
|
706
|
+
return text ? { type: "text", text } : undefined;
|
|
707
|
+
}
|
|
708
|
+
function actionsBlock(buttons, sessionKey, replyCtx) {
|
|
709
|
+
if (buttons.length === 0)
|
|
710
|
+
return undefined;
|
|
711
|
+
return {
|
|
712
|
+
type: "actions",
|
|
713
|
+
actions: buttons,
|
|
714
|
+
sessionKey,
|
|
715
|
+
replyCtx,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function normalizeBridgeButtons(buttons) {
|
|
719
|
+
if (!Array.isArray(buttons))
|
|
720
|
+
return [];
|
|
721
|
+
const actions = [];
|
|
722
|
+
const rows = buttons.some(Array.isArray) ? buttons : [buttons];
|
|
723
|
+
for (const row of rows) {
|
|
724
|
+
const buttonRow = Array.isArray(row) ? row : [row];
|
|
725
|
+
for (const button of buttonRow) {
|
|
726
|
+
const action = normalizeBridgeAction(button);
|
|
727
|
+
if (action)
|
|
728
|
+
actions.push(action);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return actions;
|
|
732
|
+
}
|
|
733
|
+
function normalizeBridgeCard(card, sessionKey, replyCtx) {
|
|
734
|
+
if (!isRecord(card)) {
|
|
735
|
+
return { text: "[card message]", blocks: [textBlock("[card message]")].filter(Boolean) };
|
|
736
|
+
}
|
|
737
|
+
const blocks = [];
|
|
738
|
+
const textParts = [];
|
|
739
|
+
const header = isRecord(card.header) ? stringValue(card.header.title) : undefined;
|
|
740
|
+
if (header) {
|
|
741
|
+
blocks.push({ type: "text", text: `**${header}**` });
|
|
742
|
+
textParts.push(header);
|
|
743
|
+
}
|
|
744
|
+
const elements = Array.isArray(card.elements) ? card.elements : [];
|
|
745
|
+
for (const element of elements) {
|
|
746
|
+
if (!isRecord(element))
|
|
747
|
+
continue;
|
|
748
|
+
const type = stringValue(element.type);
|
|
749
|
+
if (type === "markdown") {
|
|
750
|
+
const text = stringValue(element.content) ?? "";
|
|
751
|
+
if (text) {
|
|
752
|
+
blocks.push({ type: "markdown", text });
|
|
753
|
+
textParts.push(text);
|
|
754
|
+
}
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (type === "note") {
|
|
758
|
+
const text = stringValue(element.text) ?? "";
|
|
759
|
+
if (text) {
|
|
760
|
+
blocks.push({ type: "text", text });
|
|
761
|
+
textParts.push(text);
|
|
762
|
+
}
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (type === "divider") {
|
|
766
|
+
blocks.push({ type: "divider" });
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (type === "actions") {
|
|
770
|
+
const actions = normalizeCardActionButtons(element.buttons);
|
|
771
|
+
const block = actionsBlock(actions, sessionKey, replyCtx);
|
|
772
|
+
if (block)
|
|
773
|
+
blocks.push(block);
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (type === "list_item") {
|
|
777
|
+
const text = stringValue(element.text) ?? "";
|
|
778
|
+
if (text)
|
|
779
|
+
textParts.push(text);
|
|
780
|
+
const btnText = stringValue(element.btn_text);
|
|
781
|
+
const btnValue = stringValue(element.btn_value);
|
|
782
|
+
blocks.push({
|
|
783
|
+
type: "list_item",
|
|
784
|
+
text,
|
|
785
|
+
action: btnText && btnValue ? {
|
|
786
|
+
id: btnValue,
|
|
787
|
+
label: btnText,
|
|
788
|
+
value: btnValue,
|
|
789
|
+
style: actionStyle(btnValue, stringValue(element.btn_type)),
|
|
790
|
+
} : undefined,
|
|
791
|
+
sessionKey,
|
|
792
|
+
replyCtx,
|
|
793
|
+
});
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (type === "select") {
|
|
797
|
+
const options = Array.isArray(element.options) ? element.options.filter(isRecord).map((option) => ({
|
|
798
|
+
id: stringValue(option.value) ?? "",
|
|
799
|
+
label: stringValue(option.text) ?? "",
|
|
800
|
+
value: stringValue(option.value) ?? "",
|
|
801
|
+
style: "default",
|
|
802
|
+
})).filter((option) => option.label && option.value) : [];
|
|
803
|
+
blocks.push({
|
|
804
|
+
type: "select",
|
|
805
|
+
placeholder: stringValue(element.placeholder) ?? "Select",
|
|
806
|
+
initValue: stringValue(element.init_value),
|
|
807
|
+
actions: options,
|
|
808
|
+
sessionKey,
|
|
809
|
+
replyCtx,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return { text: textParts.join("\n\n"), blocks };
|
|
814
|
+
}
|
|
815
|
+
function normalizeCardActionButtons(buttons) {
|
|
816
|
+
if (!Array.isArray(buttons))
|
|
817
|
+
return [];
|
|
818
|
+
const actions = [];
|
|
819
|
+
for (const button of buttons) {
|
|
820
|
+
const action = normalizeBridgeAction(button);
|
|
821
|
+
if (action)
|
|
822
|
+
actions.push(action);
|
|
823
|
+
}
|
|
824
|
+
return actions;
|
|
825
|
+
}
|
|
826
|
+
function normalizeBridgeAction(button) {
|
|
827
|
+
if (!isRecord(button))
|
|
828
|
+
return undefined;
|
|
829
|
+
const label = stringValue(button.text)
|
|
830
|
+
?? stringValue(button.label)
|
|
831
|
+
?? stringValue(button.title);
|
|
832
|
+
const value = stringValue(button.data)
|
|
833
|
+
?? stringValue(button.value)
|
|
834
|
+
?? stringValue(button.id)
|
|
835
|
+
?? inferActionValue(label);
|
|
836
|
+
if (!label || !value)
|
|
837
|
+
return undefined;
|
|
838
|
+
return {
|
|
839
|
+
id: value,
|
|
840
|
+
label,
|
|
841
|
+
value,
|
|
842
|
+
style: actionStyle(value, stringValue(button.btn_type) ?? stringValue(button.style)),
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function permissionHintActions(content, sessionKey, replyCtx) {
|
|
846
|
+
if (!isPermissionHint(content))
|
|
847
|
+
return undefined;
|
|
848
|
+
return actionsBlock([
|
|
849
|
+
{ id: "perm:allow", label: "允许", value: "perm:allow", style: "primary" },
|
|
850
|
+
{ id: "perm:deny", label: "拒绝", value: "perm:deny", style: "danger" },
|
|
851
|
+
{ id: "perm:allow_all", label: "允许所有", value: "perm:allow_all", style: "default" },
|
|
852
|
+
], sessionKey, replyCtx);
|
|
853
|
+
}
|
|
854
|
+
function isPermissionHint(content) {
|
|
855
|
+
const normalized = content.toLowerCase();
|
|
856
|
+
return (normalized.includes("等待权限响应") || normalized.includes("permission"))
|
|
857
|
+
&& (normalized.includes("允许所有") || normalized.includes("allow all"))
|
|
858
|
+
&& (normalized.includes("拒绝") || normalized.includes("deny"));
|
|
859
|
+
}
|
|
860
|
+
function isIntermediateReply(content) {
|
|
861
|
+
const normalized = content.toLowerCase().trim();
|
|
862
|
+
if (!normalized)
|
|
863
|
+
return false;
|
|
864
|
+
if (isPermissionHint(content))
|
|
865
|
+
return true;
|
|
866
|
+
return (normalized.includes("继续执行")
|
|
867
|
+
|| normalized.includes("继续处理中")
|
|
868
|
+
|| normalized.includes("continuing")
|
|
869
|
+
|| normalized.includes("continue execution")
|
|
870
|
+
|| normalized.includes("permission granted")
|
|
871
|
+
|| normalized.includes("已允许")
|
|
872
|
+
|| normalized.includes("已开启自动批准")
|
|
873
|
+
|| normalized.includes("自动批准")
|
|
874
|
+
|| normalized.includes("权限请求将自动允许"));
|
|
875
|
+
}
|
|
876
|
+
function inferActionValue(label) {
|
|
877
|
+
if (!label)
|
|
878
|
+
return undefined;
|
|
879
|
+
const normalized = label.toLowerCase().replace(/\s+/g, "");
|
|
880
|
+
if (normalized.includes("允许所有") || normalized.includes("全部允许") || normalized.includes("allowall")) {
|
|
881
|
+
return "perm:allow_all";
|
|
882
|
+
}
|
|
883
|
+
if (normalized.includes("拒绝") || normalized.includes("deny") || normalized.includes("reject")) {
|
|
884
|
+
return "perm:deny";
|
|
885
|
+
}
|
|
886
|
+
if (normalized.includes("允许") || normalized.includes("同意") || normalized.includes("allow") || normalized.includes("approve")) {
|
|
887
|
+
return "perm:allow";
|
|
888
|
+
}
|
|
889
|
+
return undefined;
|
|
890
|
+
}
|
|
891
|
+
function actionStyle(value, explicit) {
|
|
892
|
+
if (explicit === "primary" || explicit === "danger")
|
|
893
|
+
return explicit;
|
|
894
|
+
if (value.includes("deny") || value.includes("reject") || value.includes("cancel"))
|
|
895
|
+
return "danger";
|
|
896
|
+
if (value.includes("allow"))
|
|
897
|
+
return "primary";
|
|
898
|
+
return "default";
|
|
899
|
+
}
|
|
900
|
+
function bridgeMediaMimeType(msg) {
|
|
901
|
+
const record = msg;
|
|
902
|
+
const explicit = stringValue(record.mime_type);
|
|
903
|
+
if (explicit)
|
|
904
|
+
return explicit;
|
|
905
|
+
if (msg.type === "audio") {
|
|
906
|
+
const format = stringValue(record.format) ?? "mpeg";
|
|
907
|
+
return format.includes("/") ? format : `audio/${format}`;
|
|
908
|
+
}
|
|
909
|
+
if (msg.type === "image")
|
|
910
|
+
return "image/png";
|
|
911
|
+
return "application/octet-stream";
|
|
912
|
+
}
|
|
913
|
+
function bridgeAttachments(params) {
|
|
914
|
+
const attachments = isRecord(params) && Array.isArray(params.attachments) ? params.attachments : [];
|
|
915
|
+
const images = [];
|
|
916
|
+
const files = [];
|
|
917
|
+
let audio;
|
|
918
|
+
for (const attachment of attachments) {
|
|
919
|
+
if (!isRecord(attachment))
|
|
920
|
+
continue;
|
|
921
|
+
const content = stringValue(attachment.content);
|
|
922
|
+
const mimeType = stringValue(attachment.mimeType) ?? "application/octet-stream";
|
|
923
|
+
const fileName = stringValue(attachment.fileName);
|
|
924
|
+
if (!content)
|
|
925
|
+
continue;
|
|
926
|
+
const payload = {
|
|
927
|
+
mime_type: mimeType,
|
|
928
|
+
data: content.replace(/^data:[^;]+;base64,/, ""),
|
|
929
|
+
file_name: fileName,
|
|
930
|
+
};
|
|
931
|
+
if (mimeType.startsWith("image/")) {
|
|
932
|
+
images.push(payload);
|
|
933
|
+
}
|
|
934
|
+
else if (mimeType.startsWith("audio/") && !audio) {
|
|
935
|
+
audio = {
|
|
936
|
+
mime_type: mimeType,
|
|
937
|
+
data: payload.data,
|
|
938
|
+
format: mimeType.replace(/^audio\//, ""),
|
|
939
|
+
file_name: fileName,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
files.push(payload);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return {
|
|
947
|
+
...(images.length > 0 ? { images } : {}),
|
|
948
|
+
...(files.length > 0 ? { files } : {}),
|
|
949
|
+
...(audio ? { audio } : {}),
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function isRecord(value) {
|
|
953
|
+
return typeof value === "object" && value !== null;
|
|
954
|
+
}
|
|
448
955
|
//# sourceMappingURL=ccconnect-relay-manager.js.map
|