openclaw-quiubo 2.6.13 → 2.6.15
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 +130 -18
- package/dist/index.js.map +3 -3
- package/dist/src/channel.d.ts.map +1 -1
- package/dist/src/realtime-gateway.d.ts +26 -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,8 @@ 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");
|
|
12454
|
+
var CURSORS_FILE = join(CURSORS_DIR, "quiubo-cursors.json");
|
|
12451
12455
|
var consoleLogger = {
|
|
12452
12456
|
debug: (...args) => console.debug("[Quiubo]", ...args),
|
|
12453
12457
|
info: (...args) => console.log("[Quiubo]", ...args),
|
|
@@ -12470,8 +12474,16 @@ var RealtimeGateway = class {
|
|
|
12470
12474
|
pollTimer = null;
|
|
12471
12475
|
polling = false;
|
|
12472
12476
|
cursors = /* @__PURE__ */ new Map();
|
|
12473
|
-
/** Message IDs already dispatched (Pusher + poll dedup).
|
|
12477
|
+
/** Message IDs already dispatched (Pusher + poll dedup). FIFO-capped to prevent unbounded growth. */
|
|
12474
12478
|
dispatched = /* @__PURE__ */ new Set();
|
|
12479
|
+
dispatchedOrder = [];
|
|
12480
|
+
DISPATCHED_MAX = 500;
|
|
12481
|
+
DISPATCHED_EVICT = 100;
|
|
12482
|
+
// Backoff state for poll failures
|
|
12483
|
+
consecutivePollErrors = 0;
|
|
12484
|
+
POLL_CONCURRENCY = 5;
|
|
12485
|
+
MAX_BACKOFF_MS = 6e4;
|
|
12486
|
+
// cap at 60s
|
|
12475
12487
|
// Cached directory group IDs from listAgentGroups (refreshed every 60s)
|
|
12476
12488
|
agentGroupsCache = null;
|
|
12477
12489
|
AGENT_GROUPS_TTL = 6e4;
|
|
@@ -12482,6 +12494,10 @@ var RealtimeGateway = class {
|
|
|
12482
12494
|
// Heartbeat state
|
|
12483
12495
|
agentId;
|
|
12484
12496
|
heartbeatTimer = null;
|
|
12497
|
+
// Cursor save debounce (avoids disk thrash on rapid Pusher events)
|
|
12498
|
+
cursorSaveTimer = null;
|
|
12499
|
+
// Pusher connection timeout (cancelled on stop to prevent stale firing)
|
|
12500
|
+
pusherConnectTimeout = null;
|
|
12485
12501
|
// E2EE state
|
|
12486
12502
|
keyManager;
|
|
12487
12503
|
e2eeGrantedGroups;
|
|
@@ -12504,15 +12520,64 @@ var RealtimeGateway = class {
|
|
|
12504
12520
|
this.agentName = opts.agentName ?? null;
|
|
12505
12521
|
this.agentDisplayName = opts.agentDisplayName ?? null;
|
|
12506
12522
|
}
|
|
12523
|
+
/**
|
|
12524
|
+
* Load persisted cursors from disk. Called once before start().
|
|
12525
|
+
* Non-fatal: if file doesn't exist or is corrupt, starts fresh.
|
|
12526
|
+
*/
|
|
12527
|
+
async loadCursors() {
|
|
12528
|
+
try {
|
|
12529
|
+
const raw = await readFile(CURSORS_FILE, "utf-8");
|
|
12530
|
+
const data = JSON.parse(raw);
|
|
12531
|
+
let count = 0;
|
|
12532
|
+
for (const [groupId, cursor] of Object.entries(data)) {
|
|
12533
|
+
if (typeof cursor === "string" && cursor) {
|
|
12534
|
+
this.cursors.set(groupId, cursor);
|
|
12535
|
+
count++;
|
|
12536
|
+
}
|
|
12537
|
+
}
|
|
12538
|
+
this.log.info?.(`Loaded ${count} persisted cursor(s) from disk`);
|
|
12539
|
+
} catch {
|
|
12540
|
+
this.log.debug?.("No persisted cursors found (first run or corrupt file)");
|
|
12541
|
+
}
|
|
12542
|
+
}
|
|
12543
|
+
/**
|
|
12544
|
+
* Save cursors to disk. Called after each poll cycle and on cursor updates.
|
|
12545
|
+
* Non-fatal: logs warning on failure.
|
|
12546
|
+
*/
|
|
12547
|
+
async saveCursors() {
|
|
12548
|
+
try {
|
|
12549
|
+
const data = {};
|
|
12550
|
+
for (const [groupId, cursor] of this.cursors) {
|
|
12551
|
+
data[groupId] = cursor;
|
|
12552
|
+
}
|
|
12553
|
+
await mkdir(CURSORS_DIR, { recursive: true });
|
|
12554
|
+
await writeFile(CURSORS_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
12555
|
+
} catch (err) {
|
|
12556
|
+
this.log.warn?.(`Failed to persist cursors: ${err}`);
|
|
12557
|
+
}
|
|
12558
|
+
}
|
|
12559
|
+
/** Debounced cursor save — batches rapid Pusher updates into one write per 2s */
|
|
12560
|
+
debouncedSaveCursors() {
|
|
12561
|
+
if (this.cursorSaveTimer) return;
|
|
12562
|
+
this.cursorSaveTimer = setTimeout(() => {
|
|
12563
|
+
this.cursorSaveTimer = null;
|
|
12564
|
+
this.saveCursors();
|
|
12565
|
+
}, 2e3);
|
|
12566
|
+
}
|
|
12507
12567
|
/** Set bot config for a group */
|
|
12508
12568
|
setBotConfig(groupId, config) {
|
|
12509
12569
|
this.botConfigCache.set(groupId, config);
|
|
12510
12570
|
}
|
|
12511
|
-
/** Track a dispatched message ID (capped
|
|
12571
|
+
/** Track a dispatched message ID (FIFO-capped to prevent unbounded growth) */
|
|
12512
12572
|
markDispatched(messageId) {
|
|
12573
|
+
if (this.dispatched.has(messageId)) return;
|
|
12513
12574
|
this.dispatched.add(messageId);
|
|
12514
|
-
|
|
12515
|
-
|
|
12575
|
+
this.dispatchedOrder.push(messageId);
|
|
12576
|
+
if (this.dispatched.size > this.DISPATCHED_MAX) {
|
|
12577
|
+
const toEvict = this.dispatchedOrder.splice(0, this.DISPATCHED_EVICT);
|
|
12578
|
+
for (const id of toEvict) {
|
|
12579
|
+
this.dispatched.delete(id);
|
|
12580
|
+
}
|
|
12516
12581
|
}
|
|
12517
12582
|
}
|
|
12518
12583
|
/** Get bot config for a group (for outbound scope checks) */
|
|
@@ -12628,6 +12693,11 @@ var RealtimeGateway = class {
|
|
|
12628
12693
|
this.stopHeartbeat();
|
|
12629
12694
|
this.stopPolling();
|
|
12630
12695
|
this.stopPusher();
|
|
12696
|
+
if (this.cursorSaveTimer) {
|
|
12697
|
+
clearTimeout(this.cursorSaveTimer);
|
|
12698
|
+
this.cursorSaveTimer = null;
|
|
12699
|
+
}
|
|
12700
|
+
this.saveCursors();
|
|
12631
12701
|
this.log.info?.("Gateway stopped");
|
|
12632
12702
|
}
|
|
12633
12703
|
// ── Heartbeat ────────────────────────────────────────────────────
|
|
@@ -12703,7 +12773,8 @@ var RealtimeGateway = class {
|
|
|
12703
12773
|
log.error?.(`Failed to subscribe to ${channelName}:`, error);
|
|
12704
12774
|
if (!this.stopped) this.startPolling();
|
|
12705
12775
|
});
|
|
12706
|
-
setTimeout(() => {
|
|
12776
|
+
this.pusherConnectTimeout = setTimeout(() => {
|
|
12777
|
+
this.pusherConnectTimeout = null;
|
|
12707
12778
|
if (!this.pusherConnected && !this.stopped) {
|
|
12708
12779
|
log.warn?.("Pusher connection timeout (15s) \u2014 starting polling as fallback");
|
|
12709
12780
|
this.startPolling();
|
|
@@ -12791,7 +12862,9 @@ var RealtimeGateway = class {
|
|
|
12791
12862
|
}).then((shouldProcess) => {
|
|
12792
12863
|
if (!shouldProcess) return;
|
|
12793
12864
|
if (data.plaintext) {
|
|
12865
|
+
if (this.dispatched.has(data.messageId)) return;
|
|
12794
12866
|
this.cursors.set(data.groupId, data.messageId);
|
|
12867
|
+
this.debouncedSaveCursors();
|
|
12795
12868
|
this.markDispatched(data.messageId);
|
|
12796
12869
|
Promise.resolve(this.onMessage({
|
|
12797
12870
|
messageId: data.messageId,
|
|
@@ -12816,10 +12889,12 @@ var RealtimeGateway = class {
|
|
|
12816
12889
|
return;
|
|
12817
12890
|
}
|
|
12818
12891
|
log.info?.(`[e2ee:pusher] group=${data.groupId} got epoch key, decrypting...`);
|
|
12892
|
+
if (this.dispatched.has(data.messageId)) return;
|
|
12819
12893
|
try {
|
|
12820
12894
|
const plaintext = decryptGroupMessage(data.ciphertext, cached.key);
|
|
12821
12895
|
log.info?.(`[e2ee:pusher] group=${data.groupId} decrypted OK: ${plaintext?.length} chars \u2014 routing to onMessage`);
|
|
12822
12896
|
this.cursors.set(data.groupId, data.messageId);
|
|
12897
|
+
this.debouncedSaveCursors();
|
|
12823
12898
|
this.markDispatched(data.messageId);
|
|
12824
12899
|
return Promise.resolve(this.onMessage({
|
|
12825
12900
|
messageId: data.messageId,
|
|
@@ -12861,9 +12936,14 @@ var RealtimeGateway = class {
|
|
|
12861
12936
|
const { messages } = await this.client.listMessages(groupId, cursor, 100);
|
|
12862
12937
|
if (messages.length > 0) {
|
|
12863
12938
|
this.cursors.set(groupId, messages[messages.length - 1].id);
|
|
12939
|
+
this.debouncedSaveCursors();
|
|
12864
12940
|
}
|
|
12865
12941
|
const msg = messages.find((m) => m.id === messageId);
|
|
12866
12942
|
if (!msg) {
|
|
12943
|
+
if (!cursor) {
|
|
12944
|
+
this.log.info?.(`fetchAndRoute: no cursor for group ${groupId}, caught up to latest (skip history)`);
|
|
12945
|
+
return;
|
|
12946
|
+
}
|
|
12867
12947
|
this.log.warn?.(`Message ${messageId} not found in group ${groupId} (fetched ${messages.length} from cursor)`);
|
|
12868
12948
|
return;
|
|
12869
12949
|
}
|
|
@@ -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,11 +13075,17 @@ 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
|
}
|
|
@@ -13147,7 +13252,7 @@ async function readMdAttachments(mediaUrls, source, log, accountId) {
|
|
|
13147
13252
|
const filename = basename(url);
|
|
13148
13253
|
if (!filename.endsWith(".md")) continue;
|
|
13149
13254
|
try {
|
|
13150
|
-
const content = await
|
|
13255
|
+
const content = await readFile2(url, "utf-8");
|
|
13151
13256
|
if (content.length > 1024 * 1024) {
|
|
13152
13257
|
log?.warn?.(`[${accountId}] Quiubo: skipping ${filename} \u2014 exceeds 1MB`);
|
|
13153
13258
|
continue;
|
|
@@ -13554,6 +13659,12 @@ var quiuboPlugin = {
|
|
|
13554
13659
|
log?.info?.(`[${accountId}] Quiubo: disabled, skipping`);
|
|
13555
13660
|
return;
|
|
13556
13661
|
}
|
|
13662
|
+
const existingGateway = gateways.get(accountId);
|
|
13663
|
+
if (existingGateway) {
|
|
13664
|
+
log?.warn?.(`[${accountId}] Quiubo: stopping existing gateway before restart`);
|
|
13665
|
+
existingGateway.stop();
|
|
13666
|
+
gateways.delete(accountId);
|
|
13667
|
+
}
|
|
13557
13668
|
const apiUrl = quiuboConfig.apiUrl ?? DEFAULT_API_URL;
|
|
13558
13669
|
const { apiKey, botIdentityId, pollIntervalMs } = quiuboConfig;
|
|
13559
13670
|
const runtime2 = getQuiuboRuntime();
|
|
@@ -13880,6 +13991,7 @@ var quiuboPlugin = {
|
|
|
13880
13991
|
} catch (err) {
|
|
13881
13992
|
log?.warn?.(`[${accountId}] Quiubo: failed to hydrate bot config (non-fatal): ${err}`);
|
|
13882
13993
|
}
|
|
13994
|
+
await gateway.loadCursors();
|
|
13883
13995
|
await gateway.start();
|
|
13884
13996
|
log?.info?.(`[${accountId}] Quiubo: gateway started`);
|
|
13885
13997
|
return new Promise((resolve) => {
|
|
@@ -13916,12 +14028,12 @@ function resolveOutboundGroupId(ctx) {
|
|
|
13916
14028
|
}
|
|
13917
14029
|
async function getActivityData(runtime2, log, agentId) {
|
|
13918
14030
|
try {
|
|
13919
|
-
const { readFile:
|
|
13920
|
-
const { join } = await import("node:path");
|
|
14031
|
+
const { readFile: readFile3 } = await import("node:fs/promises");
|
|
14032
|
+
const { join: join2 } = await import("node:path");
|
|
13921
14033
|
const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
13922
|
-
const cronPath =
|
|
14034
|
+
const cronPath = join2(homeDir, ".openclaw", "cron", "jobs.json");
|
|
13923
14035
|
log?.info?.(`getActivityData: reading ${cronPath} (agentId=${agentId})`);
|
|
13924
|
-
const raw = await
|
|
14036
|
+
const raw = await readFile3(cronPath, "utf-8");
|
|
13925
14037
|
const parsed = JSON.parse(raw);
|
|
13926
14038
|
const jobs = parsed?.jobs ?? [];
|
|
13927
14039
|
const items = jobs.filter((j) => j.enabled !== false && (!agentId || j.agentId === agentId)).map((job) => ({
|