claw-control-center 0.1.2 → 0.1.4

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/index.cjs CHANGED
@@ -3850,6 +3850,7 @@ var DEFAULT_THINKING_MESSAGE = "\u6B63\u5728\u5904\u7406\u60A8\u7684\u8BF7\u6C42
3850
3850
  var MAX_OUTBOX_FRAMES = 200;
3851
3851
  var RUN_WAIT_TIMEOUT_MS = 30 * 60 * 1e3;
3852
3852
  var HUB_SESSION_TITLE_PREFIX = "53AI Hub-";
3853
+ var CONTROL_CENTER_SESSION_TITLE = "Claw Control Center";
3853
3854
  var HUB_TITLE_SUMMARY_LENGTH = 40;
3854
3855
  var HUB_RPC_ACTIONS = /* @__PURE__ */ new Set([
3855
3856
  "sessions.list",
@@ -4069,10 +4070,11 @@ function createHub53AIBridge(input) {
4069
4070
  async function resolveRPCRequest(request) {
4070
4071
  if (request.action === "sessions.list") {
4071
4072
  const pagination = readRPCPagination(request.data, 50);
4072
- return input.gateway.listSessionPage({
4073
+ const page = await input.gateway.listSessionPage({
4073
4074
  limit: pagination.limit,
4074
4075
  offset: pagination.offset
4075
4076
  });
4077
+ return mergeKnownHubSessionTitles(page);
4076
4078
  }
4077
4079
  if (request.action === "sessions.current") {
4078
4080
  return resolveCurrentSessionRPC(request.data);
@@ -4216,13 +4218,6 @@ function createHub53AIBridge(input) {
4216
4218
  if (!chatId) {
4217
4219
  throw new HubRPCError("PARAM_ERROR", "chat_id or user is required");
4218
4220
  }
4219
- if (isOpenClawSessionId(chatId)) {
4220
- return input.gateway.getSession(chatId);
4221
- }
4222
- const mappedSession = await getMappedSession(chatId);
4223
- if (mappedSession) {
4224
- return mappedSession;
4225
- }
4226
4221
  return restoreLatestHubSession(chatId, userName);
4227
4222
  }
4228
4223
  async function getMappedSession(chatId) {
@@ -4252,15 +4247,34 @@ function createHub53AIBridge(input) {
4252
4247
  limit: 100,
4253
4248
  offset: 0
4254
4249
  });
4255
- const sessions = page.sessions.filter((session2) => isRestorableHubSession(session2, chatId, userName)).sort((left, right) => toTime(right.updatedAt || right.createdAt) - toTime(left.updatedAt || left.createdAt));
4250
+ const knownSessions = await listKnownSessions();
4251
+ const sessions = mergeKnownHubSessions(page.sessions, knownSessions).filter((session2) => isRestorableHubSession(session2, chatId, userName)).sort((left, right) => toTime(right.updatedAt || right.createdAt) - toTime(left.updatedAt || left.createdAt));
4256
4252
  const session = sessions[0];
4257
4253
  if (!session) {
4258
4254
  return null;
4259
4255
  }
4260
- state.mappings[chatId] = session.id;
4261
- await persistState();
4262
4256
  return session;
4263
4257
  }
4258
+ async function mergeKnownHubSessionTitles(page) {
4259
+ const knownSessions = await listKnownSessions();
4260
+ if (!knownSessions.length) {
4261
+ return page;
4262
+ }
4263
+ return {
4264
+ ...page,
4265
+ sessions: mergeKnownHubSessions(page.sessions, knownSessions)
4266
+ };
4267
+ }
4268
+ async function listKnownSessions() {
4269
+ try {
4270
+ return await input.callbacks.listKnownSessions?.() ?? [];
4271
+ } catch (error) {
4272
+ input.logger?.warn?.(
4273
+ `Failed to read known 53AIHub sessions: ${error instanceof Error ? error.message : String(error)}`
4274
+ );
4275
+ return [];
4276
+ }
4277
+ }
4264
4278
  async function buildFallbackStatus() {
4265
4279
  const [gatewayHealth, runtimeInfo] = await Promise.allSettled([input.gateway.getHealth(), input.gateway.getRuntimeInfo()]);
4266
4280
  return {
@@ -4548,18 +4562,10 @@ function createHub53AIBridge(input) {
4548
4562
  async function resolveSession(message) {
4549
4563
  const desiredTitle = buildHubSessionTitle(message);
4550
4564
  if (isOpenClawSessionId(message.chatId)) {
4551
- const session2 = await input.gateway.getSession(message.chatId);
4552
- state.mappings[message.chatId] = session2.id;
4553
- await persistState();
4565
+ const session2 = await getSessionWithKnownHubTitle(message.chatId, message.conversationTitle);
4554
4566
  await input.callbacks.onSessionUpsert(session2);
4555
4567
  return session2;
4556
4568
  }
4557
- const mappedSession = await getMappedSession(message.chatId);
4558
- if (mappedSession) {
4559
- const nextSession = await renamePlaceholderSessionIfNeeded(mappedSession, message, desiredTitle);
4560
- await input.callbacks.onSessionUpsert(nextSession);
4561
- return nextSession;
4562
- }
4563
4569
  const restoredSession = await restoreLatestHubSession(message.chatId, message.userName || message.userId);
4564
4570
  if (restoredSession) {
4565
4571
  const nextSession = await renamePlaceholderSessionIfNeeded(restoredSession, message, desiredTitle);
@@ -4567,11 +4573,15 @@ function createHub53AIBridge(input) {
4567
4573
  return nextSession;
4568
4574
  }
4569
4575
  const session = await createSessionWithUniqueTitle(desiredTitle);
4570
- state.mappings[message.chatId] = session.id;
4571
- await persistState();
4572
4576
  await input.callbacks.onSessionUpsert(session);
4573
4577
  return session;
4574
4578
  }
4579
+ async function getSessionWithKnownHubTitle(sessionId, titleHint) {
4580
+ const session = await input.gateway.getSession(sessionId);
4581
+ const knownSessions = await listKnownSessions();
4582
+ const titleHintSession = applyHubTitleHint(session, titleHint);
4583
+ return mergeKnownHubSessions([titleHintSession], knownSessions)[0] ?? titleHintSession;
4584
+ }
4575
4585
  async function createSessionWithUniqueTitle(baseTitle) {
4576
4586
  let lastDuplicateError;
4577
4587
  for (let attempt = 0; attempt < 6; attempt += 1) {
@@ -4737,6 +4747,7 @@ function parseIncomingMessage(rawJson) {
4737
4747
  chatId: chatId2,
4738
4748
  userId: userId2,
4739
4749
  userName: extractUserName(openAIReq, metadata, userObject2, userMessage),
4750
+ conversationTitle: extractConversationTitle(openAIReq, metadata),
4740
4751
  text: extractTextFromContent(content),
4741
4752
  imageUrls: extractImagesFromContent(content),
4742
4753
  fileUrls: extractFilesFromContent(content)
@@ -4756,6 +4767,7 @@ function parseIncomingMessage(rawJson) {
4756
4767
  chatId,
4757
4768
  userId,
4758
4769
  userName: extractUserName(data, userObject),
4770
+ conversationTitle: extractConversationTitle(data),
4759
4771
  text: stringOr(data.text, data.content, ""),
4760
4772
  imageUrls: normalizeUrlList(data.imageUrls, data.images),
4761
4773
  fileUrls: normalizeUrlList(data.fileUrls, data.files),
@@ -4895,6 +4907,23 @@ function extractUserName(...sources) {
4895
4907
  }
4896
4908
  return void 0;
4897
4909
  }
4910
+ function extractConversationTitle(...sources) {
4911
+ const titleKeys = [
4912
+ "openclaw_conversation_title",
4913
+ "openclawConversationTitle",
4914
+ "conversation_title",
4915
+ "conversationTitle",
4916
+ "title"
4917
+ ];
4918
+ for (const source of sources) {
4919
+ const record = toRecord2(source);
4920
+ const title = stringFromKeys(record, titleKeys);
4921
+ if (title) {
4922
+ return title;
4923
+ }
4924
+ }
4925
+ return void 0;
4926
+ }
4898
4927
  function stringFromKeys(record, keys) {
4899
4928
  for (const key of keys) {
4900
4929
  const value = record[key];
@@ -4963,6 +4992,44 @@ function isRestorableHubSession(session, chatId, userName) {
4963
4992
  }
4964
4993
  return normalized.startsWith(`${HUB_SESSION_TITLE_PREFIX}${sanitizeTitlePart(userName)}\uFF1A`);
4965
4994
  }
4995
+ function mergeKnownHubSessions(gatewaySessions, knownSessions) {
4996
+ const knownHubById = new Map(
4997
+ knownSessions.filter((session) => isHubTitle(session.title)).map((session) => [session.id, session])
4998
+ );
4999
+ if (!knownHubById.size) {
5000
+ return gatewaySessions;
5001
+ }
5002
+ const merged = gatewaySessions.map((session) => {
5003
+ const knownSession = knownHubById.get(session.id);
5004
+ if (!knownSession || !shouldPreferKnownHubTitle(session.title, knownSession.title)) {
5005
+ return session;
5006
+ }
5007
+ return {
5008
+ ...session,
5009
+ title: knownSession.title
5010
+ };
5011
+ });
5012
+ return merged;
5013
+ }
5014
+ function applyHubTitleHint(session, titleHint) {
5015
+ const normalizedTitle = titleHint?.trim();
5016
+ if (!normalizedTitle || !isHubTitle(normalizedTitle)) {
5017
+ return session;
5018
+ }
5019
+ return {
5020
+ ...session,
5021
+ title: normalizedTitle
5022
+ };
5023
+ }
5024
+ function shouldPreferKnownHubTitle(gatewayTitle, knownTitle) {
5025
+ return isHubTitle(knownTitle) && (isControlCenterTitle(gatewayTitle) || !isHubTitle(gatewayTitle));
5026
+ }
5027
+ function isHubTitle(title) {
5028
+ return title.trim().startsWith(HUB_SESSION_TITLE_PREFIX);
5029
+ }
5030
+ function isControlCenterTitle(title) {
5031
+ return title.trim() === CONTROL_CENTER_SESSION_TITLE;
5032
+ }
4966
5033
  function toTime(value) {
4967
5034
  if (!value) return 0;
4968
5035
  const time = Date.parse(value);
@@ -5181,6 +5248,8 @@ function toRecord2(value) {
5181
5248
  // src/file-store.ts
5182
5249
  var import_promises2 = require("fs/promises");
5183
5250
  var import_node_path2 = require("path");
5251
+ var HUB_SESSION_TITLE_PREFIX2 = "53AI Hub-";
5252
+ var CONTROL_CENTER_SESSION_TITLE2 = "Claw Control Center";
5184
5253
  var FileSessionStore = class {
5185
5254
  constructor(filePath, maxSessions) {
5186
5255
  this.filePath = filePath;
@@ -5213,9 +5282,29 @@ var FileSessionStore = class {
5213
5282
  return this.state.sessions[sessionId]?.hydrated ?? false;
5214
5283
  }
5215
5284
  async upsertSession(session) {
5285
+ this.mergeSession(session);
5286
+ this.trimSessions();
5287
+ await this.persist();
5288
+ }
5289
+ async replaceSessions(sessions) {
5290
+ const remoteIds = new Set(sessions.map((session) => session.id));
5291
+ for (const session of sessions) {
5292
+ this.mergeSession(session);
5293
+ }
5294
+ for (const sessionId of Object.keys(this.state.sessions)) {
5295
+ if (!remoteIds.has(sessionId)) {
5296
+ delete this.state.sessions[sessionId];
5297
+ }
5298
+ }
5299
+ this.trimSessions();
5300
+ await this.persist();
5301
+ }
5302
+ mergeSession(session) {
5216
5303
  const existing = this.state.sessions[session.id];
5304
+ const title = preserveExistingHubTitle(existing?.session.title, session.title);
5217
5305
  const mergedSession = {
5218
5306
  ...session,
5307
+ title,
5219
5308
  lastEventSeq: Math.max(session.lastEventSeq, existing?.session.lastEventSeq ?? 0)
5220
5309
  };
5221
5310
  this.state.sessions[session.id] = {
@@ -5224,8 +5313,6 @@ var FileSessionStore = class {
5224
5313
  events: existing?.events ?? [],
5225
5314
  hydrated: existing?.hydrated ?? false
5226
5315
  };
5227
- this.trimSessions();
5228
- await this.persist();
5229
5316
  }
5230
5317
  async renameSession(sessionId, title) {
5231
5318
  const record = this.requireSession(sessionId);
@@ -5294,6 +5381,18 @@ var FileSessionStore = class {
5294
5381
  await this.persistChain;
5295
5382
  }
5296
5383
  };
5384
+ function preserveExistingHubTitle(existingTitle, incomingTitle) {
5385
+ if (isHubTitle2(existingTitle) && isControlCenterTitle2(incomingTitle)) {
5386
+ return existingTitle;
5387
+ }
5388
+ return incomingTitle;
5389
+ }
5390
+ function isHubTitle2(title) {
5391
+ return typeof title === "string" && title.trim().startsWith(HUB_SESSION_TITLE_PREFIX2);
5392
+ }
5393
+ function isControlCenterTitle2(title) {
5394
+ return title.trim() === CONTROL_CENTER_SESSION_TITLE2;
5395
+ }
5297
5396
  function dedupeMessages(messages) {
5298
5397
  return Array.from(new Map(messages.map((message) => [message.id, message])).values()).sort(
5299
5398
  (left, right) => left.createdAt.localeCompare(right.createdAt)
@@ -5639,6 +5738,7 @@ function createConsoleServer(input) {
5639
5738
  broadcastSessionEvent(event.sessionId, event);
5640
5739
  },
5641
5740
  listSessionEvents: (sessionId) => store.getSession(sessionId)?.events ?? [],
5741
+ listKnownSessions: () => store.listSessions(),
5642
5742
  onEnsureSessionStream: ensureSessionStream,
5643
5743
  getLastEventSeq: (sessionId) => store.getLastEventSeq(sessionId),
5644
5744
  onStatusChange: broadcastStatus
@@ -5980,9 +6080,7 @@ function createConsoleServer(input) {
5980
6080
  const before = sessionListSignature(store.listSessions());
5981
6081
  const remoteSessions = await input.gateway.listSessions(input.persistence.maxSessions);
5982
6082
  lastGatewayError = null;
5983
- for (const session of remoteSessions) {
5984
- await store.upsertSession(session);
5985
- }
6083
+ await store.replaceSessions(remoteSessions);
5986
6084
  return before !== sessionListSignature(store.listSessions());
5987
6085
  } catch (error) {
5988
6086
  lastGatewayError = error instanceof Error ? error : new Error(String(error));
@@ -6378,6 +6476,9 @@ var CHAT_HISTORY_MAX_PAGES = 100;
6378
6476
  var CRON_LIST_PAGE_LIMIT = 50;
6379
6477
  var CRON_LIST_MAX_PAGES = 100;
6380
6478
  var OPENCLAW_STOP_SETTLE_TIMEOUT_MS = 3e3;
6479
+ var GATEWAY_PROTOCOL_MIN = 3;
6480
+ var GATEWAY_PROTOCOL_MAX = 4;
6481
+ var WEBSOCKET_CONNECTING = 0;
6381
6482
  function createGatewayClient(config) {
6382
6483
  const resolved = resolveGatewayConfig(config);
6383
6484
  const hostKind = resolved.hostKind ?? "openclaw";
@@ -7139,6 +7240,82 @@ function readUnknownPath(value, path) {
7139
7240
  }
7140
7241
  return current;
7141
7242
  }
7243
+ function createGatewayFrameError(error) {
7244
+ const message = error?.message ?? "gateway request failed";
7245
+ const details = error?.details !== void 0 ? `: ${JSON.stringify(error.details)}` : "";
7246
+ return new Error(`${message}${details}`);
7247
+ }
7248
+ function createGatewayHandshakeError(error) {
7249
+ if (!/protocol mismatch/i.test(error.message)) {
7250
+ return error;
7251
+ }
7252
+ return new Error(`Gateway protocol negotiation failed: ${error.message}`);
7253
+ }
7254
+ function createUnsupportedGatewayProtocolError(expectedProtocol, originalError) {
7255
+ return new Error(
7256
+ `Gateway protocol ${expectedProtocol} is not supported by this client; supported range is ${GATEWAY_PROTOCOL_MIN}-${GATEWAY_PROTOCOL_MAX}. Original error: ${originalError.message}`
7257
+ );
7258
+ }
7259
+ function extractExpectedGatewayProtocol(error, fallbackMessage) {
7260
+ const fromDetails = readExpectedProtocolValue(error?.details);
7261
+ if (fromDetails !== null) {
7262
+ return fromDetails;
7263
+ }
7264
+ return readExpectedProtocolFromText([error?.message, fallbackMessage].filter(Boolean).join(" "));
7265
+ }
7266
+ function readExpectedProtocolValue(value, depth = 0) {
7267
+ if (value === null || value === void 0 || depth > 4) {
7268
+ return null;
7269
+ }
7270
+ if (typeof value === "string") {
7271
+ const fromText = readExpectedProtocolFromText(value);
7272
+ if (fromText !== null) {
7273
+ return fromText;
7274
+ }
7275
+ try {
7276
+ return readExpectedProtocolValue(JSON.parse(value), depth + 1);
7277
+ } catch {
7278
+ return null;
7279
+ }
7280
+ }
7281
+ if (Array.isArray(value)) {
7282
+ for (const item of value) {
7283
+ const match = readExpectedProtocolValue(item, depth + 1);
7284
+ if (match !== null) {
7285
+ return match;
7286
+ }
7287
+ }
7288
+ return null;
7289
+ }
7290
+ if (typeof value !== "object") {
7291
+ return null;
7292
+ }
7293
+ const record = value;
7294
+ for (const key of ["expectedProtocol", "expected_protocol"]) {
7295
+ const match = coerceGatewayProtocol(record[key]);
7296
+ if (match !== null) {
7297
+ return match;
7298
+ }
7299
+ }
7300
+ for (const item of Object.values(record)) {
7301
+ const match = readExpectedProtocolValue(item, depth + 1);
7302
+ if (match !== null) {
7303
+ return match;
7304
+ }
7305
+ }
7306
+ return null;
7307
+ }
7308
+ function readExpectedProtocolFromText(value) {
7309
+ const match = value.match(/["']?expectedProtocol["']?\s*[:=]\s*["']?(\d+)["']?/i);
7310
+ return match ? coerceGatewayProtocol(match[1]) : null;
7311
+ }
7312
+ function coerceGatewayProtocol(value) {
7313
+ const protocol = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
7314
+ return Number.isInteger(protocol) ? protocol : null;
7315
+ }
7316
+ function isSupportedGatewayProtocol(protocol) {
7317
+ return protocol >= GATEWAY_PROTOCOL_MIN && protocol <= GATEWAY_PROTOCOL_MAX;
7318
+ }
7142
7319
  var RpcSocketClient = class {
7143
7320
  constructor(config) {
7144
7321
  this.config = config;
@@ -7151,6 +7328,8 @@ var RpcSocketClient = class {
7151
7328
  connectPromise = null;
7152
7329
  ready = false;
7153
7330
  challengeNonce = null;
7331
+ connectProtocolOverride = null;
7332
+ connectProtocolRetryUsed = false;
7154
7333
  stopped = false;
7155
7334
  onEvent(listener) {
7156
7335
  this.listeners.add(listener);
@@ -7220,6 +7399,7 @@ var RpcSocketClient = class {
7220
7399
  this.socket = socket;
7221
7400
  this.ready = false;
7222
7401
  this.challengeNonce = null;
7402
+ this.connectProtocolRetryUsed = false;
7223
7403
  let settled = false;
7224
7404
  const finishError = (error) => {
7225
7405
  if (settled) {
@@ -7229,6 +7409,9 @@ var RpcSocketClient = class {
7229
7409
  if (this.socket === socket) {
7230
7410
  this.socket = null;
7231
7411
  }
7412
+ if (socket.readyState === WEBSOCKET_CONNECTING || socket.readyState === wrapper_default.OPEN) {
7413
+ socket.close();
7414
+ }
7232
7415
  this.ready = false;
7233
7416
  this.challengeNonce = null;
7234
7417
  reject(error);
@@ -7296,11 +7479,20 @@ var RpcSocketClient = class {
7296
7479
  clearTimeout(pending.timeout);
7297
7480
  }
7298
7481
  if (!frame.ok) {
7299
- const message = frame.error?.message ?? "gateway request failed";
7300
- const details = frame.error?.details ? `: ${JSON.stringify(frame.error.details)}` : "";
7301
- const error = new Error(`${message}${details}`);
7482
+ const error = createGatewayFrameError(frame.error);
7302
7483
  if (frame.id === "connect-handshake") {
7303
- rejectConnect(error);
7484
+ const expectedProtocol = extractExpectedGatewayProtocol(frame.error, error.message);
7485
+ if (expectedProtocol !== null && !isSupportedGatewayProtocol(expectedProtocol)) {
7486
+ rejectConnect(createUnsupportedGatewayProtocolError(expectedProtocol, error));
7487
+ return;
7488
+ }
7489
+ if (expectedProtocol !== null && !this.connectProtocolRetryUsed) {
7490
+ this.connectProtocolOverride = expectedProtocol;
7491
+ this.connectProtocolRetryUsed = true;
7492
+ this.sendConnect();
7493
+ return;
7494
+ }
7495
+ rejectConnect(createGatewayHandshakeError(error));
7304
7496
  return;
7305
7497
  }
7306
7498
  pending.reject(error);
@@ -7318,13 +7510,15 @@ var RpcSocketClient = class {
7318
7510
  if (!this.socket || this.socket.readyState !== wrapper_default.OPEN || !this.challengeNonce) {
7319
7511
  return;
7320
7512
  }
7513
+ const minProtocol = this.connectProtocolOverride ?? GATEWAY_PROTOCOL_MIN;
7514
+ const maxProtocol = this.connectProtocolOverride ?? GATEWAY_PROTOCOL_MAX;
7321
7515
  const frame = {
7322
7516
  type: "req",
7323
7517
  id: "connect-handshake",
7324
7518
  method: "connect",
7325
7519
  params: {
7326
- minProtocol: 4,
7327
- maxProtocol: 4,
7520
+ minProtocol,
7521
+ maxProtocol,
7328
7522
  client: {
7329
7523
  id: "gateway-client",
7330
7524
  displayName: "Claw Control Center",
package/dist/index.d.cts CHANGED
@@ -142,6 +142,7 @@ type Hub53AIIncomingMessage = {
142
142
  imageUrls?: string[];
143
143
  fileUrls?: string[];
144
144
  quoteContent?: string;
145
+ conversationTitle?: string;
145
146
  };
146
147
  type Hub53AIOutgoingChunk = {
147
148
  req_id: string;
@@ -187,6 +188,7 @@ type HubBridgeCallbacks = {
187
188
  onSessionStatus(sessionId: string, status: SessionStatus): Promise<void>;
188
189
  onBridgeThinkingEvent?(event: TimelineEvent): Promise<void>;
189
190
  listSessionEvents?(sessionId: string): TimelineEvent[] | Promise<TimelineEvent[]>;
191
+ listKnownSessions?(): GatewaySession[] | Promise<GatewaySession[]>;
190
192
  onEnsureSessionStream(sessionId: string): Promise<void>;
191
193
  getLastEventSeq(sessionId: string): number;
192
194
  onStatusChange(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claw-control-center",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "types": "dist/index.d.ts",