@holon-run/agentinbox 1.2.0 → 1.3.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/dist/src/cli.js +28 -0
- package/dist/src/http.js +39 -0
- package/dist/src/service.js +82 -2
- package/dist/src/sources/feishu.js +2 -53
- package/package.json +1 -1
- package/skills/agentinbox/SKILL.md +37 -0
package/dist/src/cli.js
CHANGED
|
@@ -533,6 +533,27 @@ async function main() {
|
|
|
533
533
|
await printRemote(client, `/agents/${encodeURIComponent(agentId)}/targets/${encodeURIComponent(targetId)}/resume`, {});
|
|
534
534
|
return;
|
|
535
535
|
}
|
|
536
|
+
if (command === "agent" && normalized[1] === "target" && normalized[2] === "update") {
|
|
537
|
+
const [agentId, targetId] = normalized.slice(3, 5);
|
|
538
|
+
if (!agentId || !targetId) {
|
|
539
|
+
throw new Error("usage: agentinbox agent target update <agentId> <targetId> [--url URL] [--activation-mode MODE] [--notify-lease-ms N] [--min-unacked-items N]");
|
|
540
|
+
}
|
|
541
|
+
const body = {};
|
|
542
|
+
const url = takeFlagValue(normalized, "--url");
|
|
543
|
+
if (url)
|
|
544
|
+
body.url = url;
|
|
545
|
+
const activationMode = takeFlagValue(normalized, "--activation-mode");
|
|
546
|
+
if (activationMode)
|
|
547
|
+
body.activationMode = activationMode;
|
|
548
|
+
const notifyLeaseMs = takeFlagValue(normalized, "--notify-lease-ms");
|
|
549
|
+
if (notifyLeaseMs)
|
|
550
|
+
body.notifyLeaseMs = parseOptionalNumber(notifyLeaseMs);
|
|
551
|
+
const minUnackedItems = takeFlagValue(normalized, "--min-unacked-items");
|
|
552
|
+
if (minUnackedItems)
|
|
553
|
+
body.minUnackedItems = parseOptionalNumber(minUnackedItems);
|
|
554
|
+
await printRemote(client, `/agents/${encodeURIComponent(agentId)}/targets/${encodeURIComponent(targetId)}`, body, "PATCH");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
536
557
|
if (command === "subscription" && normalized[1] === "add") {
|
|
537
558
|
const args = normalized.slice(2);
|
|
538
559
|
const positionals = positionalArgs(args, [
|
|
@@ -1154,11 +1175,17 @@ function clientArgsForCommand(args) {
|
|
|
1154
1175
|
if (isAgentTargetAddWebhookCommand(args)) {
|
|
1155
1176
|
return withoutFlagValues(args, "--url");
|
|
1156
1177
|
}
|
|
1178
|
+
if (isAgentTargetUpdateCommand(args)) {
|
|
1179
|
+
return withoutFlagValues(args, "--url");
|
|
1180
|
+
}
|
|
1157
1181
|
return args;
|
|
1158
1182
|
}
|
|
1159
1183
|
function isAgentTargetAddWebhookCommand(args) {
|
|
1160
1184
|
return args[0] === "agent" && args[1] === "target" && args[2] === "add" && args[3] === "webhook";
|
|
1161
1185
|
}
|
|
1186
|
+
function isAgentTargetUpdateCommand(args) {
|
|
1187
|
+
return args[0] === "agent" && args[1] === "target" && args[2] === "update";
|
|
1188
|
+
}
|
|
1162
1189
|
function withoutFlagValues(args, flag) {
|
|
1163
1190
|
const filtered = [];
|
|
1164
1191
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -1514,6 +1541,7 @@ Usage:
|
|
|
1514
1541
|
agentinbox agent target list <agentId> [--limit N]
|
|
1515
1542
|
agentinbox agent target remove <agentId> <targetId>
|
|
1516
1543
|
agentinbox agent target resume <agentId> <targetId>
|
|
1544
|
+
agentinbox agent target update <agentId> <targetId> [--url URL] [--activation-mode MODE] [--notify-lease-ms N] [--min-unacked-items N]
|
|
1517
1545
|
`,
|
|
1518
1546
|
timer: `agentinbox timer
|
|
1519
1547
|
|
package/dist/src/http.js
CHANGED
|
@@ -1061,6 +1061,45 @@ function buildFastifyServer(service) {
|
|
|
1061
1061
|
minUnackedItems: body.minUnackedItems ?? null,
|
|
1062
1062
|
});
|
|
1063
1063
|
});
|
|
1064
|
+
app.patch("/agents/:agentId/targets/:targetId", {
|
|
1065
|
+
schema: {
|
|
1066
|
+
tags: ["agents"],
|
|
1067
|
+
params: {
|
|
1068
|
+
type: "object",
|
|
1069
|
+
required: ["agentId", "targetId"],
|
|
1070
|
+
properties: {
|
|
1071
|
+
agentId: { type: "string", minLength: 1 },
|
|
1072
|
+
targetId: { type: "string", minLength: 1 },
|
|
1073
|
+
},
|
|
1074
|
+
},
|
|
1075
|
+
body: {
|
|
1076
|
+
type: "object",
|
|
1077
|
+
additionalProperties: false,
|
|
1078
|
+
properties: {
|
|
1079
|
+
url: { type: "string" },
|
|
1080
|
+
activationMode: { type: "string" },
|
|
1081
|
+
notifyLeaseMs: { type: "integer", minimum: 1 },
|
|
1082
|
+
minUnackedItems: { type: "integer", minimum: 1 },
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
response: {
|
|
1086
|
+
200: jsonObjectSchema,
|
|
1087
|
+
400: errorResponseSchema,
|
|
1088
|
+
},
|
|
1089
|
+
},
|
|
1090
|
+
}, async (request) => {
|
|
1091
|
+
const params = request.params;
|
|
1092
|
+
const body = request.body;
|
|
1093
|
+
if (!body.url && body.activationMode === undefined && body.notifyLeaseMs === undefined && body.minUnackedItems === undefined) {
|
|
1094
|
+
throw new Error("at least one of --url, --activation-mode, --notify-lease-ms, --min-unacked-items must be provided");
|
|
1095
|
+
}
|
|
1096
|
+
return service.updateWebhookActivationTarget(decodeURIComponent(params.agentId), decodeURIComponent(params.targetId), {
|
|
1097
|
+
url: body.url,
|
|
1098
|
+
activationMode: body.activationMode,
|
|
1099
|
+
notifyLeaseMs: body.notifyLeaseMs ?? null,
|
|
1100
|
+
minUnackedItems: body.minUnackedItems ?? null,
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1064
1103
|
app.delete("/agents/:agentId/targets/:targetId", {
|
|
1065
1104
|
schema: {
|
|
1066
1105
|
tags: ["agents"],
|
package/dist/src/service.js
CHANGED
|
@@ -17,6 +17,8 @@ const DEFAULT_ACTIVATION_MAX_ITEMS = 20;
|
|
|
17
17
|
const DEFAULT_NOTIFY_LEASE_MS = 10 * 60 * 1000;
|
|
18
18
|
const DEFAULT_NOTIFY_RETRY_MS = 5_000;
|
|
19
19
|
const MAX_TERMINAL_DEFER_RETRY_MS = 5 * 60 * 1000;
|
|
20
|
+
const MAX_WEBHOOK_RETRY_MS = 5 * 60 * 1000;
|
|
21
|
+
const MAX_WEBHOOK_CONSECUTIVE_FAILURES = 12;
|
|
20
22
|
const DEFAULT_ACKED_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
21
23
|
const DEFAULT_OFFLINE_AGENT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
22
24
|
const DEFAULT_GC_INTERVAL_MS = 60 * 1000;
|
|
@@ -774,6 +776,29 @@ class AgentInboxService {
|
|
|
774
776
|
this.markAgentActive(agentId);
|
|
775
777
|
return target;
|
|
776
778
|
}
|
|
779
|
+
updateWebhookActivationTarget(agentId, targetId, input) {
|
|
780
|
+
this.getAgent(agentId);
|
|
781
|
+
const existing = this.getActivationTarget(targetId);
|
|
782
|
+
if (existing.agentId !== agentId) {
|
|
783
|
+
throw new Error(`activation target ${targetId} does not belong to agent ${agentId}`);
|
|
784
|
+
}
|
|
785
|
+
if (existing.kind !== "webhook") {
|
|
786
|
+
throw new Error(`activation target ${targetId} is not a webhook target`);
|
|
787
|
+
}
|
|
788
|
+
validateNotifyLeaseMs(input.notifyLeaseMs);
|
|
789
|
+
validateMinUnackedItems(input.minUnackedItems);
|
|
790
|
+
const mode = normalizeWebhookActivationMode(input.activationMode ?? existing.mode);
|
|
791
|
+
const now = (0, util_1.nowIso)();
|
|
792
|
+
const target = this.store.updateWebhookActivationTarget(targetId, {
|
|
793
|
+
url: input.url ?? existing.url,
|
|
794
|
+
mode,
|
|
795
|
+
notifyLeaseMs: input.notifyLeaseMs ?? existing.notifyLeaseMs,
|
|
796
|
+
minUnackedItems: input.minUnackedItems !== undefined ? input.minUnackedItems : existing.minUnackedItems ?? null,
|
|
797
|
+
updatedAt: now,
|
|
798
|
+
lastSeenAt: now,
|
|
799
|
+
});
|
|
800
|
+
return target;
|
|
801
|
+
}
|
|
777
802
|
listActivationTargets(agentId, limit) {
|
|
778
803
|
if (agentId) {
|
|
779
804
|
this.getAgent(agentId);
|
|
@@ -2074,6 +2099,10 @@ class AgentInboxService {
|
|
|
2074
2099
|
: uniqueSorted(entries.map((entry) => entry.sourceId)),
|
|
2075
2100
|
entries: unackedItems,
|
|
2076
2101
|
});
|
|
2102
|
+
// Permanent errors and max-retries-exceeded signal "offline":
|
|
2103
|
+
if (dispatched === "offline") {
|
|
2104
|
+
this.store.deleteActivationDispatchState(buffer.agentId, buffer.targetId);
|
|
2105
|
+
}
|
|
2077
2106
|
if (dispatched === "retryable_failure") {
|
|
2078
2107
|
if (state && state.status === "dirty" && state.leaseExpiresAt == null) {
|
|
2079
2108
|
this.store.upsertActivationDispatchState({
|
|
@@ -2253,10 +2282,17 @@ class AgentInboxService {
|
|
|
2253
2282
|
return;
|
|
2254
2283
|
}
|
|
2255
2284
|
if (dispatched === "retryable_failure") {
|
|
2285
|
+
let backoffMs = DEFAULT_NOTIFY_RETRY_MS;
|
|
2286
|
+
if (target.kind === "webhook") {
|
|
2287
|
+
const updatedTarget = this.getActivationTargetIfExists(agentId, targetId);
|
|
2288
|
+
if (updatedTarget) {
|
|
2289
|
+
backoffMs = webhookDeferRetryMs(updatedTarget.consecutiveFailures);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2256
2292
|
this.store.upsertActivationDispatchState({
|
|
2257
2293
|
...state,
|
|
2258
2294
|
status: "dirty",
|
|
2259
|
-
leaseExpiresAt: new Date(Date.now() +
|
|
2295
|
+
leaseExpiresAt: new Date(Date.now() + backoffMs).toISOString(),
|
|
2260
2296
|
deferReason: null,
|
|
2261
2297
|
deferAttempts: 0,
|
|
2262
2298
|
firstDeferredAt: null,
|
|
@@ -2525,6 +2561,35 @@ class AgentInboxService {
|
|
|
2525
2561
|
return "offline";
|
|
2526
2562
|
}
|
|
2527
2563
|
}
|
|
2564
|
+
if (target.kind === "webhook") {
|
|
2565
|
+
const statusCode = extractHttpStatus(error);
|
|
2566
|
+
// 404 / 410: endpoint gone
|
|
2567
|
+
// 409 / 422: expired or invalid webhook config
|
|
2568
|
+
if (statusCode === 404 || statusCode === 410 || statusCode === 409 || statusCode === 422) {
|
|
2569
|
+
this.logger.info("activation.webhook_permanent_error", {
|
|
2570
|
+
agentId: input.agentId,
|
|
2571
|
+
targetId: target.targetId,
|
|
2572
|
+
statusCode,
|
|
2573
|
+
message,
|
|
2574
|
+
});
|
|
2575
|
+
this.markActivationTargetOffline(target.targetId, message);
|
|
2576
|
+
this.reconcileAgentStatus(target.agentId);
|
|
2577
|
+
return "offline";
|
|
2578
|
+
}
|
|
2579
|
+
// Max consecutive transient failures exceeded
|
|
2580
|
+
if (target.consecutiveFailures + 1 >= MAX_WEBHOOK_CONSECUTIVE_FAILURES) {
|
|
2581
|
+
this.logger.info("activation.webhook_max_retries_exceeded", {
|
|
2582
|
+
agentId: input.agentId,
|
|
2583
|
+
targetId: target.targetId,
|
|
2584
|
+
consecutiveFailures: target.consecutiveFailures + 1,
|
|
2585
|
+
maxRetries: MAX_WEBHOOK_CONSECUTIVE_FAILURES,
|
|
2586
|
+
});
|
|
2587
|
+
this.markActivationTargetOffline(target.targetId, message);
|
|
2588
|
+
this.reconcileAgentStatus(target.agentId);
|
|
2589
|
+
return "offline";
|
|
2590
|
+
}
|
|
2591
|
+
// Transient error: fall through to markActivationTargetDispatchFailure
|
|
2592
|
+
}
|
|
2528
2593
|
this.markActivationTargetDispatchFailure(target.targetId, message);
|
|
2529
2594
|
return "retryable_failure";
|
|
2530
2595
|
}
|
|
@@ -3351,7 +3416,9 @@ class ActivationDispatcher {
|
|
|
3351
3416
|
body: JSON.stringify(activation),
|
|
3352
3417
|
});
|
|
3353
3418
|
if (!response.ok) {
|
|
3354
|
-
|
|
3419
|
+
const error = new Error(`activation dispatch failed for ${targetUrl}: ${response.status}`);
|
|
3420
|
+
error.statusCode = response.status;
|
|
3421
|
+
throw error;
|
|
3355
3422
|
}
|
|
3356
3423
|
}
|
|
3357
3424
|
catch (error) {
|
|
@@ -3570,6 +3637,19 @@ function terminalDeferRetryMs(attempts) {
|
|
|
3570
3637
|
const multiplier = 2 ** (normalizedAttempts - 1);
|
|
3571
3638
|
return Math.min(DEFAULT_NOTIFY_RETRY_MS * multiplier, MAX_TERMINAL_DEFER_RETRY_MS);
|
|
3572
3639
|
}
|
|
3640
|
+
function webhookDeferRetryMs(consecutiveFailures) {
|
|
3641
|
+
const normalized = Math.max(1, consecutiveFailures);
|
|
3642
|
+
const multiplier = 2 ** (normalized - 1);
|
|
3643
|
+
return Math.min(DEFAULT_NOTIFY_RETRY_MS * multiplier, MAX_WEBHOOK_RETRY_MS);
|
|
3644
|
+
}
|
|
3645
|
+
function extractHttpStatus(error) {
|
|
3646
|
+
if (error instanceof Error) {
|
|
3647
|
+
const code = error.statusCode;
|
|
3648
|
+
if (typeof code === "number")
|
|
3649
|
+
return code;
|
|
3650
|
+
}
|
|
3651
|
+
return null;
|
|
3652
|
+
}
|
|
3573
3653
|
function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
|
|
3574
3654
|
if (sourceType === "github_repo_ci") {
|
|
3575
3655
|
const parts = eventVariant.split(".");
|
|
@@ -14,11 +14,9 @@ const node_child_process_1 = require("node:child_process");
|
|
|
14
14
|
const node_crypto_1 = require("node:crypto");
|
|
15
15
|
const node_fs_1 = require("node:fs");
|
|
16
16
|
const node_path_1 = require("node:path");
|
|
17
|
-
const node_os_1 = require("node:os");
|
|
18
17
|
const node_util_1 = require("node:util");
|
|
19
18
|
exports.FEISHU_OPENAPI_ENDPOINT = "https://open.feishu.cn/open-apis";
|
|
20
19
|
exports.FEISHU_IM_SCHEMA_URL = "https://raw.githubusercontent.com/holon-run/uxc/main/skills/feishu-openapi-skill/references/feishu-im.openapi.json";
|
|
21
|
-
const FEISHU_BOT_SCHEMA_FILE = (0, node_path_1.join)((0, node_os_1.tmpdir)(), "agentinbox-feishu-bot.openapi.json");
|
|
22
20
|
exports.DEFAULT_FEISHU_EVENT_TYPES = ["im.message.receive_v1"];
|
|
23
21
|
const DEFAULT_CONTEXT_BOUND_SECONDS = 7 * 24 * 60 * 60;
|
|
24
22
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
@@ -131,10 +129,8 @@ class FeishuUxcClient {
|
|
|
131
129
|
operation: "get:/bot/v3/info",
|
|
132
130
|
payload: {},
|
|
133
131
|
options: {
|
|
134
|
-
auth: input.auth,
|
|
135
|
-
schema_url:
|
|
136
|
-
refresh_schema: true,
|
|
137
|
-
no_cache: true,
|
|
132
|
+
auth: input.auth ?? undefined,
|
|
133
|
+
schema_url: exports.FEISHU_IM_SCHEMA_URL,
|
|
138
134
|
},
|
|
139
135
|
});
|
|
140
136
|
const openId = findStringField(response.data, "open_id");
|
|
@@ -1873,50 +1869,3 @@ function asStringArray(value) {
|
|
|
1873
1869
|
.map((item) => asString(item))
|
|
1874
1870
|
.filter((item) => Boolean(item));
|
|
1875
1871
|
}
|
|
1876
|
-
function ensureFeishuBotSchemaFile() {
|
|
1877
|
-
const body = JSON.stringify(FEISHU_BOT_OPENAPI_SCHEMA);
|
|
1878
|
-
(0, node_fs_1.writeFileSync)(FEISHU_BOT_SCHEMA_FILE, body, "utf8");
|
|
1879
|
-
return FEISHU_BOT_SCHEMA_FILE;
|
|
1880
|
-
}
|
|
1881
|
-
const FEISHU_BOT_OPENAPI_SCHEMA = {
|
|
1882
|
-
openapi: "3.0.3",
|
|
1883
|
-
info: {
|
|
1884
|
-
title: "Feishu Bot Info",
|
|
1885
|
-
version: "1.0.0",
|
|
1886
|
-
},
|
|
1887
|
-
servers: [
|
|
1888
|
-
{
|
|
1889
|
-
url: exports.FEISHU_OPENAPI_ENDPOINT,
|
|
1890
|
-
},
|
|
1891
|
-
],
|
|
1892
|
-
// Feishu's docs site does not currently expose this as a stable
|
|
1893
|
-
// machine-readable OpenAPI document to UXC, so keep the tiny operation
|
|
1894
|
-
// schema needed for bot self-identification close to the adapter.
|
|
1895
|
-
paths: {
|
|
1896
|
-
"/bot/v3/info": {
|
|
1897
|
-
get: {
|
|
1898
|
-
operationId: "get:/bot/v3/info",
|
|
1899
|
-
summary: "Get bot information for the current app",
|
|
1900
|
-
responses: {
|
|
1901
|
-
"200": {
|
|
1902
|
-
description: "OK",
|
|
1903
|
-
},
|
|
1904
|
-
},
|
|
1905
|
-
security: [
|
|
1906
|
-
{
|
|
1907
|
-
bearerAuth: [],
|
|
1908
|
-
},
|
|
1909
|
-
],
|
|
1910
|
-
},
|
|
1911
|
-
},
|
|
1912
|
-
},
|
|
1913
|
-
components: {
|
|
1914
|
-
securitySchemes: {
|
|
1915
|
-
bearerAuth: {
|
|
1916
|
-
type: "http",
|
|
1917
|
-
scheme: "bearer",
|
|
1918
|
-
bearerFormat: "tenant_access_token",
|
|
1919
|
-
},
|
|
1920
|
-
},
|
|
1921
|
-
},
|
|
1922
|
-
};
|
package/package.json
CHANGED
|
@@ -193,6 +193,43 @@ agentinbox source invoke <sourceId> --operation get_message_context --input-json
|
|
|
193
193
|
agentinbox deliver invoke --handle-json '<deliveryHandle-json>' --operation send_text --input-json '{"text":"Acknowledged","uxcAuth":"feishu-default"}'
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
+
Feishu messages may carry files, images, or cloud document links. List the
|
|
197
|
+
attachments first and then save them locally:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
# List attachments for a message
|
|
201
|
+
agentinbox source invoke <sourceId> \
|
|
202
|
+
--operation list_message_attachments \
|
|
203
|
+
--input-json '{"messageId":"<messageId>"}'
|
|
204
|
+
|
|
205
|
+
# Save one attachment by messageId + attachmentId
|
|
206
|
+
agentinbox source invoke <sourceId> \
|
|
207
|
+
--operation save_attachment \
|
|
208
|
+
--input-json '{"messageId":"<messageId>","attachmentId":"<attachmentId>","outputDir":"/tmp/agentinbox-feishu"}'
|
|
209
|
+
|
|
210
|
+
# Save with explicit output path and identity
|
|
211
|
+
agentinbox source invoke <sourceId> \
|
|
212
|
+
--operation save_attachment \
|
|
213
|
+
--input-json '{"messageId":"<messageId>","attachmentId":"<attachmentId>","outputPath":"/tmp/report.pdf","identity":"bot"}'
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Output formats by attachment kind:
|
|
217
|
+
|
|
218
|
+
| Kind | Default | `format` options |
|
|
219
|
+
|---|---|---|
|
|
220
|
+
| `image` / `file` | original binary | `"original"` only |
|
|
221
|
+
| `doc` / `docx` / `wiki` | `.md` (Markdown) | N/A |
|
|
222
|
+
| `sheet` | `.xlsx` | `"csv"` (requires `subId`) |
|
|
223
|
+
| `bitable` / `base` | `.base` | `"csv"` (requires `subId`) |
|
|
224
|
+
| `drive_file` | original file | `"original"` only |
|
|
225
|
+
|
|
226
|
+
Optional `save_attachment` parameters: `outputPath`, `outputDir`, `fileName`,
|
|
227
|
+
`format`, `subId`, `identity` (`"user"` or `"bot"`), `overwrite`.
|
|
228
|
+
|
|
229
|
+
Rich text and file replies are available through `send_post`, `send_file`,
|
|
230
|
+
and `send_file_from_path` delivery operations. Use `deliver actions` to
|
|
231
|
+
inspect available schemas.
|
|
232
|
+
|
|
196
233
|
Advanced source/subscription commands:
|
|
197
234
|
|
|
198
235
|
```bash
|