@holon-run/agentinbox 1.3.2 → 1.4.0
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/src/adapters.js +13 -0
- package/dist/src/cli.js +4 -4
- package/dist/src/service.js +78 -23
- package/dist/src/source_hosts.js +16 -0
- package/dist/src/source_schema.js +35 -0
- package/dist/src/sources/remote.js +92 -1
- package/dist/src/sources/remote_modules.js +88 -2
- package/dist/src/sources/telegram.js +317 -0
- package/dist/src/store.js +16 -25
- package/dist/src/store_queries.js +24 -0
- package/package.json +2 -2
package/dist/src/adapters.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.AdapterRegistry = void 0;
|
|
|
4
4
|
const paths_1 = require("./paths");
|
|
5
5
|
const feishu_1 = require("./sources/feishu");
|
|
6
6
|
const github_1 = require("./sources/github");
|
|
7
|
+
const telegram_1 = require("./sources/telegram");
|
|
7
8
|
const remote_1 = require("./sources/remote");
|
|
8
9
|
const remote_modules_1 = require("./sources/remote_modules");
|
|
9
10
|
const source_resolution_1 = require("./source_resolution");
|
|
@@ -40,6 +41,7 @@ class AdapterRegistry {
|
|
|
40
41
|
defaultDelivery = new NoopDeliveryAdapter();
|
|
41
42
|
feishuDelivery = new feishu_1.FeishuDeliveryAdapter();
|
|
42
43
|
githubDelivery = new github_1.GithubDeliveryAdapter();
|
|
44
|
+
telegramDelivery = new telegram_1.TelegramDeliveryAdapter();
|
|
43
45
|
homeDir;
|
|
44
46
|
remoteModuleRegistry;
|
|
45
47
|
constructor(store, appendSourceEvent, options) {
|
|
@@ -51,6 +53,7 @@ class AdapterRegistry {
|
|
|
51
53
|
homeDir: this.homeDir,
|
|
52
54
|
client: options?.remoteSourceClient,
|
|
53
55
|
moduleRegistry: this.remoteModuleRegistry,
|
|
56
|
+
telegramClient: options?.telegramClient,
|
|
54
57
|
});
|
|
55
58
|
}
|
|
56
59
|
sourceAdapterFor(type) {
|
|
@@ -69,6 +72,9 @@ class AdapterRegistry {
|
|
|
69
72
|
if (type === "feishu_bot") {
|
|
70
73
|
return this.remoteSource;
|
|
71
74
|
}
|
|
75
|
+
if (type === "telegram_bot") {
|
|
76
|
+
return this.remoteSource;
|
|
77
|
+
}
|
|
72
78
|
return this.localEventSource;
|
|
73
79
|
}
|
|
74
80
|
deliveryAdapterFor(provider) {
|
|
@@ -78,6 +84,9 @@ class AdapterRegistry {
|
|
|
78
84
|
if (provider === "github") {
|
|
79
85
|
return this.githubDelivery;
|
|
80
86
|
}
|
|
87
|
+
if (provider === "telegram") {
|
|
88
|
+
return this.telegramDelivery;
|
|
89
|
+
}
|
|
81
90
|
return this.defaultDelivery;
|
|
82
91
|
}
|
|
83
92
|
async start() {
|
|
@@ -238,6 +247,9 @@ class AdapterRegistry {
|
|
|
238
247
|
if (handle.provider === "feishu") {
|
|
239
248
|
return this.remoteModuleRegistry.resolve(syntheticBuiltinSource("feishu_bot"), this.homeDir);
|
|
240
249
|
}
|
|
250
|
+
if (handle.provider === "telegram") {
|
|
251
|
+
return this.remoteModuleRegistry.resolve(syntheticBuiltinSource("telegram_bot"), this.homeDir);
|
|
252
|
+
}
|
|
241
253
|
return null;
|
|
242
254
|
}
|
|
243
255
|
}
|
|
@@ -248,6 +260,7 @@ function isExplicitFollowPreviewRef(value) {
|
|
|
248
260
|
value === "github_repo" ||
|
|
249
261
|
value === "github_repo_ci" ||
|
|
250
262
|
value === "feishu_bot" ||
|
|
263
|
+
value === "telegram_bot" ||
|
|
251
264
|
value.startsWith("remote:"));
|
|
252
265
|
}
|
|
253
266
|
function hasRemoteSourceModulePath(config) {
|
package/dist/src/cli.js
CHANGED
|
@@ -794,12 +794,12 @@ async function main() {
|
|
|
794
794
|
}
|
|
795
795
|
if (command === "inbox" && normalized[1] === "ack") {
|
|
796
796
|
const args = normalized.slice(2);
|
|
797
|
-
const itemId = takeFlagValue(normalized, "--entry");
|
|
797
|
+
const itemId = takeFlagValue(normalized, "--entry-id") ?? takeFlagValue(normalized, "--entry");
|
|
798
798
|
const throughEntryId = takeFlagValue(normalized, "--through");
|
|
799
799
|
const ackAll = hasFlag(normalized, "--all");
|
|
800
800
|
const modeCount = Number(Boolean(itemId)) + Number(Boolean(throughEntryId)) + Number(ackAll);
|
|
801
|
-
if (positionalArgs(args, ["--agent-id", "--entry", "--through"]).length > 0 || modeCount !== 1) {
|
|
802
|
-
throw new Error("usage: agentinbox inbox ack [--agent-id ID] (--through <entryId> | --entry <entryId> | --all)");
|
|
801
|
+
if (positionalArgs(args, ["--agent-id", "--entry", "--entry-id", "--through"]).length > 0 || modeCount !== 1) {
|
|
802
|
+
throw new Error("usage: agentinbox inbox ack [--agent-id ID] (--through <entryId> | --entry <entryId> | --entry-id <entryId> | --all)");
|
|
803
803
|
}
|
|
804
804
|
const selection = await selectAgentForCommand(client, {
|
|
805
805
|
explicitAgentId: takeFlagValue(normalized, "--agent-id"),
|
|
@@ -1571,7 +1571,7 @@ Usage:
|
|
|
1571
1571
|
agentinbox inbox read [--agent-id ID] [--after-entry ID] [--include-acked] [--limit N]
|
|
1572
1572
|
agentinbox inbox send --agent-id ID --message TEXT [--sender SENDER]
|
|
1573
1573
|
agentinbox inbox watch [--agent-id ID] [--after-entry ID] [--include-acked] [--heartbeat-ms N]
|
|
1574
|
-
agentinbox inbox ack [--agent-id ID] (--through <entryId> | --entry <entryId> | --all)
|
|
1574
|
+
agentinbox inbox ack [--agent-id ID] (--through <entryId> | --entry <entryId> | --entry-id <entryId> | --all)
|
|
1575
1575
|
agentinbox inbox policy show [--agent-id ID]
|
|
1576
1576
|
agentinbox inbox policy set [--agent-id ID] [--enabled|--disabled] [--window-ms N] [--max-items N] [--max-thread-age-ms N]
|
|
1577
1577
|
agentinbox inbox compact <agentId>
|
package/dist/src/service.js
CHANGED
|
@@ -1697,6 +1697,15 @@ class AgentInboxService {
|
|
|
1697
1697
|
}
|
|
1698
1698
|
async status() {
|
|
1699
1699
|
const sources = await this.listSourceViews();
|
|
1700
|
+
const agents = this.store.listAgents();
|
|
1701
|
+
const subscriptions = this.store.listSubscriptions();
|
|
1702
|
+
const inboxes = this.store.listInboxes();
|
|
1703
|
+
const activationTargets = this.store.listActivationTargets();
|
|
1704
|
+
const activationDispatchStates = this.store.listActivationDispatchStates();
|
|
1705
|
+
const streams = this.store.listStreams();
|
|
1706
|
+
const consumers = this.store.listConsumers();
|
|
1707
|
+
const recentActivations = this.store.listActivations().slice(0, 10);
|
|
1708
|
+
const recentDeliveries = this.store.listDeliveries().slice(0, 10);
|
|
1700
1709
|
return {
|
|
1701
1710
|
retention: {
|
|
1702
1711
|
ackedInboxItemsMs: this.ackedRetentionMs,
|
|
@@ -1704,17 +1713,30 @@ class AgentInboxService {
|
|
|
1704
1713
|
lastAckedInboxGcAt: this.lastAckedInboxGcAt > 0 ? new Date(this.lastAckedInboxGcAt).toISOString() : null,
|
|
1705
1714
|
},
|
|
1706
1715
|
counts: this.store.getCounts(),
|
|
1707
|
-
|
|
1716
|
+
diagnostics: {
|
|
1717
|
+
database: this.store.getDatabaseHealth(),
|
|
1718
|
+
daemon: { status: "running" },
|
|
1719
|
+
sources: summarizeByStatus(sources, ["active", "paused", "error"]),
|
|
1720
|
+
agents: summarizeByStatus(agents, ["active", "offline"]),
|
|
1721
|
+
activationTargets: summarizeByStatus(activationTargets, ["active", "offline"]),
|
|
1722
|
+
activationDispatch: {
|
|
1723
|
+
...summarizeByStatus(activationDispatchStates, ["dirty", "notified"]),
|
|
1724
|
+
pendingNewItems: activationDispatchStates.reduce((total, state) => total + state.pendingNewItemCount, 0),
|
|
1725
|
+
deferredAttempts: activationDispatchStates.reduce((total, state) => total + state.deferAttempts, 0),
|
|
1726
|
+
},
|
|
1727
|
+
deliveries: summarizeByStatus(recentDeliveries, ["accepted", "sent", "failed"]),
|
|
1728
|
+
},
|
|
1729
|
+
agents,
|
|
1708
1730
|
sources,
|
|
1709
|
-
subscriptions
|
|
1710
|
-
inboxes
|
|
1711
|
-
activationTargets
|
|
1712
|
-
activationDispatchStates
|
|
1713
|
-
streams
|
|
1714
|
-
consumers
|
|
1731
|
+
subscriptions,
|
|
1732
|
+
inboxes,
|
|
1733
|
+
activationTargets,
|
|
1734
|
+
activationDispatchStates,
|
|
1735
|
+
streams,
|
|
1736
|
+
consumers,
|
|
1715
1737
|
adapters: this.adapters.status(),
|
|
1716
|
-
recentActivations
|
|
1717
|
-
recentDeliveries
|
|
1738
|
+
recentActivations,
|
|
1739
|
+
recentDeliveries,
|
|
1718
1740
|
lifecycle: {
|
|
1719
1741
|
offlineAgentTtlMs: DEFAULT_OFFLINE_AGENT_TTL_MS,
|
|
1720
1742
|
gcIntervalMs: DEFAULT_GC_INTERVAL_MS,
|
|
@@ -1993,6 +2015,7 @@ class AgentInboxService {
|
|
|
1993
2015
|
buffer = {
|
|
1994
2016
|
agentId: target.agentId,
|
|
1995
2017
|
targetId: target.targetId,
|
|
2018
|
+
targetKind: target.kind,
|
|
1996
2019
|
pending: [],
|
|
1997
2020
|
timer: null,
|
|
1998
2021
|
inFlight: false,
|
|
@@ -2081,21 +2104,24 @@ class AgentInboxService {
|
|
|
2081
2104
|
return;
|
|
2082
2105
|
}
|
|
2083
2106
|
const state = this.store.getActivationDispatchState(buffer.agentId, buffer.targetId);
|
|
2084
|
-
const shouldAttemptDispatch = !state
|
|
2107
|
+
const shouldAttemptDispatch = !state
|
|
2108
|
+
|| (state.status === "dirty" && state.leaseExpiresAt == null)
|
|
2109
|
+
|| (state.status === "notified" && buffer.targetKind === "webhook");
|
|
2085
2110
|
if (shouldAttemptDispatch) {
|
|
2111
|
+
const pendingState = state?.status === "dirty" ? state : null;
|
|
2086
2112
|
const inbox = this.ensureInboxForAgent(buffer.agentId);
|
|
2087
2113
|
const unackedItems = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false });
|
|
2088
2114
|
const dispatched = await this.dispatchActivationTarget({
|
|
2089
2115
|
agentId: buffer.agentId,
|
|
2090
2116
|
targetId: buffer.targetId,
|
|
2091
|
-
newItemCount:
|
|
2117
|
+
newItemCount: pendingState ? pendingState.pendingNewItemCount + entries.length : entries.length,
|
|
2092
2118
|
totalUnackedCount: unackedItems.length,
|
|
2093
|
-
summary:
|
|
2094
|
-
subscriptionIds:
|
|
2095
|
-
? uniqueSortedNullable([...
|
|
2119
|
+
summary: pendingState ? latestSummary(entries) ?? pendingState.pendingSummary : latestSummary(entries),
|
|
2120
|
+
subscriptionIds: pendingState
|
|
2121
|
+
? uniqueSortedNullable([...pendingState.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)])
|
|
2096
2122
|
: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
|
|
2097
|
-
sourceIds:
|
|
2098
|
-
? uniqueSorted([...
|
|
2123
|
+
sourceIds: pendingState
|
|
2124
|
+
? uniqueSorted([...pendingState.pendingSourceIds, ...entries.map((entry) => entry.sourceId)])
|
|
2099
2125
|
: uniqueSorted(entries.map((entry) => entry.sourceId)),
|
|
2100
2126
|
entries: unackedItems,
|
|
2101
2127
|
});
|
|
@@ -2104,22 +2130,22 @@ class AgentInboxService {
|
|
|
2104
2130
|
this.store.deleteActivationDispatchState(buffer.agentId, buffer.targetId);
|
|
2105
2131
|
}
|
|
2106
2132
|
if (dispatched === "retryable_failure") {
|
|
2107
|
-
if (
|
|
2133
|
+
if (pendingState && pendingState.leaseExpiresAt == null) {
|
|
2108
2134
|
this.store.upsertActivationDispatchState({
|
|
2109
2135
|
agentId: buffer.agentId,
|
|
2110
2136
|
targetId: buffer.targetId,
|
|
2111
2137
|
status: "dirty",
|
|
2112
2138
|
leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
|
|
2113
|
-
lastNotifiedFingerprint:
|
|
2139
|
+
lastNotifiedFingerprint: pendingState.lastNotifiedFingerprint,
|
|
2114
2140
|
deferReason: null,
|
|
2115
2141
|
deferAttempts: 0,
|
|
2116
2142
|
firstDeferredAt: null,
|
|
2117
2143
|
lastDeferredAt: null,
|
|
2118
2144
|
pendingFingerprint: null,
|
|
2119
|
-
pendingNewItemCount:
|
|
2120
|
-
pendingSummary: latestSummary(entries) ??
|
|
2121
|
-
pendingSubscriptionIds: uniqueSortedNullable([...
|
|
2122
|
-
pendingSourceIds: uniqueSorted([...
|
|
2145
|
+
pendingNewItemCount: pendingState.pendingNewItemCount + entries.length,
|
|
2146
|
+
pendingSummary: latestSummary(entries) ?? pendingState.pendingSummary,
|
|
2147
|
+
pendingSubscriptionIds: uniqueSortedNullable([...pendingState.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]),
|
|
2148
|
+
pendingSourceIds: uniqueSorted([...pendingState.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]),
|
|
2123
2149
|
updatedAt: (0, util_1.nowIso)(),
|
|
2124
2150
|
});
|
|
2125
2151
|
}
|
|
@@ -3104,7 +3130,12 @@ function mergeFollowPreviewConfig(config, templateArgs) {
|
|
|
3104
3130
|
};
|
|
3105
3131
|
}
|
|
3106
3132
|
function sourceTypeForPreviewRef(sourceRef) {
|
|
3107
|
-
if (sourceRef === "local_event" ||
|
|
3133
|
+
if (sourceRef === "local_event" ||
|
|
3134
|
+
sourceRef === "remote_source" ||
|
|
3135
|
+
sourceRef === "github_repo" ||
|
|
3136
|
+
sourceRef === "github_repo_ci" ||
|
|
3137
|
+
sourceRef === "feishu_bot" ||
|
|
3138
|
+
sourceRef === "telegram_bot") {
|
|
3108
3139
|
return sourceRef;
|
|
3109
3140
|
}
|
|
3110
3141
|
if (sourceRef.startsWith("remote:")) {
|
|
@@ -3403,6 +3434,9 @@ function providerForSourceType(sourceType) {
|
|
|
3403
3434
|
if (sourceType === "feishu_bot") {
|
|
3404
3435
|
return "feishu";
|
|
3405
3436
|
}
|
|
3437
|
+
if (sourceType === "telegram_bot") {
|
|
3438
|
+
return "telegram";
|
|
3439
|
+
}
|
|
3406
3440
|
return null;
|
|
3407
3441
|
}
|
|
3408
3442
|
class ActivationDispatcher {
|
|
@@ -3972,6 +4006,8 @@ function listHostStreamKinds(hostType) {
|
|
|
3972
4006
|
return ["repo_events", "ci_runs"];
|
|
3973
4007
|
case "feishu":
|
|
3974
4008
|
return ["message_events"];
|
|
4009
|
+
case "telegram":
|
|
4010
|
+
return ["message_updates"];
|
|
3975
4011
|
case "local_event":
|
|
3976
4012
|
return ["events"];
|
|
3977
4013
|
case "remote_source":
|
|
@@ -4015,6 +4051,16 @@ function deliveryActionsHelp(handle) {
|
|
|
4015
4051
|
function isKnownDeliverySurface(handle) {
|
|
4016
4052
|
return KNOWN_DELIVERY_SURFACES[handle.provider]?.includes(handle.surface) === true;
|
|
4017
4053
|
}
|
|
4054
|
+
function summarizeByStatus(records, expectedStatuses) {
|
|
4055
|
+
const summary = { total: records.length };
|
|
4056
|
+
for (const status of expectedStatuses) {
|
|
4057
|
+
summary[status] = 0;
|
|
4058
|
+
}
|
|
4059
|
+
for (const record of records) {
|
|
4060
|
+
summary[record.status] = (summary[record.status] ?? 0) + 1;
|
|
4061
|
+
}
|
|
4062
|
+
return summary;
|
|
4063
|
+
}
|
|
4018
4064
|
function getHostConfigFields(hostType) {
|
|
4019
4065
|
switch (hostType) {
|
|
4020
4066
|
case "github":
|
|
@@ -4025,6 +4071,12 @@ function getHostConfigFields(hostType) {
|
|
|
4025
4071
|
return [
|
|
4026
4072
|
{ name: "uxcAuth", type: "string", description: "Optional shared Feishu/Lark UXC auth profile.", required: false },
|
|
4027
4073
|
];
|
|
4074
|
+
case "telegram":
|
|
4075
|
+
return [
|
|
4076
|
+
{ name: "botToken", type: "string", description: "Telegram Bot API token.", required: false },
|
|
4077
|
+
{ name: "tokenEnv", type: "string", description: "Environment variable containing the Telegram Bot API token.", required: false },
|
|
4078
|
+
{ name: "botUsername", type: "string", description: "Optional bot username used for shared host identity.", required: false },
|
|
4079
|
+
];
|
|
4028
4080
|
case "local_event":
|
|
4029
4081
|
return [];
|
|
4030
4082
|
case "remote_source":
|
|
@@ -4041,6 +4093,9 @@ function sourceTypeForStreamRegistration(hostType, streamKind) {
|
|
|
4041
4093
|
if (hostType === "feishu") {
|
|
4042
4094
|
return "feishu_bot";
|
|
4043
4095
|
}
|
|
4096
|
+
if (hostType === "telegram") {
|
|
4097
|
+
return "telegram_bot";
|
|
4098
|
+
}
|
|
4044
4099
|
if (hostType === "local_event") {
|
|
4045
4100
|
return "local_event";
|
|
4046
4101
|
}
|
package/dist/src/source_hosts.js
CHANGED
|
@@ -44,6 +44,22 @@ function resolveSourceRegistration(input) {
|
|
|
44
44
|
sourceType: input.sourceType,
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
+
if (input.sourceType === "telegram_bot") {
|
|
48
|
+
return {
|
|
49
|
+
hostType: "telegram",
|
|
50
|
+
hostKey: `bot:${stringOrDefault(config.botUsername, input.configRef ?? input.sourceKey)}`,
|
|
51
|
+
hostConfig: {
|
|
52
|
+
...(valueOrUndefined(config.botToken) ? { botToken: config.botToken } : {}),
|
|
53
|
+
...(valueOrUndefined(config.tokenEnv) ? { tokenEnv: config.tokenEnv } : {}),
|
|
54
|
+
...(valueOrUndefined(config.apiBaseUrl) ? { apiBaseUrl: config.apiBaseUrl } : {}),
|
|
55
|
+
...(valueOrUndefined(config.botUsername) ? { botUsername: config.botUsername } : {}),
|
|
56
|
+
},
|
|
57
|
+
streamKind: "message_updates",
|
|
58
|
+
streamKey: input.sourceKey,
|
|
59
|
+
streamConfig: config,
|
|
60
|
+
sourceType: input.sourceType,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
47
63
|
if (input.sourceType === "local_event") {
|
|
48
64
|
return {
|
|
49
65
|
hostType: "local_event",
|
|
@@ -154,6 +154,41 @@ const SOURCE_SCHEMAS = {
|
|
|
154
154
|
{ name: "replyInThread", type: "boolean", required: false, description: "Reply in thread when sending outbound messages." },
|
|
155
155
|
],
|
|
156
156
|
},
|
|
157
|
+
telegram_bot: {
|
|
158
|
+
sourceType: "telegram_bot",
|
|
159
|
+
metadataFields: [
|
|
160
|
+
{ name: "updateId", type: "string", description: "Telegram update_id." },
|
|
161
|
+
{ name: "chatId", type: "string", description: "Telegram chat ID." },
|
|
162
|
+
{ name: "chatType", type: "string|null", description: "Telegram chat type such as private, group, or channel." },
|
|
163
|
+
{ name: "messageId", type: "string", description: "Telegram message_id." },
|
|
164
|
+
{ name: "messageType", type: "string", description: "Normalized Telegram message type such as text or photo." },
|
|
165
|
+
{ name: "fromId", type: "string|null", description: "Telegram sender user ID when present." },
|
|
166
|
+
{ name: "fromUsername", type: "string|null", description: "Telegram sender username when present." },
|
|
167
|
+
{ name: "fromFirstName", type: "string|null", description: "Telegram sender first_name when present." },
|
|
168
|
+
{ name: "content", type: "string|null", description: "Message text or caption when present." },
|
|
169
|
+
],
|
|
170
|
+
payloadExamples: [
|
|
171
|
+
{
|
|
172
|
+
update_id: 123,
|
|
173
|
+
message: {
|
|
174
|
+
message_id: 7,
|
|
175
|
+
date: 1710000000,
|
|
176
|
+
text: "hello",
|
|
177
|
+
chat: { id: 456, type: "private" },
|
|
178
|
+
from: { id: 111, username: "operator", first_name: "Op" },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
eventVariantExamples: ["message.text", "edited_message.text", "channel_post.photo"],
|
|
183
|
+
configFields: [
|
|
184
|
+
{ name: "botToken", type: "string", required: false, description: "Telegram bot token. Prefer tokenEnv for shared/local configuration." },
|
|
185
|
+
{ name: "tokenEnv", type: "string", required: false, description: "Environment variable name containing the Telegram bot token." },
|
|
186
|
+
{ name: "botUsername", type: "string", required: false, description: "Optional bot username used for source host identity." },
|
|
187
|
+
{ name: "chatIds", type: "string[]", required: false, description: "Optional Telegram chat ID allowlist." },
|
|
188
|
+
{ name: "allowedUpdates", type: "string[]", required: false, description: "Optional Telegram Bot API allowed_updates value." },
|
|
189
|
+
{ name: "endpoint", type: "string", required: false, description: "Optional Telegram Bot API endpoint override." },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
157
192
|
};
|
|
158
193
|
function getSourceSchema(sourceType) {
|
|
159
194
|
const schema = SOURCE_SCHEMAS[sourceType];
|
|
@@ -5,11 +5,13 @@ const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
|
|
|
5
5
|
const paths_1 = require("../paths");
|
|
6
6
|
const util_1 = require("../util");
|
|
7
7
|
const remote_modules_1 = require("./remote_modules");
|
|
8
|
+
const telegram_1 = require("./telegram");
|
|
8
9
|
const REMOTE_SOURCE_TYPES = new Set([
|
|
9
10
|
"remote_source",
|
|
10
11
|
"github_repo",
|
|
11
12
|
"github_repo_ci",
|
|
12
13
|
"feishu_bot",
|
|
14
|
+
"telegram_bot",
|
|
13
15
|
]);
|
|
14
16
|
const DEFAULT_SYNC_INTERVAL_MS = 2_000;
|
|
15
17
|
const DEFAULT_BACKOFF_BASE_SECS = 2;
|
|
@@ -21,6 +23,7 @@ class RpcUxcRemoteSourceClient {
|
|
|
21
23
|
this.client = client;
|
|
22
24
|
}
|
|
23
25
|
sourceEnsure(args) {
|
|
26
|
+
const transportHint = args.spec.transport_hint ?? null;
|
|
24
27
|
return this.client.sourceEnsure({
|
|
25
28
|
namespace: args.namespace,
|
|
26
29
|
sourceKey: args.sourceKey,
|
|
@@ -29,7 +32,7 @@ class RpcUxcRemoteSourceClient {
|
|
|
29
32
|
operation_id: args.spec.operation_id ?? null,
|
|
30
33
|
resource_uri: args.spec.resource_uri ?? null,
|
|
31
34
|
read_resource: args.spec.read_resource ?? false,
|
|
32
|
-
transport_hint:
|
|
35
|
+
transport_hint: transportHint,
|
|
33
36
|
subprotocols: args.spec.subprotocols ?? [],
|
|
34
37
|
initial_text_frames: args.spec.initial_text_frames ?? [],
|
|
35
38
|
poll_config: args.spec.poll_config ?? null,
|
|
@@ -68,12 +71,14 @@ class RemoteSourceRuntime {
|
|
|
68
71
|
errorCounts = new Map();
|
|
69
72
|
nextRetryAt = new Map();
|
|
70
73
|
homeDir;
|
|
74
|
+
telegramClient;
|
|
71
75
|
constructor(store, appendSourceEvent, options) {
|
|
72
76
|
this.store = store;
|
|
73
77
|
this.appendSourceEvent = appendSourceEvent;
|
|
74
78
|
this.moduleRegistry = options?.moduleRegistry ?? new remote_modules_1.RemoteSourceModuleRegistry();
|
|
75
79
|
this.client = options?.client ?? new RpcUxcRemoteSourceClient();
|
|
76
80
|
this.homeDir = options?.homeDir ?? (0, paths_1.resolveAgentInboxHome)(process.env);
|
|
81
|
+
this.telegramClient = options?.telegramClient ?? new telegram_1.TelegramBotApiClient();
|
|
77
82
|
}
|
|
78
83
|
async ensureSource(source) {
|
|
79
84
|
if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
|
|
@@ -380,6 +385,9 @@ class RemoteSourceRuntime {
|
|
|
380
385
|
};
|
|
381
386
|
}
|
|
382
387
|
}
|
|
388
|
+
if (source.sourceType === "telegram_bot") {
|
|
389
|
+
return await this.syncTelegramSource(source);
|
|
390
|
+
}
|
|
383
391
|
const module = await this.moduleRegistry.resolve(source, this.homeDir);
|
|
384
392
|
const moduleSource = moduleInputSource(source);
|
|
385
393
|
module.validateConfig(moduleSource);
|
|
@@ -474,6 +482,70 @@ class RemoteSourceRuntime {
|
|
|
474
482
|
this.inFlight.delete(sourceId);
|
|
475
483
|
}
|
|
476
484
|
}
|
|
485
|
+
async syncTelegramSource(source) {
|
|
486
|
+
const config = (0, telegram_1.parseTelegramSourceConfig)(source);
|
|
487
|
+
const botToken = config.botToken ?? tokenFromEnv(config.tokenEnv);
|
|
488
|
+
if (!botToken) {
|
|
489
|
+
throw new Error("Telegram source requires config.botToken or tokenEnv");
|
|
490
|
+
}
|
|
491
|
+
const checkpoint = parseRemoteCheckpoint(source.checkpoint);
|
|
492
|
+
const updates = await this.telegramClient.getUpdates({
|
|
493
|
+
endpoint: config.endpoint,
|
|
494
|
+
botToken,
|
|
495
|
+
offset: checkpoint.telegramCursor?.nextOffset,
|
|
496
|
+
timeout: 0,
|
|
497
|
+
allowedUpdates: config.allowedUpdates,
|
|
498
|
+
});
|
|
499
|
+
let appended = 0;
|
|
500
|
+
let deduped = 0;
|
|
501
|
+
let nextOffset = checkpoint.telegramCursor?.nextOffset ?? 0;
|
|
502
|
+
for (const update of updates) {
|
|
503
|
+
const normalized = (0, telegram_1.normalizeTelegramBotUpdate)(source, config, update);
|
|
504
|
+
const updateId = numberFromUnknown(update.update_id);
|
|
505
|
+
if (updateId !== undefined) {
|
|
506
|
+
nextOffset = Math.max(nextOffset, updateId + 1);
|
|
507
|
+
}
|
|
508
|
+
if (!normalized) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const result = await this.appendSourceEvent(normalized);
|
|
512
|
+
appended += result.appended;
|
|
513
|
+
deduped += result.deduped;
|
|
514
|
+
}
|
|
515
|
+
const latestSource = this.store.getSource(source.sourceId);
|
|
516
|
+
if (!latestSource) {
|
|
517
|
+
throw new Error(`unknown source: ${source.sourceId}`);
|
|
518
|
+
}
|
|
519
|
+
const observedAt = (0, util_1.nowIso)();
|
|
520
|
+
this.store.updateSourceRuntime(source.sourceId, {
|
|
521
|
+
status: latestSource.status === "paused" ? "paused" : "active",
|
|
522
|
+
checkpoint: JSON.stringify({
|
|
523
|
+
...checkpoint,
|
|
524
|
+
telegramCursor: { nextOffset },
|
|
525
|
+
managedRuntime: {
|
|
526
|
+
status: "running",
|
|
527
|
+
runId: null,
|
|
528
|
+
streamId: null,
|
|
529
|
+
lastError: null,
|
|
530
|
+
updatedAt: observedAt,
|
|
531
|
+
startedAt: checkpoint.managedRuntime?.startedAt ?? observedAt,
|
|
532
|
+
stoppedAt: null,
|
|
533
|
+
},
|
|
534
|
+
lastEventAt: observedAt,
|
|
535
|
+
lastError: null,
|
|
536
|
+
}),
|
|
537
|
+
});
|
|
538
|
+
this.errorCounts.delete(source.sourceId);
|
|
539
|
+
this.nextRetryAt.delete(source.sourceId);
|
|
540
|
+
return {
|
|
541
|
+
sourceId: source.sourceId,
|
|
542
|
+
sourceType: source.sourceType,
|
|
543
|
+
appended,
|
|
544
|
+
deduped,
|
|
545
|
+
eventsRead: updates.length,
|
|
546
|
+
note: "telegram getUpdates poll completed",
|
|
547
|
+
};
|
|
548
|
+
}
|
|
477
549
|
}
|
|
478
550
|
exports.RemoteSourceRuntime = RemoteSourceRuntime;
|
|
479
551
|
function normalizeExpandedSubscriptionPlan(value) {
|
|
@@ -529,6 +601,25 @@ function managedBindingForSource(source) {
|
|
|
529
601
|
function managedSourceBindingKey(namespace, sourceKey) {
|
|
530
602
|
return `${namespace}:${sourceKey}`;
|
|
531
603
|
}
|
|
604
|
+
function tokenFromEnv(name) {
|
|
605
|
+
if (!name) {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
const value = process.env[name];
|
|
609
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
610
|
+
}
|
|
611
|
+
function numberFromUnknown(value) {
|
|
612
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
613
|
+
return value;
|
|
614
|
+
}
|
|
615
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
616
|
+
const parsed = Number(value);
|
|
617
|
+
if (Number.isFinite(parsed)) {
|
|
618
|
+
return parsed;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
532
623
|
function parseRemoteCheckpoint(checkpoint) {
|
|
533
624
|
if (!checkpoint) {
|
|
534
625
|
return {};
|
|
@@ -14,9 +14,10 @@ const node_url_1 = require("node:url");
|
|
|
14
14
|
const github_1 = require("./github");
|
|
15
15
|
const github_ci_1 = require("./github_ci");
|
|
16
16
|
const feishu_1 = require("./feishu");
|
|
17
|
+
const telegram_1 = require("./telegram");
|
|
17
18
|
const REMOTE_USER_MODULE_ROOT_DIR = "source-modules";
|
|
18
|
-
const BUILTIN_MODULE_IDS = new Set(["builtin.github_repo", "builtin.github_repo_ci", "builtin.feishu_bot"]);
|
|
19
|
-
const BUILTIN_REMOTE_SOURCE_TYPES = ["github_repo", "github_repo_ci", "feishu_bot"];
|
|
19
|
+
const BUILTIN_MODULE_IDS = new Set(["builtin.github_repo", "builtin.github_repo_ci", "builtin.feishu_bot", "builtin.telegram_bot"]);
|
|
20
|
+
const BUILTIN_REMOTE_SOURCE_TYPES = ["github_repo", "github_repo_ci", "feishu_bot", "telegram_bot"];
|
|
20
21
|
class RemoteSourceModuleRegistry {
|
|
21
22
|
moduleCache = new Map();
|
|
22
23
|
githubRepoModule;
|
|
@@ -33,6 +34,9 @@ class RemoteSourceModuleRegistry {
|
|
|
33
34
|
if (source.sourceType === "feishu_bot") {
|
|
34
35
|
return Promise.resolve(FEISHU_BOT_MODULE);
|
|
35
36
|
}
|
|
37
|
+
if (source.sourceType === "telegram_bot") {
|
|
38
|
+
return Promise.resolve(TELEGRAM_BOT_MODULE);
|
|
39
|
+
}
|
|
36
40
|
if (source.sourceType !== "remote_source") {
|
|
37
41
|
throw new Error(`unsupported source type for remote module: ${source.sourceType}`);
|
|
38
42
|
}
|
|
@@ -79,6 +83,9 @@ function builtInModuleIdForSourceType(sourceType) {
|
|
|
79
83
|
if (sourceType === "feishu_bot") {
|
|
80
84
|
return "builtin.feishu_bot";
|
|
81
85
|
}
|
|
86
|
+
if (sourceType === "telegram_bot") {
|
|
87
|
+
return "builtin.telegram_bot";
|
|
88
|
+
}
|
|
82
89
|
return null;
|
|
83
90
|
}
|
|
84
91
|
function builtinRemoteSourceTypes() {
|
|
@@ -543,6 +550,85 @@ const FEISHU_BOT_MODULE = {
|
|
|
543
550
|
};
|
|
544
551
|
},
|
|
545
552
|
};
|
|
553
|
+
const TELEGRAM_BOT_MODULE = {
|
|
554
|
+
id: "builtin.telegram_bot",
|
|
555
|
+
listDeliveryOperations(input) {
|
|
556
|
+
return (0, telegram_1.telegramDeliveryOperationsForHandle)(input.handle);
|
|
557
|
+
},
|
|
558
|
+
async invokeDeliveryOperation(input) {
|
|
559
|
+
return (0, telegram_1.invokeTelegramDeliveryOperation)(input.handle, input.operation, input.input);
|
|
560
|
+
},
|
|
561
|
+
describeCapabilities() {
|
|
562
|
+
return {
|
|
563
|
+
sourceKind: "telegram_bot",
|
|
564
|
+
aliases: ["telegram_bot"],
|
|
565
|
+
configSchema: [
|
|
566
|
+
{ name: "botToken", type: "string", required: false, description: "Telegram bot token. Prefer tokenEnv for shared/local configuration." },
|
|
567
|
+
{ name: "tokenEnv", type: "string", required: false, description: "Environment variable name containing the Telegram bot token." },
|
|
568
|
+
{ name: "botUsername", type: "string", required: false, description: "Optional bot username used for source host identity." },
|
|
569
|
+
{ name: "chatIds", type: "string[]", required: false, description: "Optional Telegram chat ID allowlist." },
|
|
570
|
+
{ name: "allowedUpdates", type: "string[]", required: false, description: "Optional Telegram Bot API allowed_updates value." },
|
|
571
|
+
{ name: "endpoint", type: "string", required: false, description: "Optional Telegram Bot API endpoint override." },
|
|
572
|
+
],
|
|
573
|
+
metadataFields: [
|
|
574
|
+
{ name: "updateId", type: "string", description: "Telegram update_id." },
|
|
575
|
+
{ name: "chatId", type: "string", description: "Telegram chat ID." },
|
|
576
|
+
{ name: "chatType", type: "string|null", description: "Telegram chat type such as private, group, or channel." },
|
|
577
|
+
{ name: "messageId", type: "string", description: "Telegram message_id." },
|
|
578
|
+
{ name: "messageType", type: "string", description: "Normalized Telegram message type such as text or photo." },
|
|
579
|
+
{ name: "fromId", type: "string|null", description: "Telegram sender user ID when present." },
|
|
580
|
+
{ name: "fromUsername", type: "string|null", description: "Telegram sender username when present." },
|
|
581
|
+
{ name: "fromFirstName", type: "string|null", description: "Telegram sender first_name when present." },
|
|
582
|
+
{ name: "content", type: "string|null", description: "Message text or caption when present." },
|
|
583
|
+
],
|
|
584
|
+
eventVariantExamples: ["message.text", "edited_message.text", "channel_post.photo"],
|
|
585
|
+
};
|
|
586
|
+
},
|
|
587
|
+
validateConfig(source) {
|
|
588
|
+
(0, telegram_1.parseTelegramSourceConfig)(source);
|
|
589
|
+
},
|
|
590
|
+
buildManagedSourceSpec(source) {
|
|
591
|
+
const config = (0, telegram_1.parseTelegramSourceConfig)(source);
|
|
592
|
+
return {
|
|
593
|
+
endpoint: config.endpoint ?? telegram_1.TELEGRAM_BOT_API_ENDPOINT,
|
|
594
|
+
operation_id: "getUpdates",
|
|
595
|
+
mode: "poll",
|
|
596
|
+
args: {
|
|
597
|
+
timeout: 5,
|
|
598
|
+
...(config.botToken ? { botToken: config.botToken } : {}),
|
|
599
|
+
...(config.tokenEnv ? { tokenEnv: config.tokenEnv } : {}),
|
|
600
|
+
...(config.chatIds && config.chatIds.length > 0 ? { chatIds: config.chatIds } : {}),
|
|
601
|
+
...(config.allowedUpdates && config.allowedUpdates.length > 0 ? { allowed_updates: config.allowedUpdates } : {}),
|
|
602
|
+
},
|
|
603
|
+
poll_config: {
|
|
604
|
+
interval_secs: 2,
|
|
605
|
+
extract_items_pointer: "/result",
|
|
606
|
+
request_cursor_arg: "offset",
|
|
607
|
+
cursor_from_item_pointer: "/update_id",
|
|
608
|
+
cursor_transform: "increment",
|
|
609
|
+
checkpoint_strategy: {
|
|
610
|
+
type: "item_key",
|
|
611
|
+
item_key_pointer: "/update_id",
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
},
|
|
616
|
+
mapRawEvent(rawPayload, source) {
|
|
617
|
+
const config = (0, telegram_1.parseTelegramSourceConfig)(source);
|
|
618
|
+
const normalized = (0, telegram_1.normalizeTelegramBotUpdate)(source, config, rawPayload);
|
|
619
|
+
if (!normalized) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
sourceNativeId: normalized.sourceNativeId,
|
|
624
|
+
eventVariant: normalized.eventVariant,
|
|
625
|
+
metadata: normalized.metadata ?? {},
|
|
626
|
+
rawPayload: normalized.rawPayload ?? rawPayload,
|
|
627
|
+
occurredAt: normalized.occurredAt,
|
|
628
|
+
deliveryHandle: normalized.deliveryHandle,
|
|
629
|
+
};
|
|
630
|
+
},
|
|
631
|
+
};
|
|
546
632
|
function moduleConfigForSource(source) {
|
|
547
633
|
const config = asRecord(source.config);
|
|
548
634
|
if (source.sourceType === "remote_source") {
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TelegramDeliveryAdapter = exports.TelegramBotApiClient = exports.TELEGRAM_BOT_API_ENDPOINT = void 0;
|
|
4
|
+
exports.telegramDeliveryOperationsForHandle = telegramDeliveryOperationsForHandle;
|
|
5
|
+
exports.invokeTelegramDeliveryOperation = invokeTelegramDeliveryOperation;
|
|
6
|
+
exports.normalizeTelegramBotUpdate = normalizeTelegramBotUpdate;
|
|
7
|
+
exports.parseTelegramSourceConfig = parseTelegramSourceConfig;
|
|
8
|
+
exports.TELEGRAM_BOT_API_ENDPOINT = "https://api.telegram.org";
|
|
9
|
+
class TelegramBotApiClient {
|
|
10
|
+
client;
|
|
11
|
+
constructor(client = { fetch: (url, init) => fetch(url, init) }) {
|
|
12
|
+
this.client = client;
|
|
13
|
+
}
|
|
14
|
+
async getUpdates(input) {
|
|
15
|
+
const endpoint = stripTrailingSlash(input.endpoint ?? exports.TELEGRAM_BOT_API_ENDPOINT);
|
|
16
|
+
const params = new URLSearchParams();
|
|
17
|
+
if (input.offset !== undefined) {
|
|
18
|
+
params.set("offset", String(input.offset));
|
|
19
|
+
}
|
|
20
|
+
if (input.timeout !== undefined) {
|
|
21
|
+
params.set("timeout", String(input.timeout));
|
|
22
|
+
}
|
|
23
|
+
if (input.allowedUpdates && input.allowedUpdates.length > 0) {
|
|
24
|
+
params.set("allowed_updates", JSON.stringify(input.allowedUpdates));
|
|
25
|
+
}
|
|
26
|
+
const query = params.toString();
|
|
27
|
+
const response = await this.client.fetch(`${endpoint}/bot${input.botToken}/getUpdates${query ? `?${query}` : ""}`);
|
|
28
|
+
const body = await response.text();
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`Telegram getUpdates failed with status ${response.status}: ${body}`);
|
|
31
|
+
}
|
|
32
|
+
const parsed = JSON.parse(body);
|
|
33
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
34
|
+
throw new Error("Telegram getUpdates returned a non-object response");
|
|
35
|
+
}
|
|
36
|
+
const envelope = parsed;
|
|
37
|
+
if (envelope.ok !== true) {
|
|
38
|
+
throw new Error(`Telegram getUpdates returned ok=false: ${body}`);
|
|
39
|
+
}
|
|
40
|
+
const result = envelope.result;
|
|
41
|
+
if (!Array.isArray(result)) {
|
|
42
|
+
throw new Error("Telegram getUpdates response missing result array");
|
|
43
|
+
}
|
|
44
|
+
return result
|
|
45
|
+
.map((item) => asRecord(item))
|
|
46
|
+
.filter((item) => Object.keys(item).length > 0);
|
|
47
|
+
}
|
|
48
|
+
async sendMessage(input) {
|
|
49
|
+
const endpoint = stripTrailingSlash(input.endpoint ?? exports.TELEGRAM_BOT_API_ENDPOINT);
|
|
50
|
+
const body = {
|
|
51
|
+
chat_id: input.chatId,
|
|
52
|
+
text: input.text,
|
|
53
|
+
};
|
|
54
|
+
if (input.replyToMessageId !== undefined) {
|
|
55
|
+
body.reply_to_message_id = input.replyToMessageId;
|
|
56
|
+
}
|
|
57
|
+
if (input.parseMode) {
|
|
58
|
+
body.parse_mode = input.parseMode;
|
|
59
|
+
}
|
|
60
|
+
const response = await this.client.fetch(`${endpoint}/bot${input.botToken}/sendMessage`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "content-type": "application/json" },
|
|
63
|
+
body: JSON.stringify(body),
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(`Telegram sendMessage failed with status ${response.status}: ${await response.text()}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.TelegramBotApiClient = TelegramBotApiClient;
|
|
71
|
+
class TelegramDeliveryAdapter {
|
|
72
|
+
client;
|
|
73
|
+
constructor(client) {
|
|
74
|
+
this.client = client ?? new TelegramBotApiClient();
|
|
75
|
+
}
|
|
76
|
+
async send(request, attempt) {
|
|
77
|
+
return invokeTelegramDeliveryOperation(attempt, "send_text", request.payload, this.client);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.TelegramDeliveryAdapter = TelegramDeliveryAdapter;
|
|
81
|
+
function telegramDeliveryOperationsForHandle(handle) {
|
|
82
|
+
if (handle.provider !== "telegram") {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
if (handle.surface !== "message_reply" && handle.surface !== "chat_message") {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
return [{
|
|
89
|
+
name: "send_text",
|
|
90
|
+
title: handle.surface === "message_reply" ? "Reply With Text" : "Send Text Message",
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
additionalProperties: false,
|
|
94
|
+
required: ["text"],
|
|
95
|
+
properties: {
|
|
96
|
+
text: { type: "string", minLength: 1 },
|
|
97
|
+
endpoint: { type: "string", minLength: 1 },
|
|
98
|
+
botToken: { type: "string", minLength: 1 },
|
|
99
|
+
token: { type: "string", minLength: 1 },
|
|
100
|
+
tokenEnv: { type: "string", minLength: 1 },
|
|
101
|
+
chatId: { type: "string", minLength: 1 },
|
|
102
|
+
chat_id: { type: "string", minLength: 1 },
|
|
103
|
+
replyToMessageId: { type: "number" },
|
|
104
|
+
reply_to_message_id: { type: "number" },
|
|
105
|
+
parseMode: { type: "string", minLength: 1 },
|
|
106
|
+
parse_mode: { type: "string", minLength: 1 },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
canonicalTextAlias: true,
|
|
110
|
+
}];
|
|
111
|
+
}
|
|
112
|
+
async function invokeTelegramDeliveryOperation(handle, operation, input, client = new TelegramBotApiClient()) {
|
|
113
|
+
if (operation !== "send_text") {
|
|
114
|
+
throw new Error(`unknown Telegram delivery operation: ${operation}`);
|
|
115
|
+
}
|
|
116
|
+
if (handle.provider !== "telegram") {
|
|
117
|
+
throw new Error(`Telegram delivery operation requires a telegram handle, got ${handle.provider}`);
|
|
118
|
+
}
|
|
119
|
+
if (handle.surface !== "message_reply" && handle.surface !== "chat_message") {
|
|
120
|
+
throw new Error(`deliver send only supports canonical Telegram text surfaces; use deliver invoke for ${handle.surface}`);
|
|
121
|
+
}
|
|
122
|
+
const text = asString(input.text);
|
|
123
|
+
if (!text || text.trim().length === 0) {
|
|
124
|
+
throw new Error("send_text requires input.text");
|
|
125
|
+
}
|
|
126
|
+
const botToken = asString(input.botToken)
|
|
127
|
+
?? asString(input.token)
|
|
128
|
+
?? tokenFromEnv(asString(input.tokenEnv));
|
|
129
|
+
if (!botToken) {
|
|
130
|
+
throw new Error("send_text requires input.botToken, input.token, or input.tokenEnv");
|
|
131
|
+
}
|
|
132
|
+
const chatId = stringFromUnknown(input.chatId) ?? stringFromUnknown(input.chat_id) ?? handle.targetRef;
|
|
133
|
+
const replyToMessageId = numberFromUnknown(input.replyToMessageId)
|
|
134
|
+
?? numberFromUnknown(input.reply_to_message_id)
|
|
135
|
+
?? (handle.surface === "message_reply" ? numberFromUnknown(handle.threadRef) : undefined);
|
|
136
|
+
await client.sendMessage({
|
|
137
|
+
endpoint: asString(input.endpoint) ?? undefined,
|
|
138
|
+
botToken,
|
|
139
|
+
chatId,
|
|
140
|
+
text,
|
|
141
|
+
replyToMessageId,
|
|
142
|
+
parseMode: asString(input.parseMode) ?? asString(input.parse_mode) ?? undefined,
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
status: "sent",
|
|
146
|
+
note: handle.surface === "message_reply" ? "sent Telegram message reply" : "sent Telegram chat message",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function normalizeTelegramBotUpdate(source, config, raw) {
|
|
150
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const payload = raw;
|
|
154
|
+
const updateId = stringFromUnknown(payload.update_id);
|
|
155
|
+
if (!updateId) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const selected = selectTelegramMessage(payload);
|
|
159
|
+
if (!selected) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const message = selected.message;
|
|
163
|
+
const messageId = stringFromUnknown(message.message_id);
|
|
164
|
+
const chat = asRecord(message.chat);
|
|
165
|
+
const chatId = stringFromUnknown(chat.id);
|
|
166
|
+
if (!messageId || !chatId) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
if (config.chatIds && config.chatIds.length > 0 && !config.chatIds.includes(chatId)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const from = asRecord(message.from);
|
|
173
|
+
const content = asString(message.text) ?? asString(message.caption);
|
|
174
|
+
const messageType = telegramMessageType(message);
|
|
175
|
+
return {
|
|
176
|
+
sourceId: source.sourceId,
|
|
177
|
+
sourceNativeId: `telegram:${source.sourceId}:${updateId}`,
|
|
178
|
+
eventVariant: `${selected.kind}.${messageType}`,
|
|
179
|
+
occurredAt: fromUnixSeconds(message.date) ?? new Date().toISOString(),
|
|
180
|
+
metadata: {
|
|
181
|
+
provider: "telegram",
|
|
182
|
+
updateId,
|
|
183
|
+
chatId,
|
|
184
|
+
chatType: asString(chat.type),
|
|
185
|
+
messageId,
|
|
186
|
+
messageType,
|
|
187
|
+
fromId: stringFromUnknown(from.id),
|
|
188
|
+
fromUsername: asString(from.username),
|
|
189
|
+
fromFirstName: asString(from.first_name),
|
|
190
|
+
content,
|
|
191
|
+
},
|
|
192
|
+
rawPayload: payload,
|
|
193
|
+
deliveryHandle: {
|
|
194
|
+
provider: "telegram",
|
|
195
|
+
surface: "message_reply",
|
|
196
|
+
targetRef: chatId,
|
|
197
|
+
threadRef: messageId,
|
|
198
|
+
replyMode: "reply",
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function parseTelegramSourceConfig(source) {
|
|
203
|
+
const config = source.config ?? {};
|
|
204
|
+
return {
|
|
205
|
+
endpoint: asString(config.endpoint) ?? asString(config.apiBaseUrl) ?? exports.TELEGRAM_BOT_API_ENDPOINT,
|
|
206
|
+
botToken: asString(config.botToken) ?? undefined,
|
|
207
|
+
tokenEnv: asString(config.tokenEnv) ?? undefined,
|
|
208
|
+
botUsername: asString(config.botUsername) ?? undefined,
|
|
209
|
+
chatIds: asStringArray(config.chatIds) ?? undefined,
|
|
210
|
+
allowedUpdates: asStringArray(config.allowedUpdates) ?? undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function selectTelegramMessage(payload) {
|
|
214
|
+
const candidates = [
|
|
215
|
+
["message", payload.message],
|
|
216
|
+
["edited_message", payload.edited_message],
|
|
217
|
+
["channel_post", payload.channel_post],
|
|
218
|
+
["edited_channel_post", payload.edited_channel_post],
|
|
219
|
+
];
|
|
220
|
+
for (const [kind, value] of candidates) {
|
|
221
|
+
const message = asRecord(value);
|
|
222
|
+
if (Object.keys(message).length > 0) {
|
|
223
|
+
return { kind, message };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
function telegramMessageType(message) {
|
|
229
|
+
if (typeof message.text === "string") {
|
|
230
|
+
return "text";
|
|
231
|
+
}
|
|
232
|
+
if (typeof message.caption === "string") {
|
|
233
|
+
return "caption";
|
|
234
|
+
}
|
|
235
|
+
if (Array.isArray(message.photo)) {
|
|
236
|
+
return "photo";
|
|
237
|
+
}
|
|
238
|
+
if (message.document && typeof message.document === "object") {
|
|
239
|
+
return "document";
|
|
240
|
+
}
|
|
241
|
+
if (message.audio && typeof message.audio === "object") {
|
|
242
|
+
return "audio";
|
|
243
|
+
}
|
|
244
|
+
if (message.video && typeof message.video === "object") {
|
|
245
|
+
return "video";
|
|
246
|
+
}
|
|
247
|
+
if (message.voice && typeof message.voice === "object") {
|
|
248
|
+
return "voice";
|
|
249
|
+
}
|
|
250
|
+
if (message.sticker && typeof message.sticker === "object") {
|
|
251
|
+
return "sticker";
|
|
252
|
+
}
|
|
253
|
+
return "unknown";
|
|
254
|
+
}
|
|
255
|
+
function fromUnixSeconds(value) {
|
|
256
|
+
const seconds = numberFromUnknown(value);
|
|
257
|
+
if (seconds === undefined) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return new Date(seconds * 1000).toISOString();
|
|
261
|
+
}
|
|
262
|
+
function tokenFromEnv(name) {
|
|
263
|
+
if (!name) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
return asString(process.env[name]);
|
|
267
|
+
}
|
|
268
|
+
function stripTrailingSlash(value) {
|
|
269
|
+
return value.replace(/\/+$/, "");
|
|
270
|
+
}
|
|
271
|
+
function asRecord(value) {
|
|
272
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
273
|
+
return {};
|
|
274
|
+
}
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
function asString(value) {
|
|
278
|
+
return typeof value === "string" ? value : null;
|
|
279
|
+
}
|
|
280
|
+
function asStringArray(value) {
|
|
281
|
+
if (Array.isArray(value)) {
|
|
282
|
+
return value
|
|
283
|
+
.map((item) => stringFromUnknown(item))
|
|
284
|
+
.filter((item) => Boolean(item));
|
|
285
|
+
}
|
|
286
|
+
if (typeof value === "string") {
|
|
287
|
+
return value
|
|
288
|
+
.split(",")
|
|
289
|
+
.map((item) => item.trim())
|
|
290
|
+
.filter((item) => item.length > 0);
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
function stringFromUnknown(value) {
|
|
295
|
+
if (typeof value === "string") {
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
299
|
+
return String(value);
|
|
300
|
+
}
|
|
301
|
+
if (typeof value === "bigint") {
|
|
302
|
+
return value.toString();
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
function numberFromUnknown(value) {
|
|
307
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
311
|
+
const parsed = Number(value);
|
|
312
|
+
if (Number.isFinite(parsed)) {
|
|
313
|
+
return parsed;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
package/dist/src/store.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.AgentInboxStore = void 0;
|
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
10
|
+
const store_queries_1 = require("./store_queries");
|
|
10
11
|
const util_1 = require("./util");
|
|
11
12
|
const DRIZZLE_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
12
13
|
const V1_BASELINE_TAG = "0000_v1_initial";
|
|
@@ -130,6 +131,13 @@ class AgentInboxStore {
|
|
|
130
131
|
save() {
|
|
131
132
|
this.persist();
|
|
132
133
|
}
|
|
134
|
+
getDatabaseHealth() {
|
|
135
|
+
return {
|
|
136
|
+
integrityCheck: String(this.db.pragma("integrity_check", { simple: true })),
|
|
137
|
+
journalMode: String(this.db.pragma("journal_mode", { simple: true })),
|
|
138
|
+
foreignKeys: Number(this.db.pragma("foreign_keys", { simple: true })) === 1,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
133
141
|
async backupHealthyDatabase() {
|
|
134
142
|
const backupPath = `${this.dbPath}.bak`;
|
|
135
143
|
const tmpPath = `${backupPath}.${process.pid}.tmp`;
|
|
@@ -667,40 +675,23 @@ class AgentInboxStore {
|
|
|
667
675
|
this.persist();
|
|
668
676
|
}
|
|
669
677
|
listSubscriptions(limit) {
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
: this.getAll("select * from subscriptions order by created_at asc, subscription_id asc");
|
|
678
|
+
const query = (0, store_queries_1.buildSubscriptionListQuery)({ limit });
|
|
679
|
+
const rows = this.getAll(query.sql, query.params);
|
|
673
680
|
return rows.map((row) => this.mapSubscription(row));
|
|
674
681
|
}
|
|
675
682
|
listSubscriptionsFiltered(filters) {
|
|
676
|
-
const
|
|
677
|
-
const
|
|
678
|
-
if (filters?.sourceId) {
|
|
679
|
-
clauses.push("source_id = ?");
|
|
680
|
-
params.push(filters.sourceId);
|
|
681
|
-
}
|
|
682
|
-
if (filters?.agentId) {
|
|
683
|
-
clauses.push("agent_id = ?");
|
|
684
|
-
params.push(filters.agentId);
|
|
685
|
-
}
|
|
686
|
-
const where = clauses.length > 0 ? ` where ${clauses.join(" and ")}` : "";
|
|
687
|
-
const limitClause = typeof filters?.limit === "number" ? " limit ?" : "";
|
|
688
|
-
if (typeof filters?.limit === "number") {
|
|
689
|
-
params.push(filters.limit);
|
|
690
|
-
}
|
|
691
|
-
const rows = this.getAll(`select * from subscriptions${where} order by created_at asc, subscription_id asc${limitClause}`, params);
|
|
683
|
+
const query = (0, store_queries_1.buildSubscriptionListQuery)(filters);
|
|
684
|
+
const rows = this.getAll(query.sql, query.params);
|
|
692
685
|
return rows.map((row) => this.mapSubscription(row));
|
|
693
686
|
}
|
|
694
687
|
listSubscriptionsForSource(sourceId, limit) {
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
: this.getAll("select * from subscriptions where source_id = ? order by created_at asc, subscription_id asc", [sourceId]);
|
|
688
|
+
const query = (0, store_queries_1.buildSubscriptionListQuery)({ sourceId, limit });
|
|
689
|
+
const rows = this.getAll(query.sql, query.params);
|
|
698
690
|
return rows.map((row) => this.mapSubscription(row));
|
|
699
691
|
}
|
|
700
692
|
listSubscriptionsForAgent(agentId, limit) {
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
: this.getAll("select * from subscriptions where agent_id = ? order by created_at asc, subscription_id asc", [agentId]);
|
|
693
|
+
const query = (0, store_queries_1.buildSubscriptionListQuery)({ agentId, limit });
|
|
694
|
+
const rows = this.getAll(query.sql, query.params);
|
|
704
695
|
return rows.map((row) => this.mapSubscription(row));
|
|
705
696
|
}
|
|
706
697
|
listSubscriptionsForHostTrackedResourceRef(hostId, trackedResourceRef) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSubscriptionListQuery = buildSubscriptionListQuery;
|
|
4
|
+
function buildSubscriptionListQuery(filters) {
|
|
5
|
+
const clauses = [];
|
|
6
|
+
const params = [];
|
|
7
|
+
if (filters?.sourceId) {
|
|
8
|
+
clauses.push("source_id = ?");
|
|
9
|
+
params.push(filters.sourceId);
|
|
10
|
+
}
|
|
11
|
+
if (filters?.agentId) {
|
|
12
|
+
clauses.push("agent_id = ?");
|
|
13
|
+
params.push(filters.agentId);
|
|
14
|
+
}
|
|
15
|
+
const where = clauses.length > 0 ? ` where ${clauses.join(" and ")}` : "";
|
|
16
|
+
const limitClause = typeof filters?.limit === "number" ? " limit ?" : "";
|
|
17
|
+
if (typeof filters?.limit === "number") {
|
|
18
|
+
params.push(filters.limit);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
sql: `select * from subscriptions${where} order by created_at asc, subscription_id asc${limitClause}`,
|
|
22
|
+
params,
|
|
23
|
+
};
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holon-run/agentinbox",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Local event subscription and delivery service for agents.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@fastify/swagger": "^9.5.2",
|
|
47
|
-
"@holon-run/uxc-daemon-client": "^0.15.
|
|
47
|
+
"@holon-run/uxc-daemon-client": "^0.15.8",
|
|
48
48
|
"better-sqlite3": "^12.10.0",
|
|
49
49
|
"commander": "^14.0.3",
|
|
50
50
|
"fastify": "^5.6.1",
|