@phronesis-io/openclaw-eigenflux 0.0.1 → 0.0.3
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/README.md +16 -3
- package/dist/config.d.ts +12 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +47 -10
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -7
- package/dist/index.js.map +1 -1
- package/dist/notifier.d.ts +1 -17
- package/dist/notifier.d.ts.map +1 -1
- package/dist/notifier.js +1 -94
- package/dist/notifier.js.map +1 -1
- package/dist/pm-polling-client.d.ts +1 -0
- package/dist/pm-polling-client.d.ts.map +1 -1
- package/dist/pm-polling-client.js +64 -57
- package/dist/pm-polling-client.js.map +1 -1
- package/dist/polling-client.d.ts +1 -0
- package/dist/polling-client.d.ts.map +1 -1
- package/dist/polling-client.js +65 -58
- package/dist/polling-client.js.map +1 -1
- package/openclaw.plugin.json +8 -6
- package/package.json +2 -2
- package/src/agent-prompt-templates.ts +0 -91
- package/src/config.test.ts +0 -188
- package/src/config.ts +0 -410
- package/src/credentials-loader.test.ts +0 -78
- package/src/credentials-loader.ts +0 -121
- package/src/gateway-rpc-client.test.ts +0 -190
- package/src/gateway-rpc-client.ts +0 -373
- package/src/index.integration.test.ts +0 -437
- package/src/index.test.ts +0 -454
- package/src/index.ts +0 -758
- package/src/logger.ts +0 -27
- package/src/notification-route-resolver.test.ts +0 -136
- package/src/notification-route-resolver.ts +0 -430
- package/src/notifier.test.ts +0 -374
- package/src/notifier.ts +0 -558
- package/src/openclaw-plugin-sdk.d.ts +0 -121
- package/src/pm-polling-client.test.ts +0 -390
- package/src/pm-polling-client.ts +0 -257
- package/src/polling-client.test.ts +0 -279
- package/src/polling-client.ts +0 -283
- package/src/session-route-memory.ts +0 -106
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
import { EigenFluxPmPollingClient } from './pm-polling-client';
|
|
2
|
-
import { Logger } from './logger';
|
|
3
|
-
|
|
4
|
-
function createLoggerSpies() {
|
|
5
|
-
return {
|
|
6
|
-
info: jest.fn(),
|
|
7
|
-
warn: jest.fn(),
|
|
8
|
-
error: jest.fn(),
|
|
9
|
-
debug: jest.fn(),
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function createLogger(spies = createLoggerSpies()): Logger {
|
|
14
|
-
return new Logger(spies);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('EigenFluxPmPollingClient', () => {
|
|
18
|
-
const originalFetch = global.fetch;
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
global.fetch = originalFetch;
|
|
22
|
-
jest.restoreAllMocks();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test('polls PM and forwards the full payload to callback', async () => {
|
|
26
|
-
const onPmFetched = jest.fn().mockResolvedValue(undefined);
|
|
27
|
-
const onAuthRequired = jest.fn().mockResolvedValue(undefined);
|
|
28
|
-
|
|
29
|
-
global.fetch = jest.fn().mockResolvedValue(
|
|
30
|
-
new Response(
|
|
31
|
-
JSON.stringify({
|
|
32
|
-
code: 0,
|
|
33
|
-
msg: 'success',
|
|
34
|
-
data: {
|
|
35
|
-
messages: [
|
|
36
|
-
{
|
|
37
|
-
message_id: 'msg-101',
|
|
38
|
-
from_agent_id: 'agent-42',
|
|
39
|
-
conversation_id: 'conv-1',
|
|
40
|
-
content: 'Hello from agent',
|
|
41
|
-
created_at: 1760000000000,
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
}),
|
|
46
|
-
{
|
|
47
|
-
status: 200,
|
|
48
|
-
headers: {
|
|
49
|
-
'Content-Type': 'application/json',
|
|
50
|
-
},
|
|
51
|
-
}
|
|
52
|
-
)
|
|
53
|
-
) as typeof fetch;
|
|
54
|
-
|
|
55
|
-
const client = new EigenFluxPmPollingClient({
|
|
56
|
-
apiUrl: 'http://127.0.0.1:8080',
|
|
57
|
-
getAuthState: () => ({
|
|
58
|
-
status: 'available',
|
|
59
|
-
accessToken: 'at_test_token',
|
|
60
|
-
source: 'file',
|
|
61
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
62
|
-
}),
|
|
63
|
-
pollIntervalSec: 60,
|
|
64
|
-
logger: createLogger(),
|
|
65
|
-
onPmFetched,
|
|
66
|
-
onAuthRequired,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const result = await client.pollOnce();
|
|
70
|
-
|
|
71
|
-
expect(result).toEqual(
|
|
72
|
-
expect.objectContaining({
|
|
73
|
-
kind: 'success',
|
|
74
|
-
})
|
|
75
|
-
);
|
|
76
|
-
expect(onPmFetched).toHaveBeenCalledWith(
|
|
77
|
-
expect.objectContaining({
|
|
78
|
-
code: 0,
|
|
79
|
-
data: expect.objectContaining({
|
|
80
|
-
messages: [
|
|
81
|
-
expect.objectContaining({
|
|
82
|
-
message_id: 'msg-101',
|
|
83
|
-
content: 'Hello from agent',
|
|
84
|
-
}),
|
|
85
|
-
],
|
|
86
|
-
}),
|
|
87
|
-
})
|
|
88
|
-
);
|
|
89
|
-
expect(onAuthRequired).not.toHaveBeenCalled();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('does not invoke callback when messages array is empty', async () => {
|
|
93
|
-
const onPmFetched = jest.fn().mockResolvedValue(undefined);
|
|
94
|
-
const onAuthRequired = jest.fn().mockResolvedValue(undefined);
|
|
95
|
-
|
|
96
|
-
global.fetch = jest.fn().mockResolvedValue(
|
|
97
|
-
new Response(
|
|
98
|
-
JSON.stringify({
|
|
99
|
-
code: 0,
|
|
100
|
-
msg: 'success',
|
|
101
|
-
data: { messages: [] },
|
|
102
|
-
}),
|
|
103
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
104
|
-
)
|
|
105
|
-
) as typeof fetch;
|
|
106
|
-
|
|
107
|
-
const client = new EigenFluxPmPollingClient({
|
|
108
|
-
apiUrl: 'http://127.0.0.1:8080',
|
|
109
|
-
getAuthState: () => ({
|
|
110
|
-
status: 'available',
|
|
111
|
-
accessToken: 'at_test_token',
|
|
112
|
-
source: 'file',
|
|
113
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
114
|
-
}),
|
|
115
|
-
pollIntervalSec: 60,
|
|
116
|
-
logger: createLogger(),
|
|
117
|
-
onPmFetched,
|
|
118
|
-
onAuthRequired,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const result = await client.pollOnce();
|
|
122
|
-
|
|
123
|
-
expect(result.kind).toBe('success');
|
|
124
|
-
expect(onPmFetched).not.toHaveBeenCalled();
|
|
125
|
-
expect(onAuthRequired).not.toHaveBeenCalled();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test('emits auth-required callback when token is missing', async () => {
|
|
129
|
-
const onPmFetched = jest.fn().mockResolvedValue(undefined);
|
|
130
|
-
const onAuthRequired = jest.fn().mockResolvedValue(undefined);
|
|
131
|
-
|
|
132
|
-
const client = new EigenFluxPmPollingClient({
|
|
133
|
-
apiUrl: 'http://127.0.0.1:8080',
|
|
134
|
-
getAuthState: () => ({
|
|
135
|
-
status: 'missing',
|
|
136
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
137
|
-
}),
|
|
138
|
-
pollIntervalSec: 60,
|
|
139
|
-
logger: createLogger(),
|
|
140
|
-
onPmFetched,
|
|
141
|
-
onAuthRequired,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const result = await client.pollOnce();
|
|
145
|
-
|
|
146
|
-
expect(result).toEqual({
|
|
147
|
-
kind: 'auth_required',
|
|
148
|
-
authEvent: {
|
|
149
|
-
reason: 'missing_token',
|
|
150
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
151
|
-
source: undefined,
|
|
152
|
-
expiresAt: undefined,
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
expect(onAuthRequired).toHaveBeenCalledWith({
|
|
156
|
-
reason: 'missing_token',
|
|
157
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
158
|
-
source: undefined,
|
|
159
|
-
expiresAt: undefined,
|
|
160
|
-
});
|
|
161
|
-
expect(onPmFetched).not.toHaveBeenCalled();
|
|
162
|
-
expect(global.fetch).toBe(originalFetch);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('emits auth-required callback when token is expired', async () => {
|
|
166
|
-
const onPmFetched = jest.fn().mockResolvedValue(undefined);
|
|
167
|
-
const onAuthRequired = jest.fn().mockResolvedValue(undefined);
|
|
168
|
-
|
|
169
|
-
const client = new EigenFluxPmPollingClient({
|
|
170
|
-
apiUrl: 'http://127.0.0.1:8080',
|
|
171
|
-
getAuthState: () => ({
|
|
172
|
-
status: 'expired',
|
|
173
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
174
|
-
source: 'file',
|
|
175
|
-
expiresAt: 1700000000000,
|
|
176
|
-
}),
|
|
177
|
-
pollIntervalSec: 60,
|
|
178
|
-
logger: createLogger(),
|
|
179
|
-
onPmFetched,
|
|
180
|
-
onAuthRequired,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
const result = await client.pollOnce();
|
|
184
|
-
|
|
185
|
-
expect(result).toEqual({
|
|
186
|
-
kind: 'auth_required',
|
|
187
|
-
authEvent: {
|
|
188
|
-
reason: 'expired_token',
|
|
189
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
190
|
-
source: 'file',
|
|
191
|
-
expiresAt: 1700000000000,
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
expect(onAuthRequired).toHaveBeenCalledWith({
|
|
195
|
-
reason: 'expired_token',
|
|
196
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
197
|
-
source: 'file',
|
|
198
|
-
expiresAt: 1700000000000,
|
|
199
|
-
});
|
|
200
|
-
expect(onPmFetched).not.toHaveBeenCalled();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test('emits auth-required callback when PM fetch returns 401', async () => {
|
|
204
|
-
const onPmFetched = jest.fn().mockResolvedValue(undefined);
|
|
205
|
-
const onAuthRequired = jest.fn().mockResolvedValue(undefined);
|
|
206
|
-
|
|
207
|
-
global.fetch = jest.fn().mockResolvedValue(
|
|
208
|
-
new Response('', {
|
|
209
|
-
status: 401,
|
|
210
|
-
statusText: 'Unauthorized',
|
|
211
|
-
})
|
|
212
|
-
) as typeof fetch;
|
|
213
|
-
|
|
214
|
-
const client = new EigenFluxPmPollingClient({
|
|
215
|
-
apiUrl: 'http://127.0.0.1:8080',
|
|
216
|
-
getAuthState: () => ({
|
|
217
|
-
status: 'available',
|
|
218
|
-
accessToken: 'at_test_token',
|
|
219
|
-
source: 'file',
|
|
220
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
221
|
-
}),
|
|
222
|
-
pollIntervalSec: 60,
|
|
223
|
-
logger: createLogger(),
|
|
224
|
-
onPmFetched,
|
|
225
|
-
onAuthRequired,
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
const result = await client.pollOnce();
|
|
229
|
-
|
|
230
|
-
expect(result).toEqual({
|
|
231
|
-
kind: 'auth_required',
|
|
232
|
-
authEvent: {
|
|
233
|
-
reason: 'unauthorized',
|
|
234
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
235
|
-
source: 'file',
|
|
236
|
-
expiresAt: undefined,
|
|
237
|
-
statusCode: 401,
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
expect(onAuthRequired).toHaveBeenCalledWith({
|
|
241
|
-
reason: 'unauthorized',
|
|
242
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
243
|
-
source: 'file',
|
|
244
|
-
expiresAt: undefined,
|
|
245
|
-
statusCode: 401,
|
|
246
|
-
});
|
|
247
|
-
expect(onPmFetched).not.toHaveBeenCalled();
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test('sends User-Agent header with plugin version', async () => {
|
|
251
|
-
const onPmFetched = jest.fn().mockResolvedValue(undefined);
|
|
252
|
-
const onAuthRequired = jest.fn().mockResolvedValue(undefined);
|
|
253
|
-
|
|
254
|
-
global.fetch = jest.fn().mockResolvedValue(
|
|
255
|
-
new Response(
|
|
256
|
-
JSON.stringify({
|
|
257
|
-
code: 0,
|
|
258
|
-
msg: 'success',
|
|
259
|
-
data: { messages: [] },
|
|
260
|
-
}),
|
|
261
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
262
|
-
)
|
|
263
|
-
) as typeof fetch;
|
|
264
|
-
|
|
265
|
-
const client = new EigenFluxPmPollingClient({
|
|
266
|
-
apiUrl: 'http://127.0.0.1:8080',
|
|
267
|
-
getAuthState: () => ({
|
|
268
|
-
status: 'available',
|
|
269
|
-
accessToken: 'at_test_token',
|
|
270
|
-
source: 'file',
|
|
271
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
272
|
-
}),
|
|
273
|
-
pollIntervalSec: 60,
|
|
274
|
-
logger: createLogger(),
|
|
275
|
-
onPmFetched,
|
|
276
|
-
onAuthRequired,
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
await client.pollOnce();
|
|
280
|
-
|
|
281
|
-
const fetchCall = (global.fetch as jest.Mock).mock.calls[0];
|
|
282
|
-
const headers = fetchCall[1].headers;
|
|
283
|
-
expect(headers['User-Agent']).toContain('eigenflux-plugin');
|
|
284
|
-
expect(headers['User-Agent']).toContain('node/');
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test('logs detailed fetch failure diagnostics', async () => {
|
|
288
|
-
const loggerSpies = createLoggerSpies();
|
|
289
|
-
const networkCause = Object.assign(
|
|
290
|
-
new Error('connect ECONNREFUSED 127.0.0.1:8080'),
|
|
291
|
-
{
|
|
292
|
-
code: 'ECONNREFUSED',
|
|
293
|
-
errno: -61,
|
|
294
|
-
syscall: 'connect',
|
|
295
|
-
address: '127.0.0.1',
|
|
296
|
-
port: 8080,
|
|
297
|
-
}
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
global.fetch = jest.fn().mockRejectedValue(
|
|
301
|
-
Object.assign(new TypeError('fetch failed'), { cause: networkCause })
|
|
302
|
-
) as typeof fetch;
|
|
303
|
-
|
|
304
|
-
const client = new EigenFluxPmPollingClient({
|
|
305
|
-
apiUrl: 'http://127.0.0.1:8080',
|
|
306
|
-
getAuthState: () => ({
|
|
307
|
-
status: 'available',
|
|
308
|
-
accessToken: 'at_test_token',
|
|
309
|
-
source: 'file',
|
|
310
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
311
|
-
}),
|
|
312
|
-
pollIntervalSec: 60,
|
|
313
|
-
logger: createLogger(loggerSpies),
|
|
314
|
-
onPmFetched: jest.fn().mockResolvedValue(undefined),
|
|
315
|
-
onAuthRequired: jest.fn().mockResolvedValue(undefined),
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
const result = await client.pollOnce();
|
|
319
|
-
|
|
320
|
-
expect(result.kind).toBe('error');
|
|
321
|
-
expect(loggerSpies.error).toHaveBeenCalledWith(
|
|
322
|
-
expect.stringContaining(
|
|
323
|
-
'[EigenFlux] Failed to poll PM (url=http://127.0.0.1:8080/api/v1/pm/fetch): TypeError: fetch failed'
|
|
324
|
-
)
|
|
325
|
-
);
|
|
326
|
-
expect(loggerSpies.error).toHaveBeenCalledWith(
|
|
327
|
-
expect.stringContaining('cause=Error: connect ECONNREFUSED 127.0.0.1:8080')
|
|
328
|
-
);
|
|
329
|
-
expect(loggerSpies.error).toHaveBeenCalledWith(
|
|
330
|
-
expect.stringContaining('code=ECONNREFUSED')
|
|
331
|
-
);
|
|
332
|
-
expect(loggerSpies.error).toHaveBeenCalledWith(
|
|
333
|
-
expect.stringContaining('address=127.0.0.1')
|
|
334
|
-
);
|
|
335
|
-
expect(loggerSpies.error).toHaveBeenCalledWith(
|
|
336
|
-
expect.stringContaining('port=8080')
|
|
337
|
-
);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test('start and stop lifecycle', async () => {
|
|
341
|
-
const loggerSpies = createLoggerSpies();
|
|
342
|
-
|
|
343
|
-
global.fetch = jest.fn().mockResolvedValue(
|
|
344
|
-
new Response(
|
|
345
|
-
JSON.stringify({
|
|
346
|
-
code: 0,
|
|
347
|
-
msg: 'success',
|
|
348
|
-
data: { messages: [] },
|
|
349
|
-
}),
|
|
350
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
351
|
-
)
|
|
352
|
-
) as typeof fetch;
|
|
353
|
-
|
|
354
|
-
const client = new EigenFluxPmPollingClient({
|
|
355
|
-
apiUrl: 'http://127.0.0.1:8080',
|
|
356
|
-
getAuthState: () => ({
|
|
357
|
-
status: 'available',
|
|
358
|
-
accessToken: 'at_test_token',
|
|
359
|
-
source: 'file',
|
|
360
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
361
|
-
}),
|
|
362
|
-
pollIntervalSec: 60,
|
|
363
|
-
logger: createLogger(loggerSpies),
|
|
364
|
-
onPmFetched: jest.fn().mockResolvedValue(undefined),
|
|
365
|
-
onAuthRequired: jest.fn().mockResolvedValue(undefined),
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
await client.start();
|
|
369
|
-
|
|
370
|
-
expect(loggerSpies.info).toHaveBeenCalledWith(
|
|
371
|
-
expect.stringContaining('Starting PM polling client')
|
|
372
|
-
);
|
|
373
|
-
// Initial poll should have fired
|
|
374
|
-
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
375
|
-
|
|
376
|
-
// Starting again should warn
|
|
377
|
-
await client.start();
|
|
378
|
-
expect(loggerSpies.warn).toHaveBeenCalledWith(
|
|
379
|
-
expect.stringContaining('PM polling client already running')
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
client.stop();
|
|
383
|
-
expect(loggerSpies.info).toHaveBeenCalledWith(
|
|
384
|
-
expect.stringContaining('Stopping PM polling client')
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
// Stopping again should be a no-op
|
|
388
|
-
client.stop();
|
|
389
|
-
});
|
|
390
|
-
});
|
package/src/pm-polling-client.ts
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Polling client for EigenFlux private message updates
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { AuthState } from './credentials-loader';
|
|
6
|
-
import { PLUGIN_CONFIG } from './config';
|
|
7
|
-
import { Logger } from './logger';
|
|
8
|
-
import { AuthRequiredEvent, PollOnceOptions } from './polling-client';
|
|
9
|
-
|
|
10
|
-
export interface PmMessage {
|
|
11
|
-
message_id: string;
|
|
12
|
-
from_agent_id: string;
|
|
13
|
-
conversation_id: string;
|
|
14
|
-
content: string;
|
|
15
|
-
created_at: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface PmFetchResponse {
|
|
19
|
-
code: number;
|
|
20
|
-
msg: string;
|
|
21
|
-
data: {
|
|
22
|
-
messages: PmMessage[];
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface PmPollingClientConfig {
|
|
27
|
-
apiUrl: string;
|
|
28
|
-
getAuthState: () => AuthState;
|
|
29
|
-
pollIntervalSec: number;
|
|
30
|
-
logger: Logger;
|
|
31
|
-
onPmFetched: (payload: PmFetchResponse) => Promise<void>;
|
|
32
|
-
onAuthRequired: (event: AuthRequiredEvent) => Promise<void>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type PmPollResult =
|
|
36
|
-
| {
|
|
37
|
-
kind: 'success';
|
|
38
|
-
payload: PmFetchResponse;
|
|
39
|
-
}
|
|
40
|
-
| {
|
|
41
|
-
kind: 'auth_required';
|
|
42
|
-
authEvent: AuthRequiredEvent;
|
|
43
|
-
}
|
|
44
|
-
| {
|
|
45
|
-
kind: 'error';
|
|
46
|
-
error: Error;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export class EigenFluxPmPollingClient {
|
|
50
|
-
private config: PmPollingClientConfig;
|
|
51
|
-
private intervalId: NodeJS.Timeout | null = null;
|
|
52
|
-
private isRunning = false;
|
|
53
|
-
|
|
54
|
-
constructor(config: PmPollingClientConfig) {
|
|
55
|
-
this.config = config;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async start(): Promise<void> {
|
|
59
|
-
if (this.isRunning) {
|
|
60
|
-
this.config.logger.warn('PM polling client already running');
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
this.isRunning = true;
|
|
65
|
-
this.config.logger.info(
|
|
66
|
-
`Starting PM polling client (interval: ${this.config.pollIntervalSec}s)`
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
// Initial fetch
|
|
70
|
-
await this.pollOnce();
|
|
71
|
-
|
|
72
|
-
// Schedule periodic polling
|
|
73
|
-
this.intervalId = setInterval(() => {
|
|
74
|
-
this.pollOnce().catch((err) => {
|
|
75
|
-
this.config.logger.error(`PM polling error: ${this.formatError(err)}`);
|
|
76
|
-
});
|
|
77
|
-
}, this.config.pollIntervalSec * 1000);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
stop(): void {
|
|
81
|
-
if (!this.isRunning) {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
this.config.logger.info('Stopping PM polling client');
|
|
86
|
-
this.isRunning = false;
|
|
87
|
-
|
|
88
|
-
if (this.intervalId) {
|
|
89
|
-
clearInterval(this.intervalId);
|
|
90
|
-
this.intervalId = null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async pollOnce(options: PollOnceOptions = {}): Promise<PmPollResult> {
|
|
95
|
-
const notifyFeed = options.notifyFeed ?? true;
|
|
96
|
-
const notifyAuthRequired = options.notifyAuthRequired ?? true;
|
|
97
|
-
const authState = this.config.getAuthState();
|
|
98
|
-
if (authState.status !== 'available') {
|
|
99
|
-
this.config.logger.warn(
|
|
100
|
-
`No usable access token available (status=${authState.status}), skipping PM poll`
|
|
101
|
-
);
|
|
102
|
-
const authEvent: AuthRequiredEvent = {
|
|
103
|
-
reason: authState.status === 'expired' ? 'expired_token' : 'missing_token',
|
|
104
|
-
credentialsPath: authState.credentialsPath,
|
|
105
|
-
source: authState.source,
|
|
106
|
-
expiresAt: authState.expiresAt,
|
|
107
|
-
};
|
|
108
|
-
if (notifyAuthRequired) {
|
|
109
|
-
await this.config.onAuthRequired(authEvent);
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
kind: 'auth_required',
|
|
113
|
-
authEvent,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const url = `${this.config.apiUrl}/api/v1/pm/fetch`;
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
this.config.logger.info(`Polling PM request: ${url}`);
|
|
121
|
-
this.config.logger.debug(`Polling: ${url}`);
|
|
122
|
-
|
|
123
|
-
const response = await fetch(url, {
|
|
124
|
-
method: 'GET',
|
|
125
|
-
headers: {
|
|
126
|
-
Authorization: `Bearer ${authState.accessToken}`,
|
|
127
|
-
'Content-Type': 'application/json',
|
|
128
|
-
'User-Agent': PLUGIN_CONFIG.USER_AGENT,
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
if (response.status === 401) {
|
|
133
|
-
const authEvent: AuthRequiredEvent = {
|
|
134
|
-
reason: 'unauthorized',
|
|
135
|
-
credentialsPath: authState.credentialsPath,
|
|
136
|
-
source: authState.source,
|
|
137
|
-
expiresAt: authState.expiresAt,
|
|
138
|
-
statusCode: 401,
|
|
139
|
-
};
|
|
140
|
-
if (notifyAuthRequired) {
|
|
141
|
-
await this.config.onAuthRequired(authEvent);
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
kind: 'auth_required',
|
|
145
|
-
authEvent,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (!response.ok) {
|
|
150
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const data = (await response.json()) as PmFetchResponse;
|
|
154
|
-
|
|
155
|
-
if (data.code !== 0) {
|
|
156
|
-
throw new Error(`API error: ${data.msg}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const messages = data.data.messages ?? [];
|
|
160
|
-
this.config.logger.info(
|
|
161
|
-
`Polled PM: ${messages.length} messages`
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
if (notifyFeed && messages.length > 0) {
|
|
165
|
-
await this.config.onPmFetched(data);
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
kind: 'success',
|
|
169
|
-
payload: data,
|
|
170
|
-
};
|
|
171
|
-
} catch (error) {
|
|
172
|
-
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
173
|
-
this.config.logger.error(
|
|
174
|
-
`Failed to poll PM (url=${url}): ${this.formatError(normalized)}`
|
|
175
|
-
);
|
|
176
|
-
return {
|
|
177
|
-
kind: 'error',
|
|
178
|
-
error: normalized,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private formatError(error: unknown): string {
|
|
184
|
-
const segments: string[] = [];
|
|
185
|
-
this.appendErrorSegment(segments, error, false);
|
|
186
|
-
return segments.join(' | ');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
private appendErrorSegment(segments: string[], error: unknown, isCause: boolean): void {
|
|
190
|
-
const prefix = isCause ? 'cause=' : '';
|
|
191
|
-
|
|
192
|
-
if (error instanceof Error) {
|
|
193
|
-
const details: string[] = [`${error.name}: ${error.message}`];
|
|
194
|
-
const metadata = this.errorMetadata(error);
|
|
195
|
-
if (metadata.length > 0) {
|
|
196
|
-
details.push(...metadata);
|
|
197
|
-
}
|
|
198
|
-
segments.push(prefix + details.join(' | '));
|
|
199
|
-
|
|
200
|
-
const cause = (error as Error & { cause?: unknown }).cause;
|
|
201
|
-
if (cause !== undefined) {
|
|
202
|
-
this.appendErrorSegment(segments, cause, true);
|
|
203
|
-
}
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (error && typeof error === 'object') {
|
|
208
|
-
const metadata = this.errorMetadata(error);
|
|
209
|
-
if (metadata.length > 0) {
|
|
210
|
-
segments.push(prefix + metadata.join(' | '));
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
segments.push(prefix + String(error));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
private errorMetadata(value: unknown): string[] {
|
|
219
|
-
if (!value || typeof value !== 'object') {
|
|
220
|
-
return [];
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const record = value as {
|
|
224
|
-
code?: unknown;
|
|
225
|
-
errno?: unknown;
|
|
226
|
-
syscall?: unknown;
|
|
227
|
-
address?: unknown;
|
|
228
|
-
port?: unknown;
|
|
229
|
-
status?: unknown;
|
|
230
|
-
statusText?: unknown;
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const metadata: string[] = [];
|
|
234
|
-
if (record.code !== undefined) {
|
|
235
|
-
metadata.push(`code=${String(record.code)}`);
|
|
236
|
-
}
|
|
237
|
-
if (record.errno !== undefined) {
|
|
238
|
-
metadata.push(`errno=${String(record.errno)}`);
|
|
239
|
-
}
|
|
240
|
-
if (record.syscall !== undefined) {
|
|
241
|
-
metadata.push(`syscall=${String(record.syscall)}`);
|
|
242
|
-
}
|
|
243
|
-
if (record.address !== undefined) {
|
|
244
|
-
metadata.push(`address=${String(record.address)}`);
|
|
245
|
-
}
|
|
246
|
-
if (record.port !== undefined) {
|
|
247
|
-
metadata.push(`port=${String(record.port)}`);
|
|
248
|
-
}
|
|
249
|
-
if (record.status !== undefined) {
|
|
250
|
-
metadata.push(`status=${String(record.status)}`);
|
|
251
|
-
}
|
|
252
|
-
if (record.statusText !== undefined) {
|
|
253
|
-
metadata.push(`status_text=${String(record.statusText)}`);
|
|
254
|
-
}
|
|
255
|
-
return metadata;
|
|
256
|
-
}
|
|
257
|
-
}
|