@rethinkingstudio/clawpilot 2.1.7-internal.0 → 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 +586 -20
- 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);
|
|
@@ -15,7 +17,11 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
15
17
|
let relayWs;
|
|
16
18
|
let bridgeWs = null;
|
|
17
19
|
let bridgeReady = false;
|
|
20
|
+
let bridgeReconnectTimer = null;
|
|
21
|
+
let bridgePingTimer = null;
|
|
22
|
+
let bridgeReconnectAttempts = 0;
|
|
18
23
|
const rawToEncoded = new Map();
|
|
24
|
+
const rawToProject = new Map();
|
|
19
25
|
const activeRunByEncoded = new Map();
|
|
20
26
|
try {
|
|
21
27
|
relayWs = new WebSocket(wsUrl);
|
|
@@ -50,7 +56,44 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
50
56
|
const sep = opts.bridgeUrl.includes("?") ? "&" : "?";
|
|
51
57
|
return `${opts.bridgeUrl}${sep}token=${encodeURIComponent(opts.bridgeToken)}`;
|
|
52
58
|
}
|
|
59
|
+
function clearBridgeTimers() {
|
|
60
|
+
if (bridgeReconnectTimer) {
|
|
61
|
+
clearTimeout(bridgeReconnectTimer);
|
|
62
|
+
bridgeReconnectTimer = null;
|
|
63
|
+
}
|
|
64
|
+
if (bridgePingTimer) {
|
|
65
|
+
clearInterval(bridgePingTimer);
|
|
66
|
+
bridgePingTimer = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function startBridgePing() {
|
|
70
|
+
if (bridgePingTimer)
|
|
71
|
+
clearInterval(bridgePingTimer);
|
|
72
|
+
bridgePingTimer = setInterval(() => {
|
|
73
|
+
if (bridgeWs?.readyState === WebSocket.OPEN) {
|
|
74
|
+
bridgeWs.send(JSON.stringify({ type: "ping", ts: Date.now() }));
|
|
75
|
+
}
|
|
76
|
+
}, 30_000);
|
|
77
|
+
bridgePingTimer.unref();
|
|
78
|
+
}
|
|
79
|
+
function scheduleBridgeReconnect(reason) {
|
|
80
|
+
if (relayWs.readyState !== WebSocket.OPEN || bridgeReconnectTimer)
|
|
81
|
+
return;
|
|
82
|
+
const delay = Math.min(30_000, 1_000 * 2 ** Math.min(bridgeReconnectAttempts, 5));
|
|
83
|
+
bridgeReconnectAttempts += 1;
|
|
84
|
+
console.log(`[runtime:ccconnect] bridge reconnect in ${delay}ms: ${reason}`);
|
|
85
|
+
bridgeReconnectTimer = setTimeout(() => {
|
|
86
|
+
bridgeReconnectTimer = null;
|
|
87
|
+
connectBridge();
|
|
88
|
+
}, delay);
|
|
89
|
+
bridgeReconnectTimer.unref();
|
|
90
|
+
}
|
|
53
91
|
function connectBridge() {
|
|
92
|
+
clearBridgeTimers();
|
|
93
|
+
bridgeReady = false;
|
|
94
|
+
if (bridgeWs && bridgeWs.readyState !== WebSocket.CLOSED) {
|
|
95
|
+
bridgeWs.close();
|
|
96
|
+
}
|
|
54
97
|
try {
|
|
55
98
|
bridgeWs = new WebSocket(bridgeUrlWithToken());
|
|
56
99
|
}
|
|
@@ -58,17 +101,33 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
58
101
|
const detail = error instanceof Error ? error.message : String(error);
|
|
59
102
|
console.error(`[runtime:ccconnect] bridge create failed: ${detail}`);
|
|
60
103
|
send({ type: "gateway_disconnected", reason: detail });
|
|
104
|
+
scheduleBridgeReconnect(detail);
|
|
61
105
|
return;
|
|
62
106
|
}
|
|
63
|
-
|
|
64
|
-
|
|
107
|
+
const ws = bridgeWs;
|
|
108
|
+
ws.on("open", () => {
|
|
109
|
+
ws.send(JSON.stringify({
|
|
65
110
|
type: "register",
|
|
66
111
|
platform: "pocketclaw",
|
|
67
|
-
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
|
+
],
|
|
68
125
|
metadata: { version: "1.0.0", description: "PocketClaw relay adapter" },
|
|
69
126
|
}));
|
|
70
127
|
});
|
|
71
|
-
|
|
128
|
+
ws.on("message", (raw) => {
|
|
129
|
+
if (bridgeWs !== ws)
|
|
130
|
+
return;
|
|
72
131
|
try {
|
|
73
132
|
handleBridgeMessage(JSON.parse(raw.toString()));
|
|
74
133
|
}
|
|
@@ -76,32 +135,50 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
76
135
|
console.warn(`[runtime:ccconnect] ignored invalid bridge message: ${String(error)}`);
|
|
77
136
|
}
|
|
78
137
|
});
|
|
79
|
-
|
|
138
|
+
ws.on("close", (code, reason) => {
|
|
139
|
+
if (bridgeWs !== ws)
|
|
140
|
+
return;
|
|
80
141
|
bridgeReady = false;
|
|
142
|
+
if (bridgePingTimer) {
|
|
143
|
+
clearInterval(bridgePingTimer);
|
|
144
|
+
bridgePingTimer = null;
|
|
145
|
+
}
|
|
81
146
|
console.log(`[runtime:ccconnect] bridge disconnected: ${code} ${reason.toString()}`);
|
|
82
147
|
send({ type: "gateway_disconnected", reason: "cc-connect bridge disconnected" });
|
|
148
|
+
scheduleBridgeReconnect(`close ${code}`);
|
|
83
149
|
});
|
|
84
|
-
|
|
150
|
+
ws.on("error", (error) => {
|
|
151
|
+
if (bridgeWs !== ws)
|
|
152
|
+
return;
|
|
85
153
|
bridgeReady = false;
|
|
86
154
|
console.error(`[runtime:ccconnect] bridge error: ${error.message}`);
|
|
87
155
|
send({ type: "gateway_disconnected", reason: error.message });
|
|
156
|
+
scheduleBridgeReconnect(error.message);
|
|
88
157
|
});
|
|
89
158
|
}
|
|
90
159
|
function handleBridgeMessage(msg) {
|
|
160
|
+
void handleBridgeMessageAsync(msg);
|
|
161
|
+
}
|
|
162
|
+
async function handleBridgeMessageAsync(msg) {
|
|
91
163
|
const record = msg;
|
|
92
164
|
if (msg.type === "register_ack") {
|
|
93
165
|
bridgeReady = Boolean(msg.ok);
|
|
94
166
|
if (bridgeReady) {
|
|
167
|
+
bridgeReconnectAttempts = 0;
|
|
95
168
|
console.log("[runtime:ccconnect] bridge connected");
|
|
169
|
+
startBridgePing();
|
|
96
170
|
send({ type: "gateway_connected" });
|
|
97
171
|
}
|
|
98
172
|
else {
|
|
99
173
|
send({ type: "gateway_disconnected", reason: stringValue(record.error) ?? "cc-connect bridge registration failed" });
|
|
174
|
+
scheduleBridgeReconnect(stringValue(record.error) ?? "registration failed");
|
|
100
175
|
}
|
|
101
176
|
return;
|
|
102
177
|
}
|
|
178
|
+
if (msg.type === "pong")
|
|
179
|
+
return;
|
|
103
180
|
const rawSessionKey = stringValue(record.session_key) ?? "";
|
|
104
|
-
const encodedSessionKey =
|
|
181
|
+
const encodedSessionKey = await resolveEncodedBridgeSession(record);
|
|
105
182
|
if (!encodedSessionKey)
|
|
106
183
|
return;
|
|
107
184
|
const runId = ensureRunId(encodedSessionKey);
|
|
@@ -128,27 +205,69 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
128
205
|
emitChat(runId, encodedSessionKey, "delta", stringValue(record.content) ?? "", false);
|
|
129
206
|
return;
|
|
130
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
|
+
}
|
|
131
221
|
if (msg.type === "reply_stream") {
|
|
132
222
|
const text = stringValue(record.full_text) ?? stringValue(record.delta) ?? "";
|
|
133
223
|
const done = Boolean(record.done);
|
|
134
|
-
|
|
135
|
-
|
|
224
|
+
const state = done && !isIntermediateReply(text) ? "final" : "delta";
|
|
225
|
+
emitChat(runId, encodedSessionKey, state, text, false);
|
|
226
|
+
if (state === "final")
|
|
136
227
|
activeRunByEncoded.delete(encodedSessionKey);
|
|
137
228
|
return;
|
|
138
229
|
}
|
|
139
230
|
if (msg.type === "reply") {
|
|
140
|
-
|
|
141
|
-
|
|
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);
|
|
142
245
|
return;
|
|
143
246
|
}
|
|
144
247
|
if (msg.type === "buttons") {
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
});
|
|
147
257
|
return;
|
|
148
258
|
}
|
|
149
259
|
if (msg.type === "card") {
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
});
|
|
152
271
|
return;
|
|
153
272
|
}
|
|
154
273
|
if (msg.type === "error") {
|
|
@@ -156,6 +275,37 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
156
275
|
activeRunByEncoded.delete(encodedSessionKey);
|
|
157
276
|
}
|
|
158
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
|
+
}
|
|
159
309
|
function ensureRunId(encodedSessionKey, preferred) {
|
|
160
310
|
const existing = activeRunByEncoded.get(encodedSessionKey);
|
|
161
311
|
if (existing)
|
|
@@ -175,6 +325,37 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
175
325
|
payload.errorMessage = text ?? "cc-connect error";
|
|
176
326
|
send({ type: "event", event: "chat", payload });
|
|
177
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
|
+
}
|
|
178
359
|
async function respondSessionsList(requestId) {
|
|
179
360
|
const projects = await client.listProjects();
|
|
180
361
|
const sessions = [];
|
|
@@ -191,18 +372,80 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
191
372
|
count: sessions.length,
|
|
192
373
|
});
|
|
193
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
|
+
}
|
|
194
436
|
async function respondMessagesHistory(requestId, params) {
|
|
195
437
|
const p = params;
|
|
196
438
|
if (!p.sessionKey)
|
|
197
439
|
throw new Error("sessionKey required");
|
|
198
440
|
const decoded = decodeCCConnectSessionKey(p.sessionKey);
|
|
199
|
-
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));
|
|
200
443
|
const history = detail.history ?? [];
|
|
201
444
|
const messages = history.map((entry, index) => ({
|
|
202
445
|
id: index + 1,
|
|
203
446
|
role: entry.role === "user" ? "user" : "assistant",
|
|
204
447
|
content: entry.content ?? "",
|
|
205
|
-
runId: entry.role === "user" ? undefined : `${
|
|
448
|
+
runId: entry.role === "user" ? undefined : `${sessionId}-${index + 1}`,
|
|
206
449
|
state: "final",
|
|
207
450
|
createdAt: timestampMs(entry.timestamp),
|
|
208
451
|
updatedAt: timestampMs(entry.timestamp),
|
|
@@ -235,11 +478,14 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
235
478
|
throw new Error("sessionKey required");
|
|
236
479
|
if (!p.message)
|
|
237
480
|
throw new Error("message required");
|
|
238
|
-
const decoded =
|
|
481
|
+
const decoded = await resolveCCConnectSendTarget(p.sessionKey);
|
|
239
482
|
rawToEncoded.set(decoded.sessionKey, p.sessionKey);
|
|
483
|
+
rawToProject.set(decoded.sessionKey, decoded.project);
|
|
484
|
+
activeRunByEncoded.delete(p.sessionKey);
|
|
240
485
|
const runId = ensureRunId(p.sessionKey, p.idempotencyKey);
|
|
241
486
|
emitChat(runId, p.sessionKey, "delta", undefined, true);
|
|
242
487
|
if (bridgeReady && bridgeWs?.readyState === WebSocket.OPEN) {
|
|
488
|
+
console.log(`[runtime:ccconnect] chat.send via bridge project=${decoded.project} rawSessionKey=${decoded.sessionKey}`);
|
|
243
489
|
bridgeWs.send(JSON.stringify({
|
|
244
490
|
type: "message",
|
|
245
491
|
msg_id: `pocketclaw-${Date.now()}`,
|
|
@@ -249,16 +495,68 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
249
495
|
content: p.message,
|
|
250
496
|
reply_ctx: decoded.sessionKey,
|
|
251
497
|
project: decoded.project,
|
|
498
|
+
...bridgeAttachments(params),
|
|
252
499
|
}));
|
|
253
500
|
return;
|
|
254
501
|
}
|
|
502
|
+
scheduleBridgeReconnect("bridge not ready during chat.send");
|
|
503
|
+
console.log(`[runtime:ccconnect] chat.send via management fallback project=${decoded.project} rawSessionKey=${decoded.sessionKey}`);
|
|
255
504
|
await client.send(decoded.project, decoded.sessionKey, p.message);
|
|
256
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
|
+
}
|
|
257
545
|
function rememberSession(project, session) {
|
|
258
|
-
const encoded = encodeCCConnectSessionKey(project, session.session_key, session.
|
|
259
|
-
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
|
+
}
|
|
260
551
|
return encoded;
|
|
261
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
|
+
}
|
|
262
560
|
function toRuntimeSession(project, session, encoded) {
|
|
263
561
|
const platform = session.platform || session.session_key.split(":")[0] || "cc-connect";
|
|
264
562
|
const title = session.name || session.chat_name || session.user_name || session.session_key || session.id;
|
|
@@ -325,6 +623,18 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
325
623
|
}
|
|
326
624
|
try {
|
|
327
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;
|
|
328
638
|
case "sessions.list":
|
|
329
639
|
if (requestId)
|
|
330
640
|
await respondSessionsList(requestId);
|
|
@@ -342,6 +652,11 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
342
652
|
if (requestId)
|
|
343
653
|
sendResponse(requestId, { ok: true });
|
|
344
654
|
return;
|
|
655
|
+
case "chat.action":
|
|
656
|
+
await handleChatAction(msg.params ?? {});
|
|
657
|
+
if (requestId)
|
|
658
|
+
sendResponse(requestId, { ok: true });
|
|
659
|
+
return;
|
|
345
660
|
case "chat.abort":
|
|
346
661
|
if (requestId)
|
|
347
662
|
sendResponse(requestId, { ok: true });
|
|
@@ -370,6 +685,7 @@ export async function runCCConnectRelayManager(opts) {
|
|
|
370
685
|
relayWs.on("close", (code, reason) => {
|
|
371
686
|
console.log(`cc-connect relay connection closed: ${code} ${reason.toString()}`);
|
|
372
687
|
opts.onDisconnected?.();
|
|
688
|
+
clearBridgeTimers();
|
|
373
689
|
bridgeWs?.close();
|
|
374
690
|
bridgeWs = null;
|
|
375
691
|
resolve(code !== 4000);
|
|
@@ -386,4 +702,254 @@ function buildRelayUrl(base, gatewayId, relaySecret) {
|
|
|
386
702
|
function stringValue(value) {
|
|
387
703
|
return typeof value === "string" ? value : undefined;
|
|
388
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
|
+
}
|
|
389
955
|
//# sourceMappingURL=ccconnect-relay-manager.js.map
|