@syengup/friday-channel-next 0.0.35 → 0.0.38
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.d.ts +4 -0
- package/dist/index.js +182 -0
- package/dist/src/agent/abort-run.d.ts +1 -0
- package/dist/src/agent/abort-run.js +11 -0
- package/dist/src/agent/active-runs.d.ts +9 -0
- package/dist/src/agent/active-runs.js +20 -0
- package/dist/src/agent/dispatch-bridge.d.ts +5 -0
- package/dist/src/agent/dispatch-bridge.js +12 -0
- package/dist/src/agent/media-bridge.d.ts +4 -0
- package/dist/src/agent/media-bridge.js +21 -0
- package/dist/src/agent/subagent-registry.d.ts +68 -0
- package/dist/src/agent/subagent-registry.js +142 -0
- package/dist/src/agent-forward-runtime.d.ts +17 -0
- package/dist/src/agent-forward-runtime.js +16 -0
- package/dist/src/agent-run-context-bridge.d.ts +13 -0
- package/dist/src/agent-run-context-bridge.js +23 -0
- package/dist/src/channel-actions.d.ts +13 -0
- package/dist/src/channel-actions.js +101 -0
- package/dist/src/channel.d.ts +6 -0
- package/dist/src/channel.js +248 -0
- package/dist/src/collect-message-media-paths.d.ts +11 -0
- package/dist/src/collect-message-media-paths.js +143 -0
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +39 -0
- package/dist/src/friday-inbound-stats.d.ts +2 -0
- package/dist/src/friday-inbound-stats.js +8 -0
- package/dist/src/friday-session.d.ts +40 -0
- package/dist/src/friday-session.js +395 -0
- package/dist/src/host-config.d.ts +1 -0
- package/dist/src/host-config.js +15 -0
- package/dist/src/http/handlers/cancel.d.ts +2 -0
- package/dist/src/http/handlers/cancel.js +33 -0
- package/dist/src/http/handlers/device-approve.d.ts +2 -0
- package/dist/src/http/handlers/device-approve.js +125 -0
- package/dist/src/http/handlers/files-download.d.ts +10 -0
- package/dist/src/http/handlers/files-download.js +210 -0
- package/dist/src/http/handlers/files-upload.d.ts +8 -0
- package/dist/src/http/handlers/files-upload.js +136 -0
- package/dist/src/http/handlers/files.d.ts +75 -0
- package/dist/src/http/handlers/files.js +305 -0
- package/dist/src/http/handlers/messages.d.ts +34 -0
- package/dist/src/http/handlers/messages.js +476 -0
- package/dist/src/http/handlers/models-list.d.ts +10 -0
- package/dist/src/http/handlers/models-list.js +113 -0
- package/dist/src/http/handlers/nodes-approve.d.ts +2 -0
- package/dist/src/http/handlers/nodes-approve.js +146 -0
- package/dist/src/http/handlers/sessions-delete.d.ts +2 -0
- package/dist/src/http/handlers/sessions-delete.js +49 -0
- package/dist/src/http/handlers/sessions-settings.d.ts +2 -0
- package/dist/src/http/handlers/sessions-settings.js +71 -0
- package/dist/src/http/handlers/sse.d.ts +2 -0
- package/dist/src/http/handlers/sse.js +70 -0
- package/dist/src/http/handlers/status.d.ts +2 -0
- package/dist/src/http/handlers/status.js +29 -0
- package/dist/src/http/middleware/auth.d.ts +13 -0
- package/dist/src/http/middleware/auth.js +29 -0
- package/dist/src/http/middleware/body.d.ts +2 -0
- package/dist/src/http/middleware/body.js +24 -0
- package/dist/src/http/middleware/cors.d.ts +2 -0
- package/dist/src/http/middleware/cors.js +11 -0
- package/dist/src/http/server.d.ts +19 -0
- package/dist/src/http/server.js +87 -0
- package/dist/src/logging.d.ts +7 -0
- package/dist/src/logging.js +28 -0
- package/dist/src/run-metadata.d.ts +25 -0
- package/dist/src/run-metadata.js +139 -0
- package/dist/src/runtime.d.ts +13 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/session/session-manager.d.ts +22 -0
- package/dist/src/session/session-manager.js +190 -0
- package/dist/src/session-usage-snapshot.d.ts +23 -0
- package/dist/src/session-usage-snapshot.js +65 -0
- package/dist/src/sse/emitter.d.ts +59 -0
- package/dist/src/sse/emitter.js +219 -0
- package/dist/src/sse/offline-queue.d.ts +26 -0
- package/dist/src/sse/offline-queue.js +134 -0
- package/dist/src/vendor/runtime-store.d.ts +26 -0
- package/dist/src/vendor/runtime-store.js +60 -0
- package/index.ts +10 -4
- package/package.json +11 -10
- package/src/agent/subagent-registry.ts +195 -0
- package/src/channel.ts +6 -4
- package/src/e2e/subagent-smoke.e2e.test.ts +223 -0
- package/src/e2e/subagent.e2e.test.ts +502 -0
- package/src/friday-session.ts +140 -1
- package/src/http/handlers/device-approve.test.ts +0 -1
- package/src/http/handlers/device-approve.ts +0 -2
- package/src/http/handlers/files-download.ts +4 -1
- package/src/http/handlers/files.ts +7 -4
- package/src/http/handlers/messages.ts +54 -4
- package/src/http/handlers/models-list.ts +24 -2
- package/src/http/handlers/nodes-approve.test.ts +288 -0
- package/src/http/handlers/nodes-approve.ts +189 -0
- package/src/http/server.ts +5 -0
- package/src/openclaw.d.ts +5 -0
- package/src/sse/emitter.ts +1 -1
- package/src/test-support/mock-runtime.ts +2 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createFridayNextLogger } from "../logging.js";
|
|
2
|
+
import { fridaySseOfflineQueue } from "./offline-queue.js";
|
|
3
|
+
const logger = createFridayNextLogger("sse", "info");
|
|
4
|
+
export class SseConnection {
|
|
5
|
+
deviceId;
|
|
6
|
+
res;
|
|
7
|
+
closed = false;
|
|
8
|
+
pending = [];
|
|
9
|
+
flushTimer = null;
|
|
10
|
+
waitingDrain = false;
|
|
11
|
+
constructor(deviceId, res) {
|
|
12
|
+
this.deviceId = deviceId;
|
|
13
|
+
this.res = res;
|
|
14
|
+
this.res.on("drain", () => {
|
|
15
|
+
this.waitingDrain = false;
|
|
16
|
+
this.scheduleFlush();
|
|
17
|
+
});
|
|
18
|
+
this.res.on("error", () => this.close());
|
|
19
|
+
}
|
|
20
|
+
send(entry, flushNow) {
|
|
21
|
+
if (this.closed)
|
|
22
|
+
return;
|
|
23
|
+
const normalized = "id" in entry && "event" in entry ? entry : { id: Date.now(), event: entry };
|
|
24
|
+
const payload = JSON.stringify(normalized.event.data);
|
|
25
|
+
this.pending.push(`id: ${normalized.id}\nevent: ${normalized.event.type}\ndata: ${payload}\n\n`);
|
|
26
|
+
if (flushNow) {
|
|
27
|
+
if (this.flushTimer)
|
|
28
|
+
clearTimeout(this.flushTimer);
|
|
29
|
+
this.flushTimer = null;
|
|
30
|
+
this.flush();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.scheduleFlush();
|
|
34
|
+
}
|
|
35
|
+
sendRaw(line) {
|
|
36
|
+
if (this.closed)
|
|
37
|
+
return;
|
|
38
|
+
const ok = this.res.write(line);
|
|
39
|
+
if (ok === false)
|
|
40
|
+
this.waitingDrain = true;
|
|
41
|
+
}
|
|
42
|
+
scheduleFlush() {
|
|
43
|
+
if (this.waitingDrain || this.flushTimer)
|
|
44
|
+
return;
|
|
45
|
+
this.flushTimer = setTimeout(() => {
|
|
46
|
+
this.flushTimer = null;
|
|
47
|
+
this.flush();
|
|
48
|
+
}, 16);
|
|
49
|
+
}
|
|
50
|
+
flush() {
|
|
51
|
+
if (this.closed || this.waitingDrain || this.pending.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
const data = this.pending.join("");
|
|
54
|
+
this.pending = [];
|
|
55
|
+
const ok = this.res.write(data);
|
|
56
|
+
if (ok === false)
|
|
57
|
+
this.waitingDrain = true;
|
|
58
|
+
}
|
|
59
|
+
close() {
|
|
60
|
+
if (this.closed)
|
|
61
|
+
return;
|
|
62
|
+
this.closed = true;
|
|
63
|
+
if (this.flushTimer)
|
|
64
|
+
clearTimeout(this.flushTimer);
|
|
65
|
+
this.flushTimer = null;
|
|
66
|
+
this.pending = [];
|
|
67
|
+
try {
|
|
68
|
+
this.res.end();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// ignore
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
get isClosed() {
|
|
75
|
+
return this.closed;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
class SseEmitterRegistry {
|
|
79
|
+
connections = new Map();
|
|
80
|
+
runEmitter = new Map();
|
|
81
|
+
lastRunIdByDevice = new Map();
|
|
82
|
+
eventSeqByDevice = new Map();
|
|
83
|
+
backlogLimit = 200;
|
|
84
|
+
getConnectionCount() {
|
|
85
|
+
return this.connections.size;
|
|
86
|
+
}
|
|
87
|
+
setBacklogLimit(limit) {
|
|
88
|
+
this.backlogLimit = Math.max(0, Math.floor(limit));
|
|
89
|
+
}
|
|
90
|
+
getBacklogLimit() {
|
|
91
|
+
return this.backlogLimit;
|
|
92
|
+
}
|
|
93
|
+
/** Last persisted / assigned SSE id for device (for `connected.lastSeq`). */
|
|
94
|
+
latestSeqForDevice(deviceId) {
|
|
95
|
+
const key = deviceId.trim().toUpperCase();
|
|
96
|
+
const disk = fridaySseOfflineQueue.latestId(key);
|
|
97
|
+
const mem = this.eventSeqByDevice.get(key) ?? 0;
|
|
98
|
+
return Math.max(disk, mem);
|
|
99
|
+
}
|
|
100
|
+
addConnection(deviceId, res) {
|
|
101
|
+
const normalized = deviceId.trim().toUpperCase();
|
|
102
|
+
const existing = this.connections.get(normalized);
|
|
103
|
+
if (existing && !existing.isClosed) {
|
|
104
|
+
existing.close();
|
|
105
|
+
for (const set of this.runEmitter.values()) {
|
|
106
|
+
set.delete(normalized);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const conn = new SseConnection(normalized, res);
|
|
110
|
+
this.connections.set(normalized, conn);
|
|
111
|
+
logger.info(`connect ${normalized} total=${this.connections.size}`);
|
|
112
|
+
return conn;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* @param expectedConn When provided, only removes if this connection is still the active one
|
|
116
|
+
* (avoids stale `req.close` after a reconnect replaced the map entry).
|
|
117
|
+
*/
|
|
118
|
+
removeConnection(deviceId, expectedConn) {
|
|
119
|
+
const normalized = deviceId.trim().toUpperCase();
|
|
120
|
+
const current = this.connections.get(normalized);
|
|
121
|
+
if (expectedConn !== undefined && current !== expectedConn) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
current?.close();
|
|
125
|
+
this.connections.delete(normalized);
|
|
126
|
+
for (const set of this.runEmitter.values())
|
|
127
|
+
set.delete(normalized);
|
|
128
|
+
logger.info(`disconnect ${normalized} total=${this.connections.size}`);
|
|
129
|
+
}
|
|
130
|
+
getConnection(deviceId) {
|
|
131
|
+
return this.connections.get(deviceId.trim().toUpperCase());
|
|
132
|
+
}
|
|
133
|
+
nextEntry(deviceId, event) {
|
|
134
|
+
const key = deviceId.trim().toUpperCase();
|
|
135
|
+
const diskMax = fridaySseOfflineQueue.latestId(key);
|
|
136
|
+
const memMax = this.eventSeqByDevice.get(key) ?? 0;
|
|
137
|
+
const last = Math.max(memMax, diskMax);
|
|
138
|
+
const id = last + 1;
|
|
139
|
+
this.eventSeqByDevice.set(key, id);
|
|
140
|
+
fridaySseOfflineQueue.append(key, id, event.type, event.data, this.backlogLimit);
|
|
141
|
+
return { id, event };
|
|
142
|
+
}
|
|
143
|
+
replayBacklog(deviceId, afterEventId) {
|
|
144
|
+
const key = deviceId.trim().toUpperCase();
|
|
145
|
+
const conn = this.connections.get(key);
|
|
146
|
+
if (!conn)
|
|
147
|
+
return 0;
|
|
148
|
+
const entries = fridaySseOfflineQueue.readAfter(key, afterEventId);
|
|
149
|
+
let count = 0;
|
|
150
|
+
for (const e of entries) {
|
|
151
|
+
conn.send({ id: e.id, event: { type: e.event, data: e.data } }, true);
|
|
152
|
+
count += 1;
|
|
153
|
+
}
|
|
154
|
+
return count;
|
|
155
|
+
}
|
|
156
|
+
broadcast(event, deviceId, flushNow) {
|
|
157
|
+
if (deviceId) {
|
|
158
|
+
const key = deviceId.trim().toUpperCase();
|
|
159
|
+
const entry = this.nextEntry(key, event);
|
|
160
|
+
this.connections.get(key)?.send(entry, flushNow);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
for (const conn of this.connections.values()) {
|
|
164
|
+
const entry = this.nextEntry(conn.deviceId, event);
|
|
165
|
+
conn.send(entry, flushNow);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
trackDeviceForRun(deviceId, runId) {
|
|
169
|
+
const key = deviceId.trim().toUpperCase();
|
|
170
|
+
const set = this.runEmitter.get(runId) ?? new Set();
|
|
171
|
+
set.add(key);
|
|
172
|
+
this.runEmitter.set(runId, set);
|
|
173
|
+
this.lastRunIdByDevice.set(key, runId);
|
|
174
|
+
}
|
|
175
|
+
untrackRun(runId) {
|
|
176
|
+
this.runEmitter.delete(runId);
|
|
177
|
+
}
|
|
178
|
+
hasTrackedDevices(runId) {
|
|
179
|
+
return (this.runEmitter.get(runId)?.size ?? 0) > 0;
|
|
180
|
+
}
|
|
181
|
+
getDeviceIdByRunId(runId) {
|
|
182
|
+
const first = this.runEmitter.get(runId)?.values().next().value;
|
|
183
|
+
return typeof first === "string" ? first : null;
|
|
184
|
+
}
|
|
185
|
+
getSoleConnectedDeviceId() {
|
|
186
|
+
if (this.connections.size !== 1)
|
|
187
|
+
return null;
|
|
188
|
+
return this.connections.keys().next().value ?? null;
|
|
189
|
+
}
|
|
190
|
+
getLastRunIdForDevice(deviceId) {
|
|
191
|
+
return this.lastRunIdByDevice.get(deviceId.trim().toUpperCase()) ?? null;
|
|
192
|
+
}
|
|
193
|
+
broadcastToRun(runId, event, flushNow) {
|
|
194
|
+
const direct = typeof event.data.deviceId === "string" ? event.data.deviceId : "";
|
|
195
|
+
if (direct.trim()) {
|
|
196
|
+
this.broadcast(event, direct, flushNow);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const set = this.runEmitter.get(runId);
|
|
200
|
+
if (!set || set.size === 0)
|
|
201
|
+
return;
|
|
202
|
+
for (const deviceId of set)
|
|
203
|
+
this.broadcast(event, deviceId, flushNow);
|
|
204
|
+
}
|
|
205
|
+
broadcastToolEvent(deviceId, runId, event, flushNow) {
|
|
206
|
+
this.trackDeviceForRun(deviceId, runId);
|
|
207
|
+
this.broadcastToRun(runId, event, flushNow ?? true);
|
|
208
|
+
}
|
|
209
|
+
/** Vitest / e2e: drop connections and in-memory seq maps (does not delete disk queue files). */
|
|
210
|
+
resetForTest() {
|
|
211
|
+
for (const c of this.connections.values())
|
|
212
|
+
c.close();
|
|
213
|
+
this.connections.clear();
|
|
214
|
+
this.runEmitter.clear();
|
|
215
|
+
this.lastRunIdByDevice.clear();
|
|
216
|
+
this.eventSeqByDevice.clear();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export const sseEmitter = new SseEmitterRegistry();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type PersistedSseEntry = {
|
|
2
|
+
id: number;
|
|
3
|
+
event: string;
|
|
4
|
+
data: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
export declare function setOfflineQueueBaseDirForTest(dir: string | null): void;
|
|
7
|
+
export declare function resolveFridayNextEventsQueueDir(): string;
|
|
8
|
+
/**
|
|
9
|
+
* Per-device JSONL persistence for SSE replay.
|
|
10
|
+
* `overrideBaseDir` is for tests; production uses `resolveFridayNextEventsQueueDir()`.
|
|
11
|
+
*/
|
|
12
|
+
export declare class FridaySseOfflineQueue {
|
|
13
|
+
private readonly overrideBaseDir;
|
|
14
|
+
constructor(overrideBaseDir?: string | null);
|
|
15
|
+
private baseDir;
|
|
16
|
+
private devicePath;
|
|
17
|
+
private ensureDir;
|
|
18
|
+
/** Highest id in file (full scan; ok for bounded backlog). */
|
|
19
|
+
scanMaxId(deviceId: string): number;
|
|
20
|
+
latestId(deviceId: string): number;
|
|
21
|
+
append(deviceId: string, id: number, event: string, data: Record<string, unknown>, backlogLimit: number): void;
|
|
22
|
+
readAfter(deviceId: string, afterId: number): PersistedSseEntry[];
|
|
23
|
+
truncateKeepLastN(deviceId: string, keep: number): void;
|
|
24
|
+
}
|
|
25
|
+
/** Shared queue: base directory follows `setOfflineQueueBaseDirForTest` / config. */
|
|
26
|
+
export declare const fridaySseOfflineQueue: FridaySseOfflineQueue;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { resolveFridayNextConfig } from "../config.js";
|
|
5
|
+
import { getHostOpenClawConfigSnapshot } from "../host-config.js";
|
|
6
|
+
import { getFridayNextRuntime } from "../runtime.js";
|
|
7
|
+
/** Test-only override for queue base directory. */
|
|
8
|
+
let testQueueBaseDir = null;
|
|
9
|
+
export function setOfflineQueueBaseDirForTest(dir) {
|
|
10
|
+
testQueueBaseDir = dir;
|
|
11
|
+
}
|
|
12
|
+
export function resolveFridayNextEventsQueueDir() {
|
|
13
|
+
if (testQueueBaseDir)
|
|
14
|
+
return testQueueBaseDir;
|
|
15
|
+
try {
|
|
16
|
+
const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
|
|
17
|
+
return path.join(path.dirname(cfg.historyDir), "events-queue");
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return path.join(os.homedir(), ".openclaw", "friday-next", "events-queue");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Per-device JSONL persistence for SSE replay.
|
|
25
|
+
* `overrideBaseDir` is for tests; production uses `resolveFridayNextEventsQueueDir()`.
|
|
26
|
+
*/
|
|
27
|
+
export class FridaySseOfflineQueue {
|
|
28
|
+
overrideBaseDir;
|
|
29
|
+
constructor(overrideBaseDir = null) {
|
|
30
|
+
this.overrideBaseDir = overrideBaseDir;
|
|
31
|
+
}
|
|
32
|
+
baseDir() {
|
|
33
|
+
if (this.overrideBaseDir)
|
|
34
|
+
return this.overrideBaseDir;
|
|
35
|
+
return resolveFridayNextEventsQueueDir();
|
|
36
|
+
}
|
|
37
|
+
devicePath(deviceId) {
|
|
38
|
+
const key = deviceId.trim().toUpperCase();
|
|
39
|
+
return path.join(this.baseDir(), `${key}.jsonl`);
|
|
40
|
+
}
|
|
41
|
+
ensureDir() {
|
|
42
|
+
fs.mkdirSync(this.baseDir(), { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
/** Highest id in file (full scan; ok for bounded backlog). */
|
|
45
|
+
scanMaxId(deviceId) {
|
|
46
|
+
const file = this.devicePath(deviceId);
|
|
47
|
+
if (!fs.existsSync(file))
|
|
48
|
+
return 0;
|
|
49
|
+
let max = 0;
|
|
50
|
+
const content = fs.readFileSync(file, "utf8");
|
|
51
|
+
for (const line of content.split("\n")) {
|
|
52
|
+
if (!line.trim())
|
|
53
|
+
continue;
|
|
54
|
+
try {
|
|
55
|
+
const o = JSON.parse(line);
|
|
56
|
+
if (typeof o.id === "number" && o.id > max)
|
|
57
|
+
max = o.id;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* skip corrupt line */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return max;
|
|
64
|
+
}
|
|
65
|
+
latestId(deviceId) {
|
|
66
|
+
return this.scanMaxId(deviceId.trim().toUpperCase());
|
|
67
|
+
}
|
|
68
|
+
append(deviceId, id, event, data, backlogLimit) {
|
|
69
|
+
if (event === "connected")
|
|
70
|
+
return;
|
|
71
|
+
this.ensureDir();
|
|
72
|
+
const file = this.devicePath(deviceId);
|
|
73
|
+
const line = JSON.stringify({ id, event, data }) + "\n";
|
|
74
|
+
fs.appendFileSync(file, line, "utf8");
|
|
75
|
+
if (backlogLimit > 0) {
|
|
76
|
+
this.truncateKeepLastN(deviceId, backlogLimit);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
readAfter(deviceId, afterId) {
|
|
80
|
+
const file = this.devicePath(deviceId);
|
|
81
|
+
if (!fs.existsSync(file))
|
|
82
|
+
return [];
|
|
83
|
+
const out = [];
|
|
84
|
+
const content = fs.readFileSync(file, "utf8");
|
|
85
|
+
for (const line of content.split("\n")) {
|
|
86
|
+
if (!line.trim())
|
|
87
|
+
continue;
|
|
88
|
+
try {
|
|
89
|
+
const o = JSON.parse(line);
|
|
90
|
+
if (typeof o.id === "number" &&
|
|
91
|
+
o.id > afterId &&
|
|
92
|
+
typeof o.event === "string" &&
|
|
93
|
+
o.data &&
|
|
94
|
+
typeof o.data === "object" &&
|
|
95
|
+
!Array.isArray(o.data)) {
|
|
96
|
+
out.push(o);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
/* skip */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
out.sort((a, b) => a.id - b.id);
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
truncateKeepLastN(deviceId, keep) {
|
|
107
|
+
if (keep <= 0)
|
|
108
|
+
return;
|
|
109
|
+
const file = this.devicePath(deviceId);
|
|
110
|
+
if (!fs.existsSync(file))
|
|
111
|
+
return;
|
|
112
|
+
const all = [];
|
|
113
|
+
const content = fs.readFileSync(file, "utf8");
|
|
114
|
+
for (const line of content.split("\n")) {
|
|
115
|
+
if (!line.trim())
|
|
116
|
+
continue;
|
|
117
|
+
try {
|
|
118
|
+
const o = JSON.parse(line);
|
|
119
|
+
if (typeof o.id === "number" && typeof o.event === "string" && o.data && typeof o.data === "object") {
|
|
120
|
+
all.push(o);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* skip */
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (all.length <= keep)
|
|
128
|
+
return;
|
|
129
|
+
const slice = all.slice(-keep);
|
|
130
|
+
fs.writeFileSync(file, slice.map((e) => JSON.stringify(e) + "\n").join(""), "utf8");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Shared queue: base directory follows `setOfflineQueueBaseDirForTest` / config. */
|
|
134
|
+
export const fridaySseOfflineQueue = new FridaySseOfflineQueue(null);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored from OpenClaw `plugin-sdk/runtime-store` (tiny, no transitive deps).
|
|
3
|
+
* Keeps friday-next HTTP tests from importing the full gateway graph.
|
|
4
|
+
*/
|
|
5
|
+
type PluginRuntimeStoreKeyOptions = {
|
|
6
|
+
key: string;
|
|
7
|
+
errorMessage: string;
|
|
8
|
+
};
|
|
9
|
+
type PluginRuntimeStorePluginOptions = {
|
|
10
|
+
pluginId: string;
|
|
11
|
+
errorMessage: string;
|
|
12
|
+
};
|
|
13
|
+
type PluginRuntimeStoreOptions = PluginRuntimeStoreKeyOptions | PluginRuntimeStorePluginOptions;
|
|
14
|
+
export declare function createPluginRuntimeStore<T>(errorMessage: string): {
|
|
15
|
+
setRuntime: (next: T) => void;
|
|
16
|
+
clearRuntime: () => void;
|
|
17
|
+
tryGetRuntime: () => T | null;
|
|
18
|
+
getRuntime: () => T;
|
|
19
|
+
};
|
|
20
|
+
export declare function createPluginRuntimeStore<T>(options: PluginRuntimeStoreOptions): {
|
|
21
|
+
setRuntime: (next: T) => void;
|
|
22
|
+
clearRuntime: () => void;
|
|
23
|
+
tryGetRuntime: () => T | null;
|
|
24
|
+
getRuntime: () => T;
|
|
25
|
+
};
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored from OpenClaw `plugin-sdk/runtime-store` (tiny, no transitive deps).
|
|
3
|
+
* Keeps friday-next HTTP tests from importing the full gateway graph.
|
|
4
|
+
*/
|
|
5
|
+
const pluginRuntimeStoreRegistryKey = Symbol.for("openclaw.plugin-sdk.runtime-store-registry");
|
|
6
|
+
function getPluginRuntimeStoreRegistry() {
|
|
7
|
+
const globalRecord = globalThis;
|
|
8
|
+
globalRecord[pluginRuntimeStoreRegistryKey] ??= new Map();
|
|
9
|
+
return globalRecord[pluginRuntimeStoreRegistryKey];
|
|
10
|
+
}
|
|
11
|
+
function pluginRuntimeStoreKeyForPluginId(pluginId) {
|
|
12
|
+
const normalizedPluginId = pluginId.trim();
|
|
13
|
+
if (!normalizedPluginId) {
|
|
14
|
+
throw new Error("createPluginRuntimeStore: pluginId must not be empty");
|
|
15
|
+
}
|
|
16
|
+
return `plugin-runtime:${normalizedPluginId}`;
|
|
17
|
+
}
|
|
18
|
+
function resolvePluginRuntimeStoreOptions(options) {
|
|
19
|
+
if (typeof options === "string") {
|
|
20
|
+
return { key: options, errorMessage: options };
|
|
21
|
+
}
|
|
22
|
+
if ("pluginId" in options) {
|
|
23
|
+
return {
|
|
24
|
+
key: pluginRuntimeStoreKeyForPluginId(options.pluginId),
|
|
25
|
+
errorMessage: options.errorMessage,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return options;
|
|
29
|
+
}
|
|
30
|
+
export function createPluginRuntimeStore(options) {
|
|
31
|
+
const resolved = resolvePluginRuntimeStoreOptions(options);
|
|
32
|
+
const slot = typeof options === "string"
|
|
33
|
+
? { runtime: null }
|
|
34
|
+
: (() => {
|
|
35
|
+
const registry = getPluginRuntimeStoreRegistry();
|
|
36
|
+
let existingSlot = registry.get(resolved.key);
|
|
37
|
+
if (!existingSlot) {
|
|
38
|
+
existingSlot = { runtime: null };
|
|
39
|
+
registry.set(resolved.key, existingSlot);
|
|
40
|
+
}
|
|
41
|
+
return existingSlot;
|
|
42
|
+
})();
|
|
43
|
+
return {
|
|
44
|
+
setRuntime(next) {
|
|
45
|
+
slot.runtime = next;
|
|
46
|
+
},
|
|
47
|
+
clearRuntime() {
|
|
48
|
+
slot.runtime = null;
|
|
49
|
+
},
|
|
50
|
+
tryGetRuntime() {
|
|
51
|
+
return slot.runtime ?? null;
|
|
52
|
+
},
|
|
53
|
+
getRuntime() {
|
|
54
|
+
if (slot.runtime === null) {
|
|
55
|
+
throw new Error(resolved.errorMessage);
|
|
56
|
+
}
|
|
57
|
+
return slot.runtime;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
package/index.ts
CHANGED
|
@@ -23,8 +23,13 @@ export { setFridayNextRuntime } from "./src/runtime.js";
|
|
|
23
23
|
/** `api.on` returns void — register tool hooks at most once per process. */
|
|
24
24
|
let fridayNextToolHooksRegistered = false;
|
|
25
25
|
let disposeAgentEventListener: (() => void) | null = null;
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Track the last `api` instance on which HTTP routes were registered.
|
|
28
|
+
* When the health-monitor restarts the plugin, `registerFull` receives a fresh `api` whose
|
|
29
|
+
* old routes are gone — we must re-register. A WeakRef lets us distinguish "same api,
|
|
30
|
+
* re-entered" (skip) from "new api after restart" (re-register).
|
|
31
|
+
*/
|
|
32
|
+
let lastApiRoutesRegistered: WeakRef<OpenClawPluginApi> | null = null;
|
|
28
33
|
|
|
29
34
|
function deviceIdFromToolContext(ctx: PluginHookToolContext): string | null {
|
|
30
35
|
if (ctx.runId) {
|
|
@@ -77,8 +82,9 @@ export default defineChannelPluginEntry({
|
|
|
77
82
|
setRuntime: setFridayNextRuntime,
|
|
78
83
|
registerFull: (api: OpenClawPluginApi) => {
|
|
79
84
|
setFridayAgentForwardRuntime(api);
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
const sameApi = lastApiRoutesRegistered?.deref() === api;
|
|
86
|
+
if (!sameApi) {
|
|
87
|
+
lastApiRoutesRegistered = new WeakRef(api);
|
|
82
88
|
registerFridayNextHttpRoutes(api);
|
|
83
89
|
} else {
|
|
84
90
|
const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
|
package/package.json
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syengup/friday-channel-next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"description": "OpenClaw Friday Next Apple channel plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
|
+
"dist/",
|
|
7
8
|
"index.ts",
|
|
8
9
|
"src/",
|
|
9
10
|
"install.js",
|
|
10
11
|
"tsconfig.json",
|
|
11
12
|
"openclaw.plugin.json"
|
|
12
13
|
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"build": "tsc -p tsconfig.json",
|
|
15
|
-
"test": "npm run test:unit && npm run test:e2e",
|
|
16
|
-
"test:unit": "vitest run",
|
|
17
|
-
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
18
|
-
"test:smoke": "node scripts/e2e-smoke.mjs",
|
|
19
|
-
"test:msg-live": "node scripts/message-roundtrip-live.mjs"
|
|
20
|
-
},
|
|
21
14
|
"bin": {
|
|
22
15
|
"friday-channel-next": "install.js"
|
|
23
16
|
},
|
|
@@ -63,5 +56,13 @@
|
|
|
63
56
|
"typescript": "^6.0.3",
|
|
64
57
|
"vitest": "^4.1.5",
|
|
65
58
|
"zod": "^4.3.6"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsc -p tsconfig.json",
|
|
62
|
+
"test": "npm run test:unit && npm run test:e2e",
|
|
63
|
+
"test:unit": "vitest run",
|
|
64
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
65
|
+
"test:smoke": "node scripts/e2e-smoke.mjs",
|
|
66
|
+
"test:msg-live": "node scripts/message-roundtrip-live.mjs"
|
|
66
67
|
}
|
|
67
|
-
}
|
|
68
|
+
}
|