openclaw-quiubo 2.6.13 → 2.6.16
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.js +154 -19
- package/dist/index.js.map +3 -3
- package/dist/src/channel.d.ts.map +1 -1
- package/dist/src/realtime-gateway.d.ts +35 -2
- package/dist/src/realtime-gateway.d.ts.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8952,7 +8952,7 @@ function getQuiuboRuntime() {
|
|
|
8952
8952
|
}
|
|
8953
8953
|
|
|
8954
8954
|
// src/channel.ts
|
|
8955
|
-
import { readFile } from "fs/promises";
|
|
8955
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
8956
8956
|
import { basename } from "path";
|
|
8957
8957
|
|
|
8958
8958
|
// src/api.ts
|
|
@@ -9228,6 +9228,8 @@ var QuiuboApiClient = class {
|
|
|
9228
9228
|
|
|
9229
9229
|
// src/realtime-gateway.ts
|
|
9230
9230
|
var import_pusher_js = __toESM(require_pusher(), 1);
|
|
9231
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
9232
|
+
import { join } from "path";
|
|
9231
9233
|
|
|
9232
9234
|
// ../../node_modules/.pnpm/@noble+hashes@1.8.0/node_modules/@noble/hashes/esm/cryptoNode.js
|
|
9233
9235
|
import * as nc from "node:crypto";
|
|
@@ -12448,6 +12450,7 @@ function signChallenge(challengeBase64, signingPrivateKey) {
|
|
|
12448
12450
|
}
|
|
12449
12451
|
|
|
12450
12452
|
// src/realtime-gateway.ts
|
|
12453
|
+
var CURSORS_DIR = join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".openclaw", "cron");
|
|
12451
12454
|
var consoleLogger = {
|
|
12452
12455
|
debug: (...args) => console.debug("[Quiubo]", ...args),
|
|
12453
12456
|
info: (...args) => console.log("[Quiubo]", ...args),
|
|
@@ -12470,8 +12473,16 @@ var RealtimeGateway = class {
|
|
|
12470
12473
|
pollTimer = null;
|
|
12471
12474
|
polling = false;
|
|
12472
12475
|
cursors = /* @__PURE__ */ new Map();
|
|
12473
|
-
/** Message IDs already dispatched (Pusher + poll dedup).
|
|
12476
|
+
/** Message IDs already dispatched (Pusher + poll dedup). FIFO-capped to prevent unbounded growth. */
|
|
12474
12477
|
dispatched = /* @__PURE__ */ new Set();
|
|
12478
|
+
dispatchedOrder = [];
|
|
12479
|
+
DISPATCHED_MAX = 500;
|
|
12480
|
+
DISPATCHED_EVICT = 100;
|
|
12481
|
+
// Backoff state for poll failures
|
|
12482
|
+
consecutivePollErrors = 0;
|
|
12483
|
+
POLL_CONCURRENCY = 5;
|
|
12484
|
+
MAX_BACKOFF_MS = 6e4;
|
|
12485
|
+
// cap at 60s
|
|
12475
12486
|
// Cached directory group IDs from listAgentGroups (refreshed every 60s)
|
|
12476
12487
|
agentGroupsCache = null;
|
|
12477
12488
|
AGENT_GROUPS_TTL = 6e4;
|
|
@@ -12482,6 +12493,11 @@ var RealtimeGateway = class {
|
|
|
12482
12493
|
// Heartbeat state
|
|
12483
12494
|
agentId;
|
|
12484
12495
|
heartbeatTimer = null;
|
|
12496
|
+
// Cursor save debounce (avoids disk thrash on rapid Pusher events)
|
|
12497
|
+
cursorSaveTimer = null;
|
|
12498
|
+
cursorsFile;
|
|
12499
|
+
// Pusher connection timeout (cancelled on stop to prevent stale firing)
|
|
12500
|
+
pusherConnectTimeout = null;
|
|
12485
12501
|
// E2EE state
|
|
12486
12502
|
keyManager;
|
|
12487
12503
|
e2eeGrantedGroups;
|
|
@@ -12492,6 +12508,7 @@ var RealtimeGateway = class {
|
|
|
12492
12508
|
constructor(opts) {
|
|
12493
12509
|
this.client = opts.client;
|
|
12494
12510
|
this.botIdentityId = opts.botIdentityId;
|
|
12511
|
+
this.cursorsFile = join(CURSORS_DIR, `quiubo-cursors-${opts.accountId}.json`);
|
|
12495
12512
|
this.pusherConfig = opts.pusher ?? null;
|
|
12496
12513
|
this.pollIntervalMs = opts.pollIntervalMs ?? 5e3;
|
|
12497
12514
|
this.onMessage = opts.onMessage;
|
|
@@ -12504,15 +12521,64 @@ var RealtimeGateway = class {
|
|
|
12504
12521
|
this.agentName = opts.agentName ?? null;
|
|
12505
12522
|
this.agentDisplayName = opts.agentDisplayName ?? null;
|
|
12506
12523
|
}
|
|
12524
|
+
/**
|
|
12525
|
+
* Load persisted cursors from disk. Called once before start().
|
|
12526
|
+
* Non-fatal: if file doesn't exist or is corrupt, starts fresh.
|
|
12527
|
+
*/
|
|
12528
|
+
async loadCursors() {
|
|
12529
|
+
try {
|
|
12530
|
+
const raw = await readFile(this.cursorsFile, "utf-8");
|
|
12531
|
+
const data = JSON.parse(raw);
|
|
12532
|
+
let count = 0;
|
|
12533
|
+
for (const [groupId, cursor] of Object.entries(data)) {
|
|
12534
|
+
if (typeof cursor === "string" && cursor) {
|
|
12535
|
+
this.cursors.set(groupId, cursor);
|
|
12536
|
+
count++;
|
|
12537
|
+
}
|
|
12538
|
+
}
|
|
12539
|
+
this.log.info?.(`Loaded ${count} persisted cursor(s) from disk`);
|
|
12540
|
+
} catch {
|
|
12541
|
+
this.log.debug?.("No persisted cursors found (first run or corrupt file)");
|
|
12542
|
+
}
|
|
12543
|
+
}
|
|
12544
|
+
/**
|
|
12545
|
+
* Save cursors to disk. Called after each poll cycle and on cursor updates.
|
|
12546
|
+
* Non-fatal: logs warning on failure.
|
|
12547
|
+
*/
|
|
12548
|
+
async saveCursors() {
|
|
12549
|
+
try {
|
|
12550
|
+
const data = {};
|
|
12551
|
+
for (const [groupId, cursor] of this.cursors) {
|
|
12552
|
+
data[groupId] = cursor;
|
|
12553
|
+
}
|
|
12554
|
+
await mkdir(CURSORS_DIR, { recursive: true });
|
|
12555
|
+
await writeFile(this.cursorsFile, JSON.stringify(data, null, 2), "utf-8");
|
|
12556
|
+
} catch (err) {
|
|
12557
|
+
this.log.warn?.(`Failed to persist cursors: ${err}`);
|
|
12558
|
+
}
|
|
12559
|
+
}
|
|
12560
|
+
/** Debounced cursor save — batches rapid Pusher updates into one write per 2s */
|
|
12561
|
+
debouncedSaveCursors() {
|
|
12562
|
+
if (this.cursorSaveTimer) return;
|
|
12563
|
+
this.cursorSaveTimer = setTimeout(() => {
|
|
12564
|
+
this.cursorSaveTimer = null;
|
|
12565
|
+
this.saveCursors();
|
|
12566
|
+
}, 2e3);
|
|
12567
|
+
}
|
|
12507
12568
|
/** Set bot config for a group */
|
|
12508
12569
|
setBotConfig(groupId, config) {
|
|
12509
12570
|
this.botConfigCache.set(groupId, config);
|
|
12510
12571
|
}
|
|
12511
|
-
/** Track a dispatched message ID (capped
|
|
12572
|
+
/** Track a dispatched message ID (FIFO-capped to prevent unbounded growth) */
|
|
12512
12573
|
markDispatched(messageId) {
|
|
12574
|
+
if (this.dispatched.has(messageId)) return;
|
|
12513
12575
|
this.dispatched.add(messageId);
|
|
12514
|
-
|
|
12515
|
-
|
|
12576
|
+
this.dispatchedOrder.push(messageId);
|
|
12577
|
+
if (this.dispatched.size > this.DISPATCHED_MAX) {
|
|
12578
|
+
const toEvict = this.dispatchedOrder.splice(0, this.DISPATCHED_EVICT);
|
|
12579
|
+
for (const id of toEvict) {
|
|
12580
|
+
this.dispatched.delete(id);
|
|
12581
|
+
}
|
|
12516
12582
|
}
|
|
12517
12583
|
}
|
|
12518
12584
|
/** Get bot config for a group (for outbound scope checks) */
|
|
@@ -12628,6 +12694,11 @@ var RealtimeGateway = class {
|
|
|
12628
12694
|
this.stopHeartbeat();
|
|
12629
12695
|
this.stopPolling();
|
|
12630
12696
|
this.stopPusher();
|
|
12697
|
+
if (this.cursorSaveTimer) {
|
|
12698
|
+
clearTimeout(this.cursorSaveTimer);
|
|
12699
|
+
this.cursorSaveTimer = null;
|
|
12700
|
+
}
|
|
12701
|
+
this.saveCursors();
|
|
12631
12702
|
this.log.info?.("Gateway stopped");
|
|
12632
12703
|
}
|
|
12633
12704
|
// ── Heartbeat ────────────────────────────────────────────────────
|
|
@@ -12703,7 +12774,8 @@ var RealtimeGateway = class {
|
|
|
12703
12774
|
log.error?.(`Failed to subscribe to ${channelName}:`, error);
|
|
12704
12775
|
if (!this.stopped) this.startPolling();
|
|
12705
12776
|
});
|
|
12706
|
-
setTimeout(() => {
|
|
12777
|
+
this.pusherConnectTimeout = setTimeout(() => {
|
|
12778
|
+
this.pusherConnectTimeout = null;
|
|
12707
12779
|
if (!this.pusherConnected && !this.stopped) {
|
|
12708
12780
|
log.warn?.("Pusher connection timeout (15s) \u2014 starting polling as fallback");
|
|
12709
12781
|
this.startPolling();
|
|
@@ -12791,7 +12863,9 @@ var RealtimeGateway = class {
|
|
|
12791
12863
|
}).then((shouldProcess) => {
|
|
12792
12864
|
if (!shouldProcess) return;
|
|
12793
12865
|
if (data.plaintext) {
|
|
12866
|
+
if (this.dispatched.has(data.messageId)) return;
|
|
12794
12867
|
this.cursors.set(data.groupId, data.messageId);
|
|
12868
|
+
this.debouncedSaveCursors();
|
|
12795
12869
|
this.markDispatched(data.messageId);
|
|
12796
12870
|
Promise.resolve(this.onMessage({
|
|
12797
12871
|
messageId: data.messageId,
|
|
@@ -12816,10 +12890,12 @@ var RealtimeGateway = class {
|
|
|
12816
12890
|
return;
|
|
12817
12891
|
}
|
|
12818
12892
|
log.info?.(`[e2ee:pusher] group=${data.groupId} got epoch key, decrypting...`);
|
|
12893
|
+
if (this.dispatched.has(data.messageId)) return;
|
|
12819
12894
|
try {
|
|
12820
12895
|
const plaintext = decryptGroupMessage(data.ciphertext, cached.key);
|
|
12821
12896
|
log.info?.(`[e2ee:pusher] group=${data.groupId} decrypted OK: ${plaintext?.length} chars \u2014 routing to onMessage`);
|
|
12822
12897
|
this.cursors.set(data.groupId, data.messageId);
|
|
12898
|
+
this.debouncedSaveCursors();
|
|
12823
12899
|
this.markDispatched(data.messageId);
|
|
12824
12900
|
return Promise.resolve(this.onMessage({
|
|
12825
12901
|
messageId: data.messageId,
|
|
@@ -12857,10 +12933,14 @@ var RealtimeGateway = class {
|
|
|
12857
12933
|
*/
|
|
12858
12934
|
async fetchAndRouteMessage(groupId, messageId) {
|
|
12859
12935
|
try {
|
|
12936
|
+
if (!this.cursors.has(groupId)) {
|
|
12937
|
+
await this.seekToLatest(groupId);
|
|
12938
|
+
}
|
|
12860
12939
|
const cursor = this.cursors.get(groupId);
|
|
12861
12940
|
const { messages } = await this.client.listMessages(groupId, cursor, 100);
|
|
12862
12941
|
if (messages.length > 0) {
|
|
12863
12942
|
this.cursors.set(groupId, messages[messages.length - 1].id);
|
|
12943
|
+
this.debouncedSaveCursors();
|
|
12864
12944
|
}
|
|
12865
12945
|
const msg = messages.find((m) => m.id === messageId);
|
|
12866
12946
|
if (!msg) {
|
|
@@ -12886,6 +12966,10 @@ var RealtimeGateway = class {
|
|
|
12886
12966
|
}
|
|
12887
12967
|
}
|
|
12888
12968
|
stopPusher() {
|
|
12969
|
+
if (this.pusherConnectTimeout) {
|
|
12970
|
+
clearTimeout(this.pusherConnectTimeout);
|
|
12971
|
+
this.pusherConnectTimeout = null;
|
|
12972
|
+
}
|
|
12889
12973
|
if (this.pusherClient) {
|
|
12890
12974
|
this.pusherClient.disconnect();
|
|
12891
12975
|
this.pusherClient = null;
|
|
@@ -12896,12 +12980,27 @@ var RealtimeGateway = class {
|
|
|
12896
12980
|
startPolling() {
|
|
12897
12981
|
if (this.pollTimer) return;
|
|
12898
12982
|
this.log.info?.(`Polling started: interval=${this.pollIntervalMs}ms`);
|
|
12899
|
-
this.
|
|
12900
|
-
|
|
12983
|
+
this.schedulePoll();
|
|
12984
|
+
}
|
|
12985
|
+
/** Schedule the next poll with backoff-aware delay */
|
|
12986
|
+
schedulePoll() {
|
|
12987
|
+
if (this.stopped || this.pollTimer) return;
|
|
12988
|
+
const delay = this.getBackoffDelay();
|
|
12989
|
+
this.pollTimer = setTimeout(async () => {
|
|
12990
|
+
this.pollTimer = null;
|
|
12991
|
+
await this.poll();
|
|
12992
|
+
if (!this.stopped) this.schedulePoll();
|
|
12993
|
+
}, delay);
|
|
12994
|
+
}
|
|
12995
|
+
/** Get poll delay: base interval * 2^errors, capped at MAX_BACKOFF_MS */
|
|
12996
|
+
getBackoffDelay() {
|
|
12997
|
+
if (this.consecutivePollErrors === 0) return this.pollIntervalMs;
|
|
12998
|
+
const backoff = this.pollIntervalMs * Math.pow(2, Math.min(this.consecutivePollErrors, 6));
|
|
12999
|
+
return Math.min(backoff, this.MAX_BACKOFF_MS);
|
|
12901
13000
|
}
|
|
12902
13001
|
stopPolling() {
|
|
12903
13002
|
if (this.pollTimer) {
|
|
12904
|
-
|
|
13003
|
+
clearTimeout(this.pollTimer);
|
|
12905
13004
|
this.pollTimer = null;
|
|
12906
13005
|
}
|
|
12907
13006
|
}
|
|
@@ -12976,23 +13075,51 @@ var RealtimeGateway = class {
|
|
|
12976
13075
|
const directoryIds = await this.getDirectoryGroupIds();
|
|
12977
13076
|
for (const gid of directoryIds) groupIds.add(gid);
|
|
12978
13077
|
this.log.debug?.(`Poll cycle: partner=${groups.length} directory=${directoryIds.size} total=${groupIds.size}`);
|
|
12979
|
-
|
|
12980
|
-
|
|
12981
|
-
|
|
13078
|
+
const allGroupIds = [...groupIds];
|
|
13079
|
+
for (let i = 0; i < allGroupIds.length; i += this.POLL_CONCURRENCY) {
|
|
13080
|
+
const batch = allGroupIds.slice(i, i + this.POLL_CONCURRENCY);
|
|
13081
|
+
await Promise.allSettled(batch.map((gid) => this.pollGroup(gid)));
|
|
13082
|
+
}
|
|
13083
|
+
await this.saveCursors();
|
|
13084
|
+
this.consecutivePollErrors = 0;
|
|
12982
13085
|
} catch (error) {
|
|
12983
|
-
this.
|
|
13086
|
+
this.consecutivePollErrors++;
|
|
13087
|
+
const nextDelay = this.getBackoffDelay();
|
|
13088
|
+
this.log.error?.(`Poll cycle failed (${this.consecutivePollErrors} consecutive, next in ${nextDelay}ms):`, error);
|
|
12984
13089
|
} finally {
|
|
12985
13090
|
this.polling = false;
|
|
12986
13091
|
}
|
|
12987
13092
|
}
|
|
13093
|
+
/**
|
|
13094
|
+
* Seek cursor to the latest message without processing anything.
|
|
13095
|
+
* Called on cold start (no cursor for a group) to avoid replaying history.
|
|
13096
|
+
* Paginates forward until there are no more messages.
|
|
13097
|
+
*/
|
|
13098
|
+
async seekToLatest(groupId) {
|
|
13099
|
+
let cursor;
|
|
13100
|
+
let hasMore = true;
|
|
13101
|
+
while (hasMore) {
|
|
13102
|
+
const result = await this.client.listMessages(groupId, cursor, 100);
|
|
13103
|
+
if (result.messages.length === 0) break;
|
|
13104
|
+
cursor = result.messages[result.messages.length - 1].id;
|
|
13105
|
+
hasMore = result.hasMore;
|
|
13106
|
+
}
|
|
13107
|
+
if (cursor) {
|
|
13108
|
+
this.cursors.set(groupId, cursor);
|
|
13109
|
+
this.log.info?.(`Seeked to latest in group ${groupId}: cursor=${cursor}`);
|
|
13110
|
+
}
|
|
13111
|
+
}
|
|
12988
13112
|
async pollGroup(groupId) {
|
|
12989
13113
|
try {
|
|
12990
13114
|
const cursor = this.cursors.get(groupId);
|
|
13115
|
+
if (!cursor) {
|
|
13116
|
+
await this.seekToLatest(groupId);
|
|
13117
|
+
return;
|
|
13118
|
+
}
|
|
12991
13119
|
const { messages } = await this.client.listMessages(groupId, cursor);
|
|
12992
13120
|
if (messages.length === 0) return;
|
|
12993
13121
|
const lastMessage = messages[messages.length - 1];
|
|
12994
13122
|
this.cursors.set(groupId, lastMessage.id);
|
|
12995
|
-
if (!cursor) return;
|
|
12996
13123
|
for (const msg of messages) {
|
|
12997
13124
|
if (msg.senderIdentityId === this.botIdentityId) continue;
|
|
12998
13125
|
let plaintext = msg.plaintext;
|
|
@@ -13147,7 +13274,7 @@ async function readMdAttachments(mediaUrls, source, log, accountId) {
|
|
|
13147
13274
|
const filename = basename(url);
|
|
13148
13275
|
if (!filename.endsWith(".md")) continue;
|
|
13149
13276
|
try {
|
|
13150
|
-
const content = await
|
|
13277
|
+
const content = await readFile2(url, "utf-8");
|
|
13151
13278
|
if (content.length > 1024 * 1024) {
|
|
13152
13279
|
log?.warn?.(`[${accountId}] Quiubo: skipping ${filename} \u2014 exceeds 1MB`);
|
|
13153
13280
|
continue;
|
|
@@ -13554,6 +13681,12 @@ var quiuboPlugin = {
|
|
|
13554
13681
|
log?.info?.(`[${accountId}] Quiubo: disabled, skipping`);
|
|
13555
13682
|
return;
|
|
13556
13683
|
}
|
|
13684
|
+
const existingGateway = gateways.get(accountId);
|
|
13685
|
+
if (existingGateway) {
|
|
13686
|
+
log?.warn?.(`[${accountId}] Quiubo: stopping existing gateway before restart`);
|
|
13687
|
+
existingGateway.stop();
|
|
13688
|
+
gateways.delete(accountId);
|
|
13689
|
+
}
|
|
13557
13690
|
const apiUrl = quiuboConfig.apiUrl ?? DEFAULT_API_URL;
|
|
13558
13691
|
const { apiKey, botIdentityId, pollIntervalMs } = quiuboConfig;
|
|
13559
13692
|
const runtime2 = getQuiuboRuntime();
|
|
@@ -13696,6 +13829,7 @@ var quiuboPlugin = {
|
|
|
13696
13829
|
const gateway = new RealtimeGateway({
|
|
13697
13830
|
client,
|
|
13698
13831
|
botIdentityId,
|
|
13832
|
+
accountId,
|
|
13699
13833
|
pusher: pusherConfig,
|
|
13700
13834
|
pollIntervalMs: pollIntervalMs ?? 5e3,
|
|
13701
13835
|
log: gatewayLog,
|
|
@@ -13880,6 +14014,7 @@ var quiuboPlugin = {
|
|
|
13880
14014
|
} catch (err) {
|
|
13881
14015
|
log?.warn?.(`[${accountId}] Quiubo: failed to hydrate bot config (non-fatal): ${err}`);
|
|
13882
14016
|
}
|
|
14017
|
+
await gateway.loadCursors();
|
|
13883
14018
|
await gateway.start();
|
|
13884
14019
|
log?.info?.(`[${accountId}] Quiubo: gateway started`);
|
|
13885
14020
|
return new Promise((resolve) => {
|
|
@@ -13916,12 +14051,12 @@ function resolveOutboundGroupId(ctx) {
|
|
|
13916
14051
|
}
|
|
13917
14052
|
async function getActivityData(runtime2, log, agentId) {
|
|
13918
14053
|
try {
|
|
13919
|
-
const { readFile:
|
|
13920
|
-
const { join } = await import("node:path");
|
|
14054
|
+
const { readFile: readFile3 } = await import("node:fs/promises");
|
|
14055
|
+
const { join: join2 } = await import("node:path");
|
|
13921
14056
|
const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
13922
|
-
const cronPath =
|
|
14057
|
+
const cronPath = join2(homeDir, ".openclaw", "cron", "jobs.json");
|
|
13923
14058
|
log?.info?.(`getActivityData: reading ${cronPath} (agentId=${agentId})`);
|
|
13924
|
-
const raw = await
|
|
14059
|
+
const raw = await readFile3(cronPath, "utf-8");
|
|
13925
14060
|
const parsed = JSON.parse(raw);
|
|
13926
14061
|
const jobs = parsed?.jobs ?? [];
|
|
13927
14062
|
const items = jobs.filter((j) => j.enabled !== false && (!agentId || j.agentId === agentId)).map((job) => ({
|