@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,279 +0,0 @@
|
|
|
1
|
-
import { EigenFluxPollingClient } from './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('EigenFluxPollingClient', () => {
|
|
18
|
-
const originalFetch = global.fetch;
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
global.fetch = originalFetch;
|
|
22
|
-
jest.restoreAllMocks();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test('polls feed and forwards the full payload to callback', async () => {
|
|
26
|
-
const onFeedPolled = 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
|
-
items: [
|
|
36
|
-
{
|
|
37
|
-
item_id: 'item-101',
|
|
38
|
-
group_id: 'group-101',
|
|
39
|
-
summary: 'Important signal',
|
|
40
|
-
broadcast_type: 'info',
|
|
41
|
-
updated_at: 1760000000000,
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
has_more: false,
|
|
45
|
-
notifications: [
|
|
46
|
-
{
|
|
47
|
-
notification_id: 'notif-1',
|
|
48
|
-
type: 'system',
|
|
49
|
-
content: 'Feed refreshed successfully',
|
|
50
|
-
created_at: 1760000000100,
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
|
-
}),
|
|
55
|
-
{
|
|
56
|
-
status: 200,
|
|
57
|
-
headers: {
|
|
58
|
-
'Content-Type': 'application/json',
|
|
59
|
-
},
|
|
60
|
-
}
|
|
61
|
-
)
|
|
62
|
-
) as typeof fetch;
|
|
63
|
-
|
|
64
|
-
const client = new EigenFluxPollingClient({
|
|
65
|
-
apiUrl: 'http://127.0.0.1:8080',
|
|
66
|
-
getAuthState: () => ({
|
|
67
|
-
status: 'available',
|
|
68
|
-
accessToken: 'at_test_token',
|
|
69
|
-
source: 'file',
|
|
70
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
71
|
-
}),
|
|
72
|
-
pollIntervalSec: 60,
|
|
73
|
-
logger: createLogger(),
|
|
74
|
-
onFeedPolled,
|
|
75
|
-
onAuthRequired,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const result = await client.pollOnce();
|
|
79
|
-
|
|
80
|
-
expect(result).toEqual(
|
|
81
|
-
expect.objectContaining({
|
|
82
|
-
kind: 'success',
|
|
83
|
-
})
|
|
84
|
-
);
|
|
85
|
-
expect(onFeedPolled).toHaveBeenCalledWith(
|
|
86
|
-
expect.objectContaining({
|
|
87
|
-
code: 0,
|
|
88
|
-
data: expect.objectContaining({
|
|
89
|
-
items: [
|
|
90
|
-
expect.objectContaining({
|
|
91
|
-
item_id: 'item-101',
|
|
92
|
-
summary: 'Important signal',
|
|
93
|
-
}),
|
|
94
|
-
],
|
|
95
|
-
notifications: [
|
|
96
|
-
expect.objectContaining({
|
|
97
|
-
notification_id: 'notif-1',
|
|
98
|
-
}),
|
|
99
|
-
],
|
|
100
|
-
}),
|
|
101
|
-
})
|
|
102
|
-
);
|
|
103
|
-
expect(onAuthRequired).not.toHaveBeenCalled();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test('emits auth-required callback when token is missing', async () => {
|
|
107
|
-
const onFeedPolled = jest.fn().mockResolvedValue(undefined);
|
|
108
|
-
const onAuthRequired = jest.fn().mockResolvedValue(undefined);
|
|
109
|
-
|
|
110
|
-
const client = new EigenFluxPollingClient({
|
|
111
|
-
apiUrl: 'http://127.0.0.1:8080',
|
|
112
|
-
getAuthState: () => ({
|
|
113
|
-
status: 'missing',
|
|
114
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
115
|
-
}),
|
|
116
|
-
pollIntervalSec: 60,
|
|
117
|
-
logger: createLogger(),
|
|
118
|
-
onFeedPolled,
|
|
119
|
-
onAuthRequired,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
const result = await client.pollOnce();
|
|
123
|
-
|
|
124
|
-
expect(result).toEqual({
|
|
125
|
-
kind: 'auth_required',
|
|
126
|
-
authEvent: {
|
|
127
|
-
reason: 'missing_token',
|
|
128
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
129
|
-
source: undefined,
|
|
130
|
-
expiresAt: undefined,
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
expect(onAuthRequired).toHaveBeenCalledWith({
|
|
134
|
-
reason: 'missing_token',
|
|
135
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
136
|
-
source: undefined,
|
|
137
|
-
expiresAt: undefined,
|
|
138
|
-
});
|
|
139
|
-
expect(onFeedPolled).not.toHaveBeenCalled();
|
|
140
|
-
expect(global.fetch).toBe(originalFetch);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test('emits auth-required callback when feed returns 401', async () => {
|
|
144
|
-
const onFeedPolled = jest.fn().mockResolvedValue(undefined);
|
|
145
|
-
const onAuthRequired = jest.fn().mockResolvedValue(undefined);
|
|
146
|
-
|
|
147
|
-
global.fetch = jest.fn().mockResolvedValue(
|
|
148
|
-
new Response('', {
|
|
149
|
-
status: 401,
|
|
150
|
-
statusText: 'Unauthorized',
|
|
151
|
-
})
|
|
152
|
-
) as typeof fetch;
|
|
153
|
-
|
|
154
|
-
const client = new EigenFluxPollingClient({
|
|
155
|
-
apiUrl: 'http://127.0.0.1:8080',
|
|
156
|
-
getAuthState: () => ({
|
|
157
|
-
status: 'available',
|
|
158
|
-
accessToken: 'at_test_token',
|
|
159
|
-
source: 'file',
|
|
160
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
161
|
-
}),
|
|
162
|
-
pollIntervalSec: 60,
|
|
163
|
-
logger: createLogger(),
|
|
164
|
-
onFeedPolled,
|
|
165
|
-
onAuthRequired,
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
const result = await client.pollOnce();
|
|
169
|
-
|
|
170
|
-
expect(result).toEqual({
|
|
171
|
-
kind: 'auth_required',
|
|
172
|
-
authEvent: {
|
|
173
|
-
reason: 'unauthorized',
|
|
174
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
175
|
-
source: 'file',
|
|
176
|
-
expiresAt: undefined,
|
|
177
|
-
statusCode: 401,
|
|
178
|
-
},
|
|
179
|
-
});
|
|
180
|
-
expect(onAuthRequired).toHaveBeenCalledWith({
|
|
181
|
-
reason: 'unauthorized',
|
|
182
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
183
|
-
source: 'file',
|
|
184
|
-
expiresAt: undefined,
|
|
185
|
-
statusCode: 401,
|
|
186
|
-
});
|
|
187
|
-
expect(onFeedPolled).not.toHaveBeenCalled();
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test('sends User-Agent header with plugin version', async () => {
|
|
191
|
-
const onFeedPolled = jest.fn().mockResolvedValue(undefined);
|
|
192
|
-
const onAuthRequired = jest.fn().mockResolvedValue(undefined);
|
|
193
|
-
|
|
194
|
-
global.fetch = jest.fn().mockResolvedValue(
|
|
195
|
-
new Response(
|
|
196
|
-
JSON.stringify({
|
|
197
|
-
code: 0,
|
|
198
|
-
msg: 'success',
|
|
199
|
-
data: { items: [], has_more: false, notifications: [] },
|
|
200
|
-
}),
|
|
201
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
202
|
-
)
|
|
203
|
-
) as typeof fetch;
|
|
204
|
-
|
|
205
|
-
const client = new EigenFluxPollingClient({
|
|
206
|
-
apiUrl: 'http://127.0.0.1:8080',
|
|
207
|
-
getAuthState: () => ({
|
|
208
|
-
status: 'available',
|
|
209
|
-
accessToken: 'at_test_token',
|
|
210
|
-
source: 'file',
|
|
211
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
212
|
-
}),
|
|
213
|
-
pollIntervalSec: 60,
|
|
214
|
-
logger: createLogger(),
|
|
215
|
-
onFeedPolled,
|
|
216
|
-
onAuthRequired,
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
await client.pollOnce();
|
|
220
|
-
|
|
221
|
-
const fetchCall = (global.fetch as jest.Mock).mock.calls[0];
|
|
222
|
-
const headers = fetchCall[1].headers;
|
|
223
|
-
expect(headers['User-Agent']).toContain('eigenflux-plugin');
|
|
224
|
-
expect(headers['User-Agent']).toContain('node/');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test('logs detailed fetch failure diagnostics', async () => {
|
|
228
|
-
const loggerSpies = createLoggerSpies();
|
|
229
|
-
const networkCause = Object.assign(
|
|
230
|
-
new Error('connect ECONNREFUSED 127.0.0.1:8080'),
|
|
231
|
-
{
|
|
232
|
-
code: 'ECONNREFUSED',
|
|
233
|
-
errno: -61,
|
|
234
|
-
syscall: 'connect',
|
|
235
|
-
address: '127.0.0.1',
|
|
236
|
-
port: 8080,
|
|
237
|
-
}
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
global.fetch = jest.fn().mockRejectedValue(
|
|
241
|
-
Object.assign(new TypeError('fetch failed'), { cause: networkCause })
|
|
242
|
-
) as typeof fetch;
|
|
243
|
-
|
|
244
|
-
const client = new EigenFluxPollingClient({
|
|
245
|
-
apiUrl: 'http://127.0.0.1:8080',
|
|
246
|
-
getAuthState: () => ({
|
|
247
|
-
status: 'available',
|
|
248
|
-
accessToken: 'at_test_token',
|
|
249
|
-
source: 'file',
|
|
250
|
-
credentialsPath: '/tmp/eigenflux/credentials.json',
|
|
251
|
-
}),
|
|
252
|
-
pollIntervalSec: 60,
|
|
253
|
-
logger: createLogger(loggerSpies),
|
|
254
|
-
onFeedPolled: jest.fn().mockResolvedValue(undefined),
|
|
255
|
-
onAuthRequired: jest.fn().mockResolvedValue(undefined),
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
const result = await client.pollOnce();
|
|
259
|
-
|
|
260
|
-
expect(result.kind).toBe('error');
|
|
261
|
-
expect(loggerSpies.error).toHaveBeenCalledWith(
|
|
262
|
-
expect.stringContaining(
|
|
263
|
-
'[EigenFlux] Failed to poll feed (url=http://127.0.0.1:8080/api/v1/items/feed?action=refresh&limit=20): TypeError: fetch failed'
|
|
264
|
-
)
|
|
265
|
-
);
|
|
266
|
-
expect(loggerSpies.error).toHaveBeenCalledWith(
|
|
267
|
-
expect.stringContaining('cause=Error: connect ECONNREFUSED 127.0.0.1:8080')
|
|
268
|
-
);
|
|
269
|
-
expect(loggerSpies.error).toHaveBeenCalledWith(
|
|
270
|
-
expect.stringContaining('code=ECONNREFUSED')
|
|
271
|
-
);
|
|
272
|
-
expect(loggerSpies.error).toHaveBeenCalledWith(
|
|
273
|
-
expect.stringContaining('address=127.0.0.1')
|
|
274
|
-
);
|
|
275
|
-
expect(loggerSpies.error).toHaveBeenCalledWith(
|
|
276
|
-
expect.stringContaining('port=8080')
|
|
277
|
-
);
|
|
278
|
-
});
|
|
279
|
-
});
|
package/src/polling-client.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Polling client for EigenFlux feed updates
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { AuthState } from './credentials-loader';
|
|
6
|
-
import { PLUGIN_CONFIG } from './config';
|
|
7
|
-
import { Logger } from './logger';
|
|
8
|
-
|
|
9
|
-
export interface FeedItem {
|
|
10
|
-
item_id: string;
|
|
11
|
-
summary?: string;
|
|
12
|
-
broadcast_type: string;
|
|
13
|
-
domains?: string[];
|
|
14
|
-
keywords?: string[];
|
|
15
|
-
group_id?: string;
|
|
16
|
-
source_type?: string;
|
|
17
|
-
url?: string;
|
|
18
|
-
updated_at: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface FeedNotification {
|
|
22
|
-
notification_id: string;
|
|
23
|
-
type: string;
|
|
24
|
-
content: string;
|
|
25
|
-
created_at: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface FeedResponse {
|
|
29
|
-
code: number;
|
|
30
|
-
msg: string;
|
|
31
|
-
data: {
|
|
32
|
-
items: FeedItem[];
|
|
33
|
-
has_more: boolean;
|
|
34
|
-
notifications: FeedNotification[];
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface PollingClientConfig {
|
|
39
|
-
apiUrl: string;
|
|
40
|
-
getAuthState: () => AuthState;
|
|
41
|
-
pollIntervalSec: number;
|
|
42
|
-
logger: Logger;
|
|
43
|
-
onFeedPolled: (payload: FeedResponse) => Promise<void>;
|
|
44
|
-
onAuthRequired: (event: AuthRequiredEvent) => Promise<void>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface AuthRequiredEvent {
|
|
48
|
-
reason: 'missing_token' | 'expired_token' | 'unauthorized';
|
|
49
|
-
credentialsPath: string;
|
|
50
|
-
source?: 'file';
|
|
51
|
-
expiresAt?: number;
|
|
52
|
-
statusCode?: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export type PollResult =
|
|
56
|
-
| {
|
|
57
|
-
kind: 'success';
|
|
58
|
-
payload: FeedResponse;
|
|
59
|
-
}
|
|
60
|
-
| {
|
|
61
|
-
kind: 'auth_required';
|
|
62
|
-
authEvent: AuthRequiredEvent;
|
|
63
|
-
}
|
|
64
|
-
| {
|
|
65
|
-
kind: 'error';
|
|
66
|
-
error: Error;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export interface PollOnceOptions {
|
|
70
|
-
notifyFeed?: boolean;
|
|
71
|
-
notifyAuthRequired?: boolean;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export class EigenFluxPollingClient {
|
|
75
|
-
private config: PollingClientConfig;
|
|
76
|
-
private intervalId: NodeJS.Timeout | null = null;
|
|
77
|
-
private isRunning = false;
|
|
78
|
-
|
|
79
|
-
constructor(config: PollingClientConfig) {
|
|
80
|
-
this.config = config;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async start(): Promise<void> {
|
|
84
|
-
if (this.isRunning) {
|
|
85
|
-
this.config.logger.warn('Polling client already running');
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
this.isRunning = true;
|
|
90
|
-
this.config.logger.info(
|
|
91
|
-
`Starting polling client (interval: ${this.config.pollIntervalSec}s)`
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
// Initial fetch
|
|
95
|
-
await this.pollOnce();
|
|
96
|
-
|
|
97
|
-
// Schedule periodic polling
|
|
98
|
-
this.intervalId = setInterval(() => {
|
|
99
|
-
this.pollOnce().catch((err) => {
|
|
100
|
-
this.config.logger.error(`Polling error: ${this.formatError(err)}`);
|
|
101
|
-
});
|
|
102
|
-
}, this.config.pollIntervalSec * 1000);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
stop(): void {
|
|
106
|
-
if (!this.isRunning) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
this.config.logger.info('Stopping polling client');
|
|
111
|
-
this.isRunning = false;
|
|
112
|
-
|
|
113
|
-
if (this.intervalId) {
|
|
114
|
-
clearInterval(this.intervalId);
|
|
115
|
-
this.intervalId = null;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async pollOnce(options: PollOnceOptions = {}): Promise<PollResult> {
|
|
120
|
-
const notifyFeed = options.notifyFeed ?? true;
|
|
121
|
-
const notifyAuthRequired = options.notifyAuthRequired ?? true;
|
|
122
|
-
const authState = this.config.getAuthState();
|
|
123
|
-
if (authState.status !== 'available') {
|
|
124
|
-
this.config.logger.warn(
|
|
125
|
-
`No usable access token available (status=${authState.status}), skipping poll`
|
|
126
|
-
);
|
|
127
|
-
const authEvent: AuthRequiredEvent = {
|
|
128
|
-
reason: authState.status === 'expired' ? 'expired_token' : 'missing_token',
|
|
129
|
-
credentialsPath: authState.credentialsPath,
|
|
130
|
-
source: authState.source,
|
|
131
|
-
expiresAt: authState.expiresAt,
|
|
132
|
-
};
|
|
133
|
-
if (notifyAuthRequired) {
|
|
134
|
-
await this.config.onAuthRequired(authEvent);
|
|
135
|
-
}
|
|
136
|
-
return {
|
|
137
|
-
kind: 'auth_required',
|
|
138
|
-
authEvent,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const url = `${this.config.apiUrl}/api/v1/items/feed?action=refresh&limit=20`;
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
this.config.logger.info(`Polling feed request: ${url}`);
|
|
146
|
-
this.config.logger.debug(`Polling: ${url}`);
|
|
147
|
-
|
|
148
|
-
const response = await fetch(url, {
|
|
149
|
-
method: 'GET',
|
|
150
|
-
headers: {
|
|
151
|
-
Authorization: `Bearer ${authState.accessToken}`,
|
|
152
|
-
'Content-Type': 'application/json',
|
|
153
|
-
'User-Agent': PLUGIN_CONFIG.USER_AGENT,
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
if (response.status === 401) {
|
|
158
|
-
const authEvent: AuthRequiredEvent = {
|
|
159
|
-
reason: 'unauthorized',
|
|
160
|
-
credentialsPath: authState.credentialsPath,
|
|
161
|
-
source: authState.source,
|
|
162
|
-
expiresAt: authState.expiresAt,
|
|
163
|
-
statusCode: 401,
|
|
164
|
-
};
|
|
165
|
-
if (notifyAuthRequired) {
|
|
166
|
-
await this.config.onAuthRequired(authEvent);
|
|
167
|
-
}
|
|
168
|
-
return {
|
|
169
|
-
kind: 'auth_required',
|
|
170
|
-
authEvent,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (!response.ok) {
|
|
175
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const data = (await response.json()) as FeedResponse;
|
|
179
|
-
|
|
180
|
-
if (data.code !== 0) {
|
|
181
|
-
throw new Error(`API error: ${data.msg}`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const items = data.data.items ?? [];
|
|
185
|
-
const notifications = data.data.notifications ?? [];
|
|
186
|
-
this.config.logger.info(
|
|
187
|
-
`Polled feed: ${items.length} items, notifications=${notifications.length}, has_more=${data.data.has_more}`
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
if (notifyFeed && (items.length > 0 || notifications.length > 0)) {
|
|
191
|
-
await this.config.onFeedPolled(data);
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
kind: 'success',
|
|
195
|
-
payload: data,
|
|
196
|
-
};
|
|
197
|
-
} catch (error) {
|
|
198
|
-
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
199
|
-
this.config.logger.error(
|
|
200
|
-
`Failed to poll feed (url=${url}): ${this.formatError(normalized)}`
|
|
201
|
-
);
|
|
202
|
-
return {
|
|
203
|
-
kind: 'error',
|
|
204
|
-
error: normalized,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
private formatError(error: unknown): string {
|
|
210
|
-
const segments: string[] = [];
|
|
211
|
-
this.appendErrorSegment(segments, error, false);
|
|
212
|
-
return segments.join(' | ');
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
private appendErrorSegment(segments: string[], error: unknown, isCause: boolean): void {
|
|
216
|
-
const prefix = isCause ? 'cause=' : '';
|
|
217
|
-
|
|
218
|
-
if (error instanceof Error) {
|
|
219
|
-
const details: string[] = [`${error.name}: ${error.message}`];
|
|
220
|
-
const metadata = this.errorMetadata(error);
|
|
221
|
-
if (metadata.length > 0) {
|
|
222
|
-
details.push(...metadata);
|
|
223
|
-
}
|
|
224
|
-
segments.push(prefix + details.join(' | '));
|
|
225
|
-
|
|
226
|
-
const cause = (error as Error & { cause?: unknown }).cause;
|
|
227
|
-
if (cause !== undefined) {
|
|
228
|
-
this.appendErrorSegment(segments, cause, true);
|
|
229
|
-
}
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (error && typeof error === 'object') {
|
|
234
|
-
const metadata = this.errorMetadata(error);
|
|
235
|
-
if (metadata.length > 0) {
|
|
236
|
-
segments.push(prefix + metadata.join(' | '));
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
segments.push(prefix + String(error));
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private errorMetadata(value: unknown): string[] {
|
|
245
|
-
if (!value || typeof value !== 'object') {
|
|
246
|
-
return [];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const record = value as {
|
|
250
|
-
code?: unknown;
|
|
251
|
-
errno?: unknown;
|
|
252
|
-
syscall?: unknown;
|
|
253
|
-
address?: unknown;
|
|
254
|
-
port?: unknown;
|
|
255
|
-
status?: unknown;
|
|
256
|
-
statusText?: unknown;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const metadata: string[] = [];
|
|
260
|
-
if (record.code !== undefined) {
|
|
261
|
-
metadata.push(`code=${String(record.code)}`);
|
|
262
|
-
}
|
|
263
|
-
if (record.errno !== undefined) {
|
|
264
|
-
metadata.push(`errno=${String(record.errno)}`);
|
|
265
|
-
}
|
|
266
|
-
if (record.syscall !== undefined) {
|
|
267
|
-
metadata.push(`syscall=${String(record.syscall)}`);
|
|
268
|
-
}
|
|
269
|
-
if (record.address !== undefined) {
|
|
270
|
-
metadata.push(`address=${String(record.address)}`);
|
|
271
|
-
}
|
|
272
|
-
if (record.port !== undefined) {
|
|
273
|
-
metadata.push(`port=${String(record.port)}`);
|
|
274
|
-
}
|
|
275
|
-
if (record.status !== undefined) {
|
|
276
|
-
metadata.push(`status=${String(record.status)}`);
|
|
277
|
-
}
|
|
278
|
-
if (record.statusText !== undefined) {
|
|
279
|
-
metadata.push(`status_text=${String(record.statusText)}`);
|
|
280
|
-
}
|
|
281
|
-
return metadata;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { Logger } from './logger';
|
|
4
|
-
|
|
5
|
-
export type StoredNotificationRoute = {
|
|
6
|
-
sessionKey: string;
|
|
7
|
-
agentId: string;
|
|
8
|
-
replyChannel?: string;
|
|
9
|
-
replyTo?: string;
|
|
10
|
-
replyAccountId?: string;
|
|
11
|
-
updatedAt: number;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
function readNonEmptyString(value: unknown): string | undefined {
|
|
15
|
-
if (typeof value !== 'string') {
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
const trimmed = value.trim();
|
|
19
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function normalizeChannel(value: unknown): string | undefined {
|
|
23
|
-
return readNonEmptyString(value)?.toLowerCase();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function resolveSessionRouteMemoryPath(workdir: string): string {
|
|
27
|
-
return path.join(workdir, 'session.json');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function readStoredNotificationRoute(
|
|
31
|
-
workdir: string | undefined,
|
|
32
|
-
logger: Logger
|
|
33
|
-
): StoredNotificationRoute | undefined {
|
|
34
|
-
const trimmedWorkdir = readNonEmptyString(workdir);
|
|
35
|
-
if (!trimmedWorkdir) {
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const filePath = resolveSessionRouteMemoryPath(trimmedWorkdir);
|
|
40
|
-
try {
|
|
41
|
-
if (!fs.existsSync(filePath)) {
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as unknown;
|
|
46
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const record = parsed as Record<string, unknown>;
|
|
51
|
-
const sessionKey = readNonEmptyString(record.sessionKey);
|
|
52
|
-
const agentId = readNonEmptyString(record.agentId);
|
|
53
|
-
if (!sessionKey || !agentId) {
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
sessionKey,
|
|
59
|
-
agentId,
|
|
60
|
-
replyChannel: normalizeChannel(record.replyChannel),
|
|
61
|
-
replyTo: readNonEmptyString(record.replyTo),
|
|
62
|
-
replyAccountId: readNonEmptyString(record.replyAccountId),
|
|
63
|
-
updatedAt:
|
|
64
|
-
typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt)
|
|
65
|
-
? record.updatedAt
|
|
66
|
-
: 0,
|
|
67
|
-
};
|
|
68
|
-
} catch (error) {
|
|
69
|
-
logger.debug(
|
|
70
|
-
`Failed to read remembered session route ${filePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
71
|
-
);
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function writeStoredNotificationRoute(
|
|
77
|
-
workdir: string | undefined,
|
|
78
|
-
route: Omit<StoredNotificationRoute, 'updatedAt'>,
|
|
79
|
-
logger: Logger
|
|
80
|
-
): boolean {
|
|
81
|
-
const trimmedWorkdir = readNonEmptyString(workdir);
|
|
82
|
-
if (!trimmedWorkdir) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const filePath = resolveSessionRouteMemoryPath(trimmedWorkdir);
|
|
87
|
-
const payload: StoredNotificationRoute = {
|
|
88
|
-
sessionKey: route.sessionKey,
|
|
89
|
-
agentId: route.agentId,
|
|
90
|
-
replyChannel: normalizeChannel(route.replyChannel),
|
|
91
|
-
replyTo: readNonEmptyString(route.replyTo),
|
|
92
|
-
replyAccountId: readNonEmptyString(route.replyAccountId),
|
|
93
|
-
updatedAt: Date.now(),
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
98
|
-
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
|
|
99
|
-
return true;
|
|
100
|
-
} catch (error) {
|
|
101
|
-
logger.warn(
|
|
102
|
-
`Failed to write remembered session route ${filePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
103
|
-
);
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
}
|