@holon-run/agentinbox 0.1.0 → 0.1.1
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 +31 -16
- package/dist/src/adapters.js +38 -31
- package/dist/src/cli.js +274 -66
- package/dist/src/current_agent.js +126 -0
- package/dist/src/http.js +962 -354
- package/dist/src/service.js +94 -10
- package/dist/src/source_schema.js +2 -10
- package/dist/src/sources/feishu.js +16 -15
- package/dist/src/sources/github.js +11 -10
- package/dist/src/sources/github_ci.js +13 -12
- package/dist/src/sources/remote.js +362 -0
- package/dist/src/sources/remote_profiles.js +254 -0
- package/dist/src/store.js +119 -248
- package/dist/src/util.js +19 -3
- package/drizzle/migrations/0000_initial.sql +206 -0
- package/drizzle/migrations/0001_inbox_items_source_occurred_at_idx.sql +3 -0
- package/drizzle/migrations/meta/0001_snapshot.json +1181 -0
- package/drizzle/migrations/meta/_journal.json +20 -0
- package/drizzle/schema.ts +196 -0
- package/package.json +8 -2
- package/dist/src/matcher.js +0 -47
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RemoteSourceRuntime = exports.RpcUxcRemoteSourceClient = void 0;
|
|
4
|
+
const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
|
|
5
|
+
const paths_1 = require("../paths");
|
|
6
|
+
const util_1 = require("../util");
|
|
7
|
+
const remote_profiles_1 = require("./remote_profiles");
|
|
8
|
+
const REMOTE_SOURCE_TYPES = new Set([
|
|
9
|
+
"remote_source",
|
|
10
|
+
"github_repo",
|
|
11
|
+
"github_repo_ci",
|
|
12
|
+
"feishu_bot",
|
|
13
|
+
]);
|
|
14
|
+
const DEFAULT_SYNC_INTERVAL_MS = 2_000;
|
|
15
|
+
const DEFAULT_BACKOFF_BASE_SECS = 2;
|
|
16
|
+
const MAX_ERROR_BACKOFF_MULTIPLIER = 8;
|
|
17
|
+
const MANAGED_SOURCE_NAMESPACE = "agentinbox";
|
|
18
|
+
class RpcUxcRemoteSourceClient {
|
|
19
|
+
client;
|
|
20
|
+
constructor(client = new uxc_daemon_client_1.UxcDaemonClient({ env: process.env })) {
|
|
21
|
+
this.client = client;
|
|
22
|
+
}
|
|
23
|
+
sourceEnsure(args) {
|
|
24
|
+
return this.client.sourceEnsure({
|
|
25
|
+
namespace: args.namespace,
|
|
26
|
+
sourceKey: args.sourceKey,
|
|
27
|
+
spec: {
|
|
28
|
+
...args.spec,
|
|
29
|
+
operation_id: args.spec.operation_id ?? null,
|
|
30
|
+
resource_uri: args.spec.resource_uri ?? null,
|
|
31
|
+
read_resource: args.spec.read_resource ?? false,
|
|
32
|
+
transport_hint: args.spec.transport_hint ?? null,
|
|
33
|
+
subprotocols: args.spec.subprotocols ?? [],
|
|
34
|
+
initial_text_frames: args.spec.initial_text_frames ?? [],
|
|
35
|
+
poll_config: args.spec.poll_config ?? null,
|
|
36
|
+
options: args.spec.options,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async sourceStop(namespace, sourceKey) {
|
|
41
|
+
await this.client.sourceStop(namespace, sourceKey);
|
|
42
|
+
}
|
|
43
|
+
async sourceDelete(namespace, sourceKey) {
|
|
44
|
+
await this.client.sourceDelete(namespace, sourceKey);
|
|
45
|
+
}
|
|
46
|
+
streamRead(args) {
|
|
47
|
+
return this.client.streamRead({
|
|
48
|
+
streamId: args.streamId,
|
|
49
|
+
afterOffset: args.afterOffset ?? 0,
|
|
50
|
+
limit: args.limit ?? 100,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.RpcUxcRemoteSourceClient = RpcUxcRemoteSourceClient;
|
|
55
|
+
class RemoteSourceRuntime {
|
|
56
|
+
store;
|
|
57
|
+
appendSourceEvent;
|
|
58
|
+
profileRegistry;
|
|
59
|
+
client;
|
|
60
|
+
interval = null;
|
|
61
|
+
inFlight = new Set();
|
|
62
|
+
errorCounts = new Map();
|
|
63
|
+
nextRetryAt = new Map();
|
|
64
|
+
homeDir;
|
|
65
|
+
constructor(store, appendSourceEvent, options) {
|
|
66
|
+
this.store = store;
|
|
67
|
+
this.appendSourceEvent = appendSourceEvent;
|
|
68
|
+
this.profileRegistry = options?.profileRegistry ?? new remote_profiles_1.RemoteSourceProfileRegistry();
|
|
69
|
+
this.client = options?.client ?? new RpcUxcRemoteSourceClient();
|
|
70
|
+
this.homeDir = options?.homeDir ?? (0, paths_1.resolveAgentInboxHome)(process.env);
|
|
71
|
+
}
|
|
72
|
+
async ensureSource(source) {
|
|
73
|
+
if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
await this.syncSource(source.sourceId, true);
|
|
77
|
+
}
|
|
78
|
+
async validateSource(source) {
|
|
79
|
+
if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const profile = await this.profileRegistry.resolve(source, this.homeDir);
|
|
83
|
+
const profileSource = profileInputSource(source);
|
|
84
|
+
profile.validateConfig(profileSource);
|
|
85
|
+
profile.buildManagedSourceSpec(profileSource);
|
|
86
|
+
}
|
|
87
|
+
async start() {
|
|
88
|
+
if (this.interval) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.interval = setInterval(() => {
|
|
92
|
+
void this.syncAll();
|
|
93
|
+
}, DEFAULT_SYNC_INTERVAL_MS);
|
|
94
|
+
try {
|
|
95
|
+
await this.syncAll();
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.warn("remote_source initial sync failed:", error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async stop() {
|
|
102
|
+
if (this.interval) {
|
|
103
|
+
clearInterval(this.interval);
|
|
104
|
+
this.interval = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async pollSource(sourceId) {
|
|
108
|
+
const source = this.store.getSource(sourceId);
|
|
109
|
+
if (!source) {
|
|
110
|
+
throw new Error(`unknown source: ${sourceId}`);
|
|
111
|
+
}
|
|
112
|
+
if (source.status === "paused") {
|
|
113
|
+
return {
|
|
114
|
+
sourceId,
|
|
115
|
+
sourceType: source.sourceType,
|
|
116
|
+
appended: 0,
|
|
117
|
+
deduped: 0,
|
|
118
|
+
eventsRead: 0,
|
|
119
|
+
note: "source is paused; resume it before polling",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return this.syncSource(sourceId, true);
|
|
123
|
+
}
|
|
124
|
+
async pauseSource(sourceId) {
|
|
125
|
+
const source = this.store.getSource(sourceId);
|
|
126
|
+
if (!source || !REMOTE_SOURCE_TYPES.has(source.sourceType)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const binding = managedBindingForSource(source);
|
|
130
|
+
await this.client.sourceStop(binding.namespace, binding.sourceKey);
|
|
131
|
+
this.errorCounts.delete(sourceId);
|
|
132
|
+
this.nextRetryAt.delete(sourceId);
|
|
133
|
+
const checkpoint = parseRemoteCheckpoint(source.checkpoint);
|
|
134
|
+
this.store.updateSourceRuntime(sourceId, {
|
|
135
|
+
status: "paused",
|
|
136
|
+
checkpoint: JSON.stringify({
|
|
137
|
+
...checkpoint,
|
|
138
|
+
...(checkpoint.managedSource ? {
|
|
139
|
+
managedSource: {
|
|
140
|
+
namespace: binding.namespace,
|
|
141
|
+
sourceKey: binding.sourceKey,
|
|
142
|
+
streamId: checkpoint.managedSource.streamId,
|
|
143
|
+
},
|
|
144
|
+
} : {}),
|
|
145
|
+
lastError: null,
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async resumeSource(sourceId) {
|
|
150
|
+
const source = this.store.getSource(sourceId);
|
|
151
|
+
if (!source || !REMOTE_SOURCE_TYPES.has(source.sourceType)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
await this.syncSource(sourceId, true);
|
|
155
|
+
}
|
|
156
|
+
async removeSource(sourceId) {
|
|
157
|
+
const source = this.store.getSource(sourceId);
|
|
158
|
+
if (!source || !REMOTE_SOURCE_TYPES.has(source.sourceType)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const binding = managedBindingForSource(source);
|
|
162
|
+
await this.client.sourceDelete(binding.namespace, binding.sourceKey);
|
|
163
|
+
}
|
|
164
|
+
status() {
|
|
165
|
+
return {
|
|
166
|
+
activeSourceIds: Array.from(this.inFlight.values()).sort(),
|
|
167
|
+
erroredSourceIds: Array.from(this.errorCounts.keys()).sort(),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async syncAll() {
|
|
171
|
+
const sources = this.store
|
|
172
|
+
.listSources()
|
|
173
|
+
.filter((source) => REMOTE_SOURCE_TYPES.has(source.sourceType) && source.status !== "paused");
|
|
174
|
+
for (const source of sources) {
|
|
175
|
+
try {
|
|
176
|
+
await this.syncSource(source.sourceId, false);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.warn(`remote source sync failed for ${source.sourceId}:`, error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async syncSource(sourceId, force) {
|
|
184
|
+
if (this.inFlight.has(sourceId)) {
|
|
185
|
+
const source = this.store.getSource(sourceId);
|
|
186
|
+
return {
|
|
187
|
+
sourceId,
|
|
188
|
+
sourceType: source?.sourceType ?? "remote_source",
|
|
189
|
+
appended: 0,
|
|
190
|
+
deduped: 0,
|
|
191
|
+
eventsRead: 0,
|
|
192
|
+
note: "source sync already in flight",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
this.inFlight.add(sourceId);
|
|
196
|
+
try {
|
|
197
|
+
const source = this.store.getSource(sourceId);
|
|
198
|
+
if (!source) {
|
|
199
|
+
throw new Error(`unknown source: ${sourceId}`);
|
|
200
|
+
}
|
|
201
|
+
if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
|
|
202
|
+
return {
|
|
203
|
+
sourceId,
|
|
204
|
+
sourceType: source.sourceType,
|
|
205
|
+
appended: 0,
|
|
206
|
+
deduped: 0,
|
|
207
|
+
eventsRead: 0,
|
|
208
|
+
note: "source type is not hosted by remote runtime",
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (!force && source.status === "error") {
|
|
212
|
+
const retryAt = this.nextRetryAt.get(sourceId) ?? 0;
|
|
213
|
+
if (Date.now() < retryAt) {
|
|
214
|
+
return {
|
|
215
|
+
sourceId,
|
|
216
|
+
sourceType: source.sourceType,
|
|
217
|
+
appended: 0,
|
|
218
|
+
deduped: 0,
|
|
219
|
+
eventsRead: 0,
|
|
220
|
+
note: "error backoff not elapsed",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const profile = await this.profileRegistry.resolve(source, this.homeDir);
|
|
225
|
+
const profileSource = profileInputSource(source);
|
|
226
|
+
profile.validateConfig(profileSource);
|
|
227
|
+
const spec = profile.buildManagedSourceSpec(profileSource);
|
|
228
|
+
const binding = managedBindingForSource(source);
|
|
229
|
+
const ensured = await this.client.sourceEnsure({
|
|
230
|
+
namespace: binding.namespace,
|
|
231
|
+
sourceKey: binding.sourceKey,
|
|
232
|
+
spec,
|
|
233
|
+
});
|
|
234
|
+
const checkpoint = parseRemoteCheckpoint(source.checkpoint);
|
|
235
|
+
const afterOffset = checkpoint.streamCursor?.afterOffset ?? 0;
|
|
236
|
+
const batch = await this.client.streamRead({
|
|
237
|
+
streamId: ensured.stream_id,
|
|
238
|
+
afterOffset,
|
|
239
|
+
limit: 100,
|
|
240
|
+
});
|
|
241
|
+
let appended = 0;
|
|
242
|
+
let deduped = 0;
|
|
243
|
+
for (const event of batch.events) {
|
|
244
|
+
const rawPayload = asRecord(event.raw_payload);
|
|
245
|
+
if (Object.keys(rawPayload).length === 0) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const mapped = profile.mapRawEvent(rawPayload, profileSource);
|
|
249
|
+
if (!mapped) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const result = await this.appendSourceEvent({
|
|
253
|
+
sourceId: source.sourceId,
|
|
254
|
+
sourceNativeId: mapped.sourceNativeId,
|
|
255
|
+
eventVariant: mapped.eventVariant,
|
|
256
|
+
occurredAt: mapped.occurredAt,
|
|
257
|
+
metadata: mapped.metadata,
|
|
258
|
+
rawPayload: mapped.rawPayload,
|
|
259
|
+
deliveryHandle: mapped.deliveryHandle ?? null,
|
|
260
|
+
});
|
|
261
|
+
appended += result.appended;
|
|
262
|
+
deduped += result.deduped;
|
|
263
|
+
}
|
|
264
|
+
const latestSource = this.store.getSource(sourceId);
|
|
265
|
+
if (!latestSource) {
|
|
266
|
+
throw new Error(`unknown source: ${sourceId}`);
|
|
267
|
+
}
|
|
268
|
+
this.store.updateSourceRuntime(sourceId, {
|
|
269
|
+
status: latestSource.status === "paused" ? "paused" : "active",
|
|
270
|
+
checkpoint: JSON.stringify({
|
|
271
|
+
managedSource: {
|
|
272
|
+
namespace: ensured.namespace,
|
|
273
|
+
sourceKey: ensured.source_key,
|
|
274
|
+
streamId: ensured.stream_id,
|
|
275
|
+
},
|
|
276
|
+
streamCursor: {
|
|
277
|
+
afterOffset: batch.next_after_offset,
|
|
278
|
+
},
|
|
279
|
+
lastEventAt: (0, util_1.nowIso)(),
|
|
280
|
+
lastError: null,
|
|
281
|
+
}),
|
|
282
|
+
});
|
|
283
|
+
this.errorCounts.delete(sourceId);
|
|
284
|
+
this.nextRetryAt.delete(sourceId);
|
|
285
|
+
return {
|
|
286
|
+
sourceId,
|
|
287
|
+
sourceType: source.sourceType,
|
|
288
|
+
appended,
|
|
289
|
+
deduped,
|
|
290
|
+
eventsRead: batch.events.length,
|
|
291
|
+
note: `stream=${ensured.stream_id} status=${ensured.status}`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
const source = this.store.getSource(sourceId);
|
|
296
|
+
if (source) {
|
|
297
|
+
const checkpoint = parseRemoteCheckpoint(source.checkpoint);
|
|
298
|
+
const nextErrorCount = (this.errorCounts.get(sourceId) ?? 0) + 1;
|
|
299
|
+
this.errorCounts.set(sourceId, nextErrorCount);
|
|
300
|
+
this.nextRetryAt.set(sourceId, Date.now() + computeErrorBackoffMs(DEFAULT_BACKOFF_BASE_SECS, nextErrorCount));
|
|
301
|
+
this.store.updateSourceRuntime(sourceId, {
|
|
302
|
+
status: source.status === "paused" ? "paused" : "error",
|
|
303
|
+
checkpoint: JSON.stringify({
|
|
304
|
+
...checkpoint,
|
|
305
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
306
|
+
}),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
this.inFlight.delete(sourceId);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
exports.RemoteSourceRuntime = RemoteSourceRuntime;
|
|
317
|
+
function profileInputSource(source) {
|
|
318
|
+
if (source.sourceType !== "remote_source") {
|
|
319
|
+
return source;
|
|
320
|
+
}
|
|
321
|
+
const config = asRecord(source.config);
|
|
322
|
+
return {
|
|
323
|
+
...source,
|
|
324
|
+
config: asRecord(config.profileConfig),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function defaultManagedBindingForSource(source) {
|
|
328
|
+
return {
|
|
329
|
+
namespace: MANAGED_SOURCE_NAMESPACE,
|
|
330
|
+
sourceKey: `${source.sourceType}:${source.sourceKey}`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function managedBindingForSource(source) {
|
|
334
|
+
const checkpoint = parseRemoteCheckpoint(source.checkpoint);
|
|
335
|
+
const defaultBinding = defaultManagedBindingForSource(source);
|
|
336
|
+
return {
|
|
337
|
+
namespace: checkpoint.managedSource?.namespace ?? defaultBinding.namespace,
|
|
338
|
+
sourceKey: checkpoint.managedSource?.sourceKey ?? defaultBinding.sourceKey,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function parseRemoteCheckpoint(checkpoint) {
|
|
342
|
+
if (!checkpoint) {
|
|
343
|
+
return {};
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
return JSON.parse(checkpoint);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return {};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function computeErrorBackoffMs(baseIntervalSecs, errorCount) {
|
|
353
|
+
const baseMs = Math.max(1, baseIntervalSecs) * 1000;
|
|
354
|
+
const multiplier = Math.min(2 ** Math.max(0, errorCount - 1), MAX_ERROR_BACKOFF_MULTIPLIER);
|
|
355
|
+
return baseMs * multiplier;
|
|
356
|
+
}
|
|
357
|
+
function asRecord(value) {
|
|
358
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
359
|
+
return {};
|
|
360
|
+
}
|
|
361
|
+
return value;
|
|
362
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RemoteSourceProfileRegistry = void 0;
|
|
7
|
+
exports.builtInProfileIdForSourceType = builtInProfileIdForSourceType;
|
|
8
|
+
exports.profileConfigForSource = profileConfigForSource;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const node_url_1 = require("node:url");
|
|
12
|
+
const github_1 = require("./github");
|
|
13
|
+
const github_ci_1 = require("./github_ci");
|
|
14
|
+
const feishu_1 = require("./feishu");
|
|
15
|
+
const REMOTE_USER_PROFILE_ROOT_DIR = "source-profiles";
|
|
16
|
+
const BUILTIN_PROFILE_IDS = new Set(["builtin.github_repo", "builtin.github_repo_ci", "builtin.feishu_bot"]);
|
|
17
|
+
class RemoteSourceProfileRegistry {
|
|
18
|
+
profileCache = new Map();
|
|
19
|
+
resolve(source, homeDir) {
|
|
20
|
+
if (source.sourceType === "github_repo") {
|
|
21
|
+
return Promise.resolve(GITHUB_REPO_PROFILE);
|
|
22
|
+
}
|
|
23
|
+
if (source.sourceType === "github_repo_ci") {
|
|
24
|
+
return Promise.resolve(GITHUB_REPO_CI_PROFILE);
|
|
25
|
+
}
|
|
26
|
+
if (source.sourceType === "feishu_bot") {
|
|
27
|
+
return Promise.resolve(FEISHU_BOT_PROFILE);
|
|
28
|
+
}
|
|
29
|
+
if (source.sourceType !== "remote_source") {
|
|
30
|
+
throw new Error(`unsupported source type for remote profile: ${source.sourceType}`);
|
|
31
|
+
}
|
|
32
|
+
return this.loadUserProfile(source, homeDir);
|
|
33
|
+
}
|
|
34
|
+
async loadUserProfile(source, homeDir) {
|
|
35
|
+
const config = source.config ?? {};
|
|
36
|
+
const profilePath = asNonEmptyString(config.profilePath);
|
|
37
|
+
if (!profilePath) {
|
|
38
|
+
throw new Error("remote_source requires config.profilePath");
|
|
39
|
+
}
|
|
40
|
+
const profileRoot = node_path_1.default.resolve(homeDir, REMOTE_USER_PROFILE_ROOT_DIR);
|
|
41
|
+
const resolvedPath = resolveAndValidateProfilePath(profileRoot, profilePath);
|
|
42
|
+
const cacheKey = `${resolvedPath}:${source.sourceKey}`;
|
|
43
|
+
const cached = this.profileCache.get(cacheKey);
|
|
44
|
+
if (cached) {
|
|
45
|
+
return cached;
|
|
46
|
+
}
|
|
47
|
+
if (!node_fs_1.default.existsSync(resolvedPath)) {
|
|
48
|
+
throw new Error(`remote_source profile not found: ${resolvedPath}`);
|
|
49
|
+
}
|
|
50
|
+
const imported = await dynamicImportModule((0, node_url_1.pathToFileURL)(resolvedPath).href);
|
|
51
|
+
const profile = (imported.default ?? imported);
|
|
52
|
+
if (!profile || typeof profile !== "object") {
|
|
53
|
+
throw new Error(`remote_source profile must export default object: ${resolvedPath}`);
|
|
54
|
+
}
|
|
55
|
+
validateProfileContract(profile, resolvedPath);
|
|
56
|
+
this.profileCache.set(cacheKey, profile);
|
|
57
|
+
return profile;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.RemoteSourceProfileRegistry = RemoteSourceProfileRegistry;
|
|
61
|
+
async function dynamicImportModule(moduleUrl) {
|
|
62
|
+
const importer = new Function("url", "return import(url);");
|
|
63
|
+
return importer(moduleUrl);
|
|
64
|
+
}
|
|
65
|
+
function builtInProfileIdForSourceType(sourceType) {
|
|
66
|
+
if (sourceType === "github_repo") {
|
|
67
|
+
return "builtin.github_repo";
|
|
68
|
+
}
|
|
69
|
+
if (sourceType === "github_repo_ci") {
|
|
70
|
+
return "builtin.github_repo_ci";
|
|
71
|
+
}
|
|
72
|
+
if (sourceType === "feishu_bot") {
|
|
73
|
+
return "builtin.feishu_bot";
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function validateProfileContract(profile, sourcePath) {
|
|
78
|
+
if (!profile.id || typeof profile.id !== "string") {
|
|
79
|
+
throw new Error(`remote_source profile id must be a non-empty string: ${sourcePath}`);
|
|
80
|
+
}
|
|
81
|
+
if (BUILTIN_PROFILE_IDS.has(profile.id)) {
|
|
82
|
+
throw new Error(`remote_source profile id is reserved: ${profile.id}`);
|
|
83
|
+
}
|
|
84
|
+
if (typeof profile.validateConfig !== "function") {
|
|
85
|
+
throw new Error(`remote_source profile missing validateConfig(): ${sourcePath}`);
|
|
86
|
+
}
|
|
87
|
+
if (typeof profile.buildManagedSourceSpec !== "function") {
|
|
88
|
+
throw new Error(`remote_source profile missing buildManagedSourceSpec(): ${sourcePath}`);
|
|
89
|
+
}
|
|
90
|
+
if (typeof profile.mapRawEvent !== "function") {
|
|
91
|
+
throw new Error(`remote_source profile missing mapRawEvent(): ${sourcePath}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function resolveAndValidateProfilePath(profileRoot, profilePath) {
|
|
95
|
+
const resolved = node_path_1.default.isAbsolute(profilePath)
|
|
96
|
+
? node_path_1.default.resolve(profilePath)
|
|
97
|
+
: node_path_1.default.resolve(profileRoot, profilePath);
|
|
98
|
+
const normalizedRoot = ensureTrailingSep(node_path_1.default.resolve(profileRoot));
|
|
99
|
+
const normalizedResolved = node_path_1.default.resolve(resolved);
|
|
100
|
+
if (!normalizedResolved.startsWith(normalizedRoot) && normalizedResolved !== node_path_1.default.resolve(profileRoot)) {
|
|
101
|
+
throw new Error(`remote_source profilePath must stay under ${profileRoot}`);
|
|
102
|
+
}
|
|
103
|
+
return normalizedResolved;
|
|
104
|
+
}
|
|
105
|
+
function ensureTrailingSep(input) {
|
|
106
|
+
return input.endsWith(node_path_1.default.sep) ? input : `${input}${node_path_1.default.sep}`;
|
|
107
|
+
}
|
|
108
|
+
function asRecord(value) {
|
|
109
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
function asNonEmptyString(value) {
|
|
115
|
+
if (typeof value !== "string") {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const trimmed = value.trim();
|
|
119
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
120
|
+
}
|
|
121
|
+
const GITHUB_REPO_PROFILE = {
|
|
122
|
+
id: "builtin.github_repo",
|
|
123
|
+
validateConfig(source) {
|
|
124
|
+
(0, github_1.parseGithubSourceConfig)(source);
|
|
125
|
+
},
|
|
126
|
+
buildManagedSourceSpec(source) {
|
|
127
|
+
const config = (0, github_1.parseGithubSourceConfig)(source);
|
|
128
|
+
return {
|
|
129
|
+
endpoint: github_1.GITHUB_ENDPOINT,
|
|
130
|
+
operation_id: "get:/repos/{owner}/{repo}/events",
|
|
131
|
+
args: {
|
|
132
|
+
owner: config.owner,
|
|
133
|
+
repo: config.repo,
|
|
134
|
+
per_page: config.perPage ?? 10,
|
|
135
|
+
},
|
|
136
|
+
mode: "poll",
|
|
137
|
+
poll_config: {
|
|
138
|
+
interval_secs: config.pollIntervalSecs ?? 30,
|
|
139
|
+
extract_items_pointer: "",
|
|
140
|
+
checkpoint_strategy: {
|
|
141
|
+
type: "item_key",
|
|
142
|
+
item_key_pointer: "/id",
|
|
143
|
+
seen_window: 1024,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
options: { auth: config.uxcAuth },
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
mapRawEvent(rawPayload, source) {
|
|
150
|
+
const config = (0, github_1.parseGithubSourceConfig)(source);
|
|
151
|
+
const normalized = (0, github_1.normalizeGithubRepoEvent)(source, config, rawPayload);
|
|
152
|
+
if (!normalized) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
sourceNativeId: normalized.sourceNativeId,
|
|
157
|
+
eventVariant: normalized.eventVariant,
|
|
158
|
+
metadata: normalized.metadata ?? {},
|
|
159
|
+
rawPayload: normalized.rawPayload ?? rawPayload,
|
|
160
|
+
occurredAt: normalized.occurredAt,
|
|
161
|
+
deliveryHandle: normalized.deliveryHandle,
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
const GITHUB_REPO_CI_PROFILE = {
|
|
166
|
+
id: "builtin.github_repo_ci",
|
|
167
|
+
validateConfig(source) {
|
|
168
|
+
(0, github_ci_1.parseGithubCiSourceConfig)(source);
|
|
169
|
+
},
|
|
170
|
+
buildManagedSourceSpec(source) {
|
|
171
|
+
const config = (0, github_ci_1.parseGithubCiSourceConfig)(source);
|
|
172
|
+
const args = {
|
|
173
|
+
owner: config.owner,
|
|
174
|
+
repo: config.repo,
|
|
175
|
+
per_page: config.perPage ?? github_ci_1.DEFAULT_GITHUB_CI_PER_PAGE,
|
|
176
|
+
};
|
|
177
|
+
if (config.eventFilter) {
|
|
178
|
+
args.event = config.eventFilter;
|
|
179
|
+
}
|
|
180
|
+
if (config.branch) {
|
|
181
|
+
args.branch = config.branch;
|
|
182
|
+
}
|
|
183
|
+
if (config.statusFilter) {
|
|
184
|
+
args.status = config.statusFilter;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
endpoint: github_ci_1.GITHUB_CI_ENDPOINT,
|
|
188
|
+
operation_id: "get:/repos/{owner}/{repo}/actions/runs",
|
|
189
|
+
args,
|
|
190
|
+
mode: "poll",
|
|
191
|
+
poll_config: {
|
|
192
|
+
interval_secs: config.pollIntervalSecs ?? github_ci_1.DEFAULT_GITHUB_CI_POLL_INTERVAL_SECS,
|
|
193
|
+
extract_items_pointer: "/workflow_runs",
|
|
194
|
+
checkpoint_strategy: {
|
|
195
|
+
type: "content_hash",
|
|
196
|
+
seen_window: 1024,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
options: { auth: config.uxcAuth },
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
mapRawEvent(rawPayload, source) {
|
|
203
|
+
const config = (0, github_ci_1.parseGithubCiSourceConfig)(source);
|
|
204
|
+
const normalized = (0, github_ci_1.normalizeGithubWorkflowRunEvent)(source, config, rawPayload);
|
|
205
|
+
if (!normalized) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
sourceNativeId: normalized.sourceNativeId,
|
|
210
|
+
eventVariant: normalized.eventVariant,
|
|
211
|
+
metadata: normalized.metadata ?? {},
|
|
212
|
+
rawPayload: normalized.rawPayload ?? rawPayload,
|
|
213
|
+
occurredAt: normalized.occurredAt,
|
|
214
|
+
deliveryHandle: normalized.deliveryHandle,
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
const FEISHU_BOT_PROFILE = {
|
|
219
|
+
id: "builtin.feishu_bot",
|
|
220
|
+
validateConfig(source) {
|
|
221
|
+
(0, feishu_1.parseFeishuSourceConfig)(source);
|
|
222
|
+
},
|
|
223
|
+
buildManagedSourceSpec(source) {
|
|
224
|
+
const config = (0, feishu_1.parseFeishuSourceConfig)(source);
|
|
225
|
+
return {
|
|
226
|
+
endpoint: config.endpoint ?? feishu_1.FEISHU_OPENAPI_ENDPOINT,
|
|
227
|
+
mode: "stream",
|
|
228
|
+
transport_hint: "feishu_long_connection",
|
|
229
|
+
options: { auth: config.uxcAuth },
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
mapRawEvent(rawPayload, source) {
|
|
233
|
+
const config = (0, feishu_1.parseFeishuSourceConfig)(source);
|
|
234
|
+
const normalized = (0, feishu_1.normalizeFeishuBotEvent)(source, config, rawPayload);
|
|
235
|
+
if (!normalized) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
sourceNativeId: normalized.sourceNativeId,
|
|
240
|
+
eventVariant: normalized.eventVariant,
|
|
241
|
+
metadata: normalized.metadata ?? {},
|
|
242
|
+
rawPayload: normalized.rawPayload ?? rawPayload,
|
|
243
|
+
occurredAt: normalized.occurredAt,
|
|
244
|
+
deliveryHandle: normalized.deliveryHandle,
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
function profileConfigForSource(source) {
|
|
249
|
+
const config = asRecord(source.config);
|
|
250
|
+
if (source.sourceType === "remote_source") {
|
|
251
|
+
return asRecord(config.profileConfig);
|
|
252
|
+
}
|
|
253
|
+
return config;
|
|
254
|
+
}
|