@evgenyy/lessinbox-channel 0.1.6 → 0.1.9
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 +23 -6
- package/dist/index.d.ts +136 -16
- package/dist/index.js +695 -46
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/skills/lessinbox/SKILL.md +2 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,9 @@ import Redis from "ioredis";
|
|
|
2
2
|
import WebSocket from "ws";
|
|
3
3
|
const DEFAULT_RECONNECT_MS = 1_500;
|
|
4
4
|
const MIN_RECONNECT_MS = 250;
|
|
5
|
+
const INBOUND_MESSAGE_DEDUPE_TTL_MS = 5 * 60 * 1000;
|
|
5
6
|
const TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "canceled"]);
|
|
7
|
+
const ACTIVE_RUN_STATUSES = new Set(["queued", "running", "waiting_for_human"]);
|
|
6
8
|
function normalizeBaseUrl(baseUrl) {
|
|
7
9
|
return baseUrl.replace(/\/+$/, "");
|
|
8
10
|
}
|
|
@@ -91,10 +93,20 @@ function parseRunRef(value) {
|
|
|
91
93
|
const threadId = toOptionalString(record.threadId);
|
|
92
94
|
const runId = toOptionalString(record.runId);
|
|
93
95
|
const status = toOptionalString(record.status);
|
|
94
|
-
if (!threadId
|
|
96
|
+
if (!threadId) {
|
|
95
97
|
return undefined;
|
|
96
98
|
}
|
|
97
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
threadId,
|
|
101
|
+
...(runId ? { runId } : {}),
|
|
102
|
+
...(status ? { status } : {})
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function isTerminalRunStatus(status) {
|
|
106
|
+
return Boolean(status && TERMINAL_RUN_STATUSES.has(status));
|
|
107
|
+
}
|
|
108
|
+
function isActiveRunStatus(status) {
|
|
109
|
+
return Boolean(status && ACTIVE_RUN_STATUSES.has(status));
|
|
98
110
|
}
|
|
99
111
|
function extractRunId(payload) {
|
|
100
112
|
const record = asRecord(payload);
|
|
@@ -148,19 +160,19 @@ function buildWorkspaceWsEndpoint(apiUrl, wsUrlOverride) {
|
|
|
148
160
|
url.search = "";
|
|
149
161
|
const normalizedPath = url.pathname.replace(/\/+$/, "");
|
|
150
162
|
if (!normalizedPath || normalizedPath === "/") {
|
|
151
|
-
url.pathname = "/
|
|
163
|
+
url.pathname = "/v2/ws";
|
|
152
164
|
return url.toString();
|
|
153
165
|
}
|
|
154
|
-
if (normalizedPath.endsWith("/
|
|
166
|
+
if (normalizedPath.endsWith("/v1")) {
|
|
155
167
|
const prefix = normalizedPath.slice(0, -3);
|
|
156
|
-
url.pathname = `${prefix || ""}/
|
|
168
|
+
url.pathname = `${prefix || ""}/v2/ws`;
|
|
157
169
|
return url.toString();
|
|
158
170
|
}
|
|
159
|
-
if (normalizedPath.endsWith("/
|
|
171
|
+
if (normalizedPath.endsWith("/v2")) {
|
|
160
172
|
url.pathname = `${normalizedPath}/ws`;
|
|
161
173
|
return url.toString();
|
|
162
174
|
}
|
|
163
|
-
url.pathname = `${normalizedPath}/
|
|
175
|
+
url.pathname = `${normalizedPath}/v2/ws`;
|
|
164
176
|
return url.toString();
|
|
165
177
|
}
|
|
166
178
|
function buildWorkspaceWsUrl(apiUrl, token, wsUrlOverride) {
|
|
@@ -389,14 +401,40 @@ class WorkspaceStreamConsumer {
|
|
|
389
401
|
}
|
|
390
402
|
const payload = asRecord(event.payload);
|
|
391
403
|
const toStatus = toOptionalString(payload?.to);
|
|
392
|
-
|
|
404
|
+
const mappedConversationId = await this.options.mappingStore.getConversationKeyForRun(event.runId);
|
|
405
|
+
const conversationId = event.conversationId ?? mappedConversationId ?? (event.threadId ? `thread:${event.threadId}` : undefined);
|
|
406
|
+
if (!conversationId) {
|
|
407
|
+
if (isTerminalRunStatus(toStatus)) {
|
|
408
|
+
await this.options.mappingStore.deleteRun(event.runId);
|
|
409
|
+
}
|
|
393
410
|
return;
|
|
394
411
|
}
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
412
|
+
const existing = await this.options.mappingStore.getConversationRun(conversationId);
|
|
413
|
+
const threadId = event.threadId ?? existing?.threadId;
|
|
414
|
+
if (!threadId) {
|
|
415
|
+
if (isTerminalRunStatus(toStatus)) {
|
|
416
|
+
await this.options.mappingStore.deleteRun(event.runId);
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
398
419
|
}
|
|
399
|
-
|
|
420
|
+
const resolvedStatus = toStatus ?? existing?.status;
|
|
421
|
+
if (isTerminalRunStatus(toStatus)) {
|
|
422
|
+
await this.options.mappingStore.setConversationRun(conversationId, {
|
|
423
|
+
threadId,
|
|
424
|
+
...(resolvedStatus ? { status: resolvedStatus } : {})
|
|
425
|
+
});
|
|
426
|
+
await this.options.mappingStore.deleteRun(event.runId);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (existing?.runId && existing.runId !== event.runId) {
|
|
430
|
+
await this.options.mappingStore.deleteRun(existing.runId);
|
|
431
|
+
}
|
|
432
|
+
await this.options.mappingStore.setConversationRun(conversationId, {
|
|
433
|
+
threadId,
|
|
434
|
+
runId: event.runId,
|
|
435
|
+
...(resolvedStatus ? { status: resolvedStatus } : {})
|
|
436
|
+
});
|
|
437
|
+
await this.options.mappingStore.setConversationKeyForRun(event.runId, conversationId);
|
|
400
438
|
}
|
|
401
439
|
async emit(event) {
|
|
402
440
|
for (const listener of this.listeners) {
|
|
@@ -453,6 +491,10 @@ function ensureWorkspaceConsumer(accountId, account, api, mappingStore) {
|
|
|
453
491
|
return consumer;
|
|
454
492
|
}
|
|
455
493
|
export async function shutdownLessinboxPluginResources() {
|
|
494
|
+
for (const unsubscribe of gatewayUnsubscribers.values()) {
|
|
495
|
+
unsubscribe();
|
|
496
|
+
}
|
|
497
|
+
gatewayUnsubscribers.clear();
|
|
456
498
|
for (const consumer of workspaceConsumers.values()) {
|
|
457
499
|
consumer.stop();
|
|
458
500
|
}
|
|
@@ -461,6 +503,8 @@ export async function shutdownLessinboxPluginResources() {
|
|
|
461
503
|
await store.close();
|
|
462
504
|
}
|
|
463
505
|
mappingStores.clear();
|
|
506
|
+
handledInboundMessages.clear();
|
|
507
|
+
lessinboxPluginRuntime = null;
|
|
464
508
|
}
|
|
465
509
|
export class LessinboxApi {
|
|
466
510
|
constructor(config) {
|
|
@@ -524,6 +568,12 @@ export class LessinboxApi {
|
|
|
524
568
|
idempotencyKey: randomIdempotencyKey()
|
|
525
569
|
});
|
|
526
570
|
}
|
|
571
|
+
async getRun(runId) {
|
|
572
|
+
return this.request(`/v2/runs/${runId}`, { method: "GET" });
|
|
573
|
+
}
|
|
574
|
+
async getThread(threadId) {
|
|
575
|
+
return this.request(`/v2/threads/${threadId}`, { method: "GET" });
|
|
576
|
+
}
|
|
527
577
|
async listThreads(input = {}) {
|
|
528
578
|
const params = new URLSearchParams();
|
|
529
579
|
if (input.bucket)
|
|
@@ -538,11 +588,22 @@ export class LessinboxApi {
|
|
|
538
588
|
return this.request(`/v2/threads${query ? `?${query}` : ""}`, { method: "GET" });
|
|
539
589
|
}
|
|
540
590
|
async createWorkspaceWsToken() {
|
|
541
|
-
return this.request("/
|
|
591
|
+
return this.request("/v2/workspace/ws-token", {
|
|
542
592
|
method: "POST",
|
|
543
593
|
body: {}
|
|
544
594
|
});
|
|
545
595
|
}
|
|
596
|
+
async getThreadFeed(input) {
|
|
597
|
+
const params = new URLSearchParams();
|
|
598
|
+
if (typeof input.limit === "number") {
|
|
599
|
+
params.set("limit", String(input.limit));
|
|
600
|
+
}
|
|
601
|
+
if (input.cursor) {
|
|
602
|
+
params.set("cursor", input.cursor);
|
|
603
|
+
}
|
|
604
|
+
const query = params.toString();
|
|
605
|
+
return this.request(`/v2/threads/${input.threadId}/feed${query ? `?${query}` : ""}`, { method: "GET" });
|
|
606
|
+
}
|
|
546
607
|
async request(path, options) {
|
|
547
608
|
const headers = new Headers({
|
|
548
609
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -598,6 +659,7 @@ export function resolveAccountConfig(config, accountId) {
|
|
|
598
659
|
}
|
|
599
660
|
const mappingStore = resolveMappingStoreKind(account.mappingStore);
|
|
600
661
|
return {
|
|
662
|
+
accountId: resolvedId,
|
|
601
663
|
enabled: typeof account.enabled === "boolean" ? account.enabled : true,
|
|
602
664
|
apiUrl,
|
|
603
665
|
apiKey,
|
|
@@ -609,6 +671,49 @@ export function resolveAccountConfig(config, accountId) {
|
|
|
609
671
|
workspaceStream: resolveWorkspaceStreamConfig(account.workspaceStream)
|
|
610
672
|
};
|
|
611
673
|
}
|
|
674
|
+
const gatewayUnsubscribers = new Map();
|
|
675
|
+
const handledInboundMessages = new Map();
|
|
676
|
+
let lessinboxPluginRuntime = null;
|
|
677
|
+
function setLessinboxPluginRuntime(runtime) {
|
|
678
|
+
if (!runtime || typeof runtime !== "object") {
|
|
679
|
+
lessinboxPluginRuntime = null;
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
lessinboxPluginRuntime = runtime;
|
|
683
|
+
}
|
|
684
|
+
function getLessinboxPluginRuntime() {
|
|
685
|
+
return lessinboxPluginRuntime;
|
|
686
|
+
}
|
|
687
|
+
function formatError(err) {
|
|
688
|
+
if (err instanceof Error && err.message) {
|
|
689
|
+
return err.message;
|
|
690
|
+
}
|
|
691
|
+
if (typeof err === "string") {
|
|
692
|
+
return err;
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
return JSON.stringify(err);
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
return String(err);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function sleep(ms) {
|
|
702
|
+
return new Promise((resolve) => {
|
|
703
|
+
setTimeout(resolve, ms);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
function toOptionalTimestamp(value) {
|
|
707
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
708
|
+
return value;
|
|
709
|
+
}
|
|
710
|
+
const asString = toOptionalString(value);
|
|
711
|
+
if (!asString) {
|
|
712
|
+
return undefined;
|
|
713
|
+
}
|
|
714
|
+
const parsed = Date.parse(asString);
|
|
715
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
716
|
+
}
|
|
612
717
|
function deriveConversationKey(input) {
|
|
613
718
|
if (input.conversationId && input.conversationId.trim().length > 0) {
|
|
614
719
|
return input.conversationId;
|
|
@@ -618,11 +723,197 @@ function deriveConversationKey(input) {
|
|
|
618
723
|
}
|
|
619
724
|
return null;
|
|
620
725
|
}
|
|
726
|
+
function listAccountIds(config) {
|
|
727
|
+
const accounts = readPath(config, ["channels", "lessinbox", "accounts"]);
|
|
728
|
+
if (!accounts || typeof accounts !== "object") {
|
|
729
|
+
return [];
|
|
730
|
+
}
|
|
731
|
+
return Object.keys(accounts);
|
|
732
|
+
}
|
|
733
|
+
function resolveSendTarget(input) {
|
|
734
|
+
const explicitTarget = toOptionalString(input.target) ??
|
|
735
|
+
toOptionalString(input.to) ??
|
|
736
|
+
toOptionalString(input.recipient) ??
|
|
737
|
+
toOptionalString(input.metadata?.target);
|
|
738
|
+
if (!explicitTarget) {
|
|
739
|
+
return {};
|
|
740
|
+
}
|
|
741
|
+
const separatorIndex = explicitTarget.indexOf(":");
|
|
742
|
+
if (separatorIndex > 0 && separatorIndex < explicitTarget.length - 1) {
|
|
743
|
+
const kind = explicitTarget.slice(0, separatorIndex).toLowerCase();
|
|
744
|
+
const value = explicitTarget.slice(separatorIndex + 1).trim();
|
|
745
|
+
if (!value) {
|
|
746
|
+
return {};
|
|
747
|
+
}
|
|
748
|
+
if (kind === "thread") {
|
|
749
|
+
return { threadId: value, conversationId: `thread:${value}` };
|
|
750
|
+
}
|
|
751
|
+
if (kind === "channel") {
|
|
752
|
+
return { channelId: value, conversationId: `channel:${value}` };
|
|
753
|
+
}
|
|
754
|
+
if (kind === "account") {
|
|
755
|
+
return { accountId: value };
|
|
756
|
+
}
|
|
757
|
+
if (kind === "run" || kind === "session") {
|
|
758
|
+
return { runId: value };
|
|
759
|
+
}
|
|
760
|
+
if (kind === "conversation") {
|
|
761
|
+
return { conversationId: value };
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (/^thr_[a-zA-Z0-9]+$/.test(explicitTarget)) {
|
|
765
|
+
return { threadId: explicitTarget, conversationId: `thread:${explicitTarget}` };
|
|
766
|
+
}
|
|
767
|
+
if (/^chn_[a-zA-Z0-9]+$/.test(explicitTarget)) {
|
|
768
|
+
return { channelId: explicitTarget, conversationId: `channel:${explicitTarget}` };
|
|
769
|
+
}
|
|
770
|
+
const accountIds = input.config ? listAccountIds(input.config) : [];
|
|
771
|
+
if (accountIds.includes(explicitTarget)) {
|
|
772
|
+
return { accountId: explicitTarget };
|
|
773
|
+
}
|
|
774
|
+
// Raw IDs default to channel target so CLI `--target <channelId>` works out of the box.
|
|
775
|
+
return { channelId: explicitTarget, conversationId: `channel:${explicitTarget}` };
|
|
776
|
+
}
|
|
777
|
+
function deriveChannelIdFromConversationKey(conversationKey) {
|
|
778
|
+
if (!conversationKey || !conversationKey.startsWith("channel:")) {
|
|
779
|
+
return undefined;
|
|
780
|
+
}
|
|
781
|
+
const channelId = conversationKey.slice("channel:".length).trim();
|
|
782
|
+
return channelId.length > 0 ? channelId : undefined;
|
|
783
|
+
}
|
|
784
|
+
function deriveTitle(input) {
|
|
785
|
+
if (input.title && input.title.trim().length > 0) {
|
|
786
|
+
return input.title.trim();
|
|
787
|
+
}
|
|
788
|
+
return input.text.slice(0, 80);
|
|
789
|
+
}
|
|
790
|
+
function toRunRefFromThreadSummary(summary) {
|
|
791
|
+
const runId = toOptionalString(summary.session_id) ?? toOptionalString(summary.run_id);
|
|
792
|
+
const status = toOptionalString(summary.session_status) ?? toOptionalString(summary.run_status);
|
|
793
|
+
return {
|
|
794
|
+
threadId: summary.id,
|
|
795
|
+
...(runId ? { runId } : {}),
|
|
796
|
+
...(status ? { status } : {})
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function toRunRefFromThreadDetail(thread) {
|
|
800
|
+
const session = asRecord(thread.session);
|
|
801
|
+
const run = asRecord(thread.run);
|
|
802
|
+
const runId = toOptionalString(session?.id) ?? toOptionalString(run?.id);
|
|
803
|
+
const status = toOptionalString(session?.status) ?? toOptionalString(run?.status);
|
|
804
|
+
return {
|
|
805
|
+
threadId: thread.id,
|
|
806
|
+
...(runId ? { runId } : {}),
|
|
807
|
+
...(status ? { status } : {})
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
function normalizeConversationMetadata(metadata, conversationKey) {
|
|
811
|
+
if (!conversationKey) {
|
|
812
|
+
return metadata;
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
...(metadata ?? {}),
|
|
816
|
+
conversation_id: conversationKey
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function purgeHandledInboundMessages(now) {
|
|
820
|
+
for (const [key, expiresAt] of handledInboundMessages.entries()) {
|
|
821
|
+
if (expiresAt <= now) {
|
|
822
|
+
handledInboundMessages.delete(key);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
function hasHandledInboundMessage(key) {
|
|
827
|
+
const now = Date.now();
|
|
828
|
+
purgeHandledInboundMessages(now);
|
|
829
|
+
const expiresAt = handledInboundMessages.get(key);
|
|
830
|
+
return typeof expiresAt === "number" && expiresAt > now;
|
|
831
|
+
}
|
|
832
|
+
function markHandledInboundMessage(key) {
|
|
833
|
+
handledInboundMessages.set(key, Date.now() + INBOUND_MESSAGE_DEDUPE_TTL_MS);
|
|
834
|
+
}
|
|
835
|
+
function deriveHandledInboundKey(accountId, messageId) {
|
|
836
|
+
return `${accountId}:${messageId}`;
|
|
837
|
+
}
|
|
838
|
+
function resolveMessageActorType(message) {
|
|
839
|
+
const actor = asRecord(message?.actor);
|
|
840
|
+
return toOptionalString(actor?.type) ?? "unknown";
|
|
841
|
+
}
|
|
842
|
+
function resolveMessageActorUserId(message) {
|
|
843
|
+
const actor = asRecord(message?.actor);
|
|
844
|
+
return toOptionalString(actor?.user_id) ?? toOptionalString(actor?.userId);
|
|
845
|
+
}
|
|
846
|
+
function resolveEventPayloadMessage(event) {
|
|
847
|
+
const payload = asRecord(event.payload);
|
|
848
|
+
const message = asRecord(payload?.message);
|
|
849
|
+
const id = toOptionalString(message?.id);
|
|
850
|
+
if (!id) {
|
|
851
|
+
return undefined;
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
id,
|
|
855
|
+
run_id: toOptionalString(message?.run_id) ?? undefined,
|
|
856
|
+
session_id: toOptionalString(message?.session_id) ?? undefined,
|
|
857
|
+
kind: toOptionalString(message?.kind),
|
|
858
|
+
text: toOptionalString(message?.text),
|
|
859
|
+
actor: asRecord(message?.actor),
|
|
860
|
+
created_at: toOptionalString(message?.created_at)
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
async function findFeedMessageById(api, threadId, messageId) {
|
|
864
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
865
|
+
try {
|
|
866
|
+
const feed = await api.getThreadFeed({ threadId, limit: 50 });
|
|
867
|
+
const found = feed.entries.find((entry) => toOptionalString(entry?.message?.id) === messageId)?.message;
|
|
868
|
+
if (found) {
|
|
869
|
+
return found;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
catch {
|
|
873
|
+
// ignore and retry
|
|
874
|
+
}
|
|
875
|
+
if (attempt < 2) {
|
|
876
|
+
await sleep((attempt + 1) * 120);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return undefined;
|
|
880
|
+
}
|
|
881
|
+
function coalesceInboundMessage(messageId, payloadMessage, feedMessage) {
|
|
882
|
+
const text = toOptionalString(feedMessage?.text) ??
|
|
883
|
+
toOptionalString(payloadMessage?.text);
|
|
884
|
+
if (!text) {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
const actorType = resolveMessageActorType(feedMessage) !== "unknown"
|
|
888
|
+
? resolveMessageActorType(feedMessage)
|
|
889
|
+
: resolveMessageActorType(payloadMessage);
|
|
890
|
+
const actorUserId = resolveMessageActorUserId(feedMessage) ??
|
|
891
|
+
resolveMessageActorUserId(payloadMessage);
|
|
892
|
+
const runId = toOptionalString(feedMessage?.run_id) ??
|
|
893
|
+
toOptionalString(feedMessage?.session_id) ??
|
|
894
|
+
toOptionalString(payloadMessage?.run_id) ??
|
|
895
|
+
toOptionalString(payloadMessage?.session_id);
|
|
896
|
+
const createdAtMs = toOptionalTimestamp(feedMessage?.created_at) ??
|
|
897
|
+
toOptionalTimestamp(payloadMessage?.created_at);
|
|
898
|
+
return {
|
|
899
|
+
id: messageId,
|
|
900
|
+
text,
|
|
901
|
+
actorType,
|
|
902
|
+
...(actorUserId ? { actorUserId } : {}),
|
|
903
|
+
...(runId ? { runId } : {}),
|
|
904
|
+
...(typeof createdAtMs === "number" ? { createdAtMs } : {})
|
|
905
|
+
};
|
|
906
|
+
}
|
|
621
907
|
async function sendTextToLessinbox(input) {
|
|
622
908
|
if (!input.config) {
|
|
623
909
|
throw new Error("Plugin sendText did not receive runtime config");
|
|
624
910
|
}
|
|
625
|
-
const
|
|
911
|
+
const resolvedTarget = resolveSendTarget(input);
|
|
912
|
+
const resolvedThreadId = input.threadId ?? resolvedTarget.threadId;
|
|
913
|
+
const resolvedRunId = input.runId ?? resolvedTarget.runId;
|
|
914
|
+
const resolvedChannelId = input.channelId ?? resolvedTarget.channelId;
|
|
915
|
+
const resolvedConversationId = input.conversationId ?? resolvedTarget.conversationId;
|
|
916
|
+
const accountRuntimeId = deriveAccountRuntimeId(input.accountId ?? resolvedTarget.accountId);
|
|
626
917
|
const account = resolveAccountConfig(input.config, accountRuntimeId);
|
|
627
918
|
if (account.enabled === false) {
|
|
628
919
|
throw new Error("Lessinbox account is disabled");
|
|
@@ -630,40 +921,376 @@ async function sendTextToLessinbox(input) {
|
|
|
630
921
|
const api = new LessinboxApi(account);
|
|
631
922
|
const mappingStore = getConversationMappingStore(accountRuntimeId, account);
|
|
632
923
|
ensureWorkspaceConsumer(accountRuntimeId, account, api, mappingStore);
|
|
633
|
-
const conversationKey = deriveConversationKey(
|
|
924
|
+
const conversationKey = deriveConversationKey({
|
|
925
|
+
...input,
|
|
926
|
+
threadId: resolvedThreadId,
|
|
927
|
+
conversationId: resolvedConversationId
|
|
928
|
+
});
|
|
634
929
|
const mappedRun = conversationKey ? await mappingStore.getConversationRun(conversationKey) : undefined;
|
|
635
|
-
let threadId =
|
|
636
|
-
let runId =
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
930
|
+
let threadId = resolvedThreadId ?? mappedRun?.threadId;
|
|
931
|
+
let runId = resolvedRunId ?? mappedRun?.runId;
|
|
932
|
+
let runStatus = mappedRun?.status;
|
|
933
|
+
let channelId = resolvedChannelId ?? deriveChannelIdFromConversationKey(conversationKey);
|
|
934
|
+
// Recover latest known thread for channel-targeted conversations when in-memory mapping is empty.
|
|
935
|
+
if (!threadId && !runId && channelId) {
|
|
936
|
+
const listing = await api.listThreads({
|
|
937
|
+
bucket: "all",
|
|
938
|
+
channelId,
|
|
939
|
+
limit: 1
|
|
940
|
+
});
|
|
941
|
+
const latest = listing.threads[0];
|
|
942
|
+
if (latest) {
|
|
943
|
+
const recovered = toRunRefFromThreadSummary(latest);
|
|
944
|
+
threadId = recovered.threadId;
|
|
945
|
+
runId = recovered.runId;
|
|
946
|
+
runStatus = recovered.status;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
// Thread details give us canonical channel and latest session snapshot.
|
|
950
|
+
if (threadId && (!channelId || !runId || !runStatus)) {
|
|
951
|
+
const thread = await api.getThread(threadId);
|
|
952
|
+
channelId = channelId ?? thread.channel_id;
|
|
953
|
+
const detailRef = toRunRefFromThreadDetail(thread);
|
|
954
|
+
if (!runId && detailRef.runId) {
|
|
955
|
+
runId = detailRef.runId;
|
|
956
|
+
}
|
|
957
|
+
if (!runStatus && detailRef.status) {
|
|
958
|
+
runStatus = detailRef.status;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
// For mapped runs, verify status so terminal sessions rotate automatically.
|
|
962
|
+
if (runId && !resolvedRunId) {
|
|
963
|
+
try {
|
|
964
|
+
const runSnapshot = await api.getRun(runId);
|
|
965
|
+
const run = runSnapshot.run;
|
|
966
|
+
runStatus = toOptionalString(run.status) ?? runStatus;
|
|
967
|
+
if (!threadId) {
|
|
968
|
+
threadId = toOptionalString(run.thread_id) ?? threadId;
|
|
969
|
+
}
|
|
970
|
+
channelId = channelId ?? toOptionalString(run.channel_id);
|
|
971
|
+
}
|
|
972
|
+
catch {
|
|
973
|
+
runId = undefined;
|
|
974
|
+
runStatus = undefined;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (runId && !resolvedRunId && !isActiveRunStatus(runStatus)) {
|
|
978
|
+
await mappingStore.deleteRun(runId);
|
|
979
|
+
runId = undefined;
|
|
980
|
+
}
|
|
981
|
+
if (!runId) {
|
|
640
982
|
const started = await api.startRun({
|
|
641
|
-
title,
|
|
642
|
-
channelId:
|
|
643
|
-
|
|
983
|
+
title: deriveTitle(input),
|
|
984
|
+
channelId: channelId ?? api.getDefaultChannelId(),
|
|
985
|
+
threadId,
|
|
986
|
+
metadata: normalizeConversationMetadata(input.metadata, conversationKey)
|
|
644
987
|
});
|
|
645
988
|
threadId = started.thread_id;
|
|
646
989
|
runId = started.run_id;
|
|
990
|
+
runStatus = started.session_status ?? started.status;
|
|
991
|
+
channelId = channelId ?? api.getDefaultChannelId();
|
|
992
|
+
}
|
|
993
|
+
if (!threadId) {
|
|
994
|
+
throw new Error("Unable to resolve Lessinbox thread for outbound message");
|
|
647
995
|
}
|
|
648
|
-
await api.postMessage({
|
|
996
|
+
const message = await api.postMessage({
|
|
649
997
|
threadId,
|
|
650
998
|
runId,
|
|
651
999
|
text: input.text,
|
|
652
1000
|
kind: "text"
|
|
653
1001
|
});
|
|
1002
|
+
const resolvedMessageRunId = message.run_id ?? runId;
|
|
654
1003
|
if (conversationKey) {
|
|
655
|
-
if (mappedRun?.runId && mappedRun.runId !==
|
|
1004
|
+
if (mappedRun?.runId && mappedRun.runId !== resolvedMessageRunId) {
|
|
656
1005
|
await mappingStore.deleteRun(mappedRun.runId);
|
|
657
1006
|
}
|
|
658
|
-
await mappingStore.setConversationRun(conversationKey, {
|
|
659
|
-
|
|
1007
|
+
await mappingStore.setConversationRun(conversationKey, {
|
|
1008
|
+
threadId,
|
|
1009
|
+
...(resolvedMessageRunId ? { runId: resolvedMessageRunId } : {}),
|
|
1010
|
+
...(runStatus ? { status: runStatus } : {})
|
|
1011
|
+
});
|
|
1012
|
+
if (resolvedMessageRunId) {
|
|
1013
|
+
await mappingStore.setConversationKeyForRun(resolvedMessageRunId, conversationKey);
|
|
1014
|
+
}
|
|
660
1015
|
}
|
|
661
1016
|
return {
|
|
662
1017
|
ok: true,
|
|
663
1018
|
threadId,
|
|
664
|
-
runId
|
|
1019
|
+
runId: resolvedMessageRunId ?? runId
|
|
665
1020
|
};
|
|
666
1021
|
}
|
|
1022
|
+
async function sendMediaToLessinbox(input) {
|
|
1023
|
+
const mediaUrl = toOptionalString(input.mediaUrl);
|
|
1024
|
+
const caption = input.text?.trim() ?? "";
|
|
1025
|
+
if (!mediaUrl) {
|
|
1026
|
+
return sendTextToLessinbox({
|
|
1027
|
+
...input,
|
|
1028
|
+
text: caption
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
const messageText = [caption, `Attachment: ${mediaUrl}`].filter(Boolean).join("\n\n");
|
|
1032
|
+
return sendTextToLessinbox({
|
|
1033
|
+
...input,
|
|
1034
|
+
text: messageText || `Attachment: ${mediaUrl}`
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
async function sendReplyPayloadToLessinbox(input) {
|
|
1038
|
+
const mediaUrls = input.payload.mediaUrls?.length
|
|
1039
|
+
? input.payload.mediaUrls
|
|
1040
|
+
: input.payload.mediaUrl
|
|
1041
|
+
? [input.payload.mediaUrl]
|
|
1042
|
+
: [];
|
|
1043
|
+
if (mediaUrls.length === 0) {
|
|
1044
|
+
await sendTextToLessinbox({
|
|
1045
|
+
text: input.payload.text ?? "",
|
|
1046
|
+
accountId: input.accountId,
|
|
1047
|
+
config: input.config,
|
|
1048
|
+
threadId: input.threadId,
|
|
1049
|
+
runId: input.runId,
|
|
1050
|
+
conversationId: `thread:${input.threadId}`
|
|
1051
|
+
});
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
let first = true;
|
|
1055
|
+
for (const mediaUrl of mediaUrls) {
|
|
1056
|
+
await sendMediaToLessinbox({
|
|
1057
|
+
text: first ? input.payload.text ?? "" : "",
|
|
1058
|
+
mediaUrl,
|
|
1059
|
+
accountId: input.accountId,
|
|
1060
|
+
config: input.config,
|
|
1061
|
+
threadId: input.threadId,
|
|
1062
|
+
runId: input.runId,
|
|
1063
|
+
conversationId: `thread:${input.threadId}`
|
|
1064
|
+
});
|
|
1065
|
+
first = false;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
async function dispatchInboundMessageToRuntime(input) {
|
|
1069
|
+
const routing = input.runtime.channel?.routing?.resolveAgentRoute;
|
|
1070
|
+
const replyRuntime = input.runtime.channel?.reply;
|
|
1071
|
+
if (!routing || !replyRuntime?.finalizeInboundContext || !replyRuntime.dispatchReplyWithBufferedBlockDispatcher) {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const route = routing({
|
|
1075
|
+
cfg: input.cfg,
|
|
1076
|
+
channel: "lessinbox",
|
|
1077
|
+
accountId: input.accountId,
|
|
1078
|
+
peer: {
|
|
1079
|
+
kind: "direct",
|
|
1080
|
+
id: input.threadId
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
const routeSessionKey = toOptionalString(route.sessionKey) ??
|
|
1084
|
+
toOptionalString(route.mainSessionKey) ??
|
|
1085
|
+
`agent:main:lessinbox:thread:${input.threadId}`;
|
|
1086
|
+
const routeAccountId = toOptionalString(route.accountId) ?? input.accountId;
|
|
1087
|
+
const routeAgentId = toOptionalString(route.agentId);
|
|
1088
|
+
const ctxPayload = replyRuntime.finalizeInboundContext({
|
|
1089
|
+
Body: input.message.text,
|
|
1090
|
+
BodyForAgent: input.message.text,
|
|
1091
|
+
RawBody: input.message.text,
|
|
1092
|
+
CommandBody: input.message.text,
|
|
1093
|
+
From: `lessinbox:thread:${input.threadId}`,
|
|
1094
|
+
To: `thread:${input.threadId}`,
|
|
1095
|
+
SessionKey: routeSessionKey,
|
|
1096
|
+
AccountId: routeAccountId,
|
|
1097
|
+
ChatType: "direct",
|
|
1098
|
+
ConversationLabel: `Lessinbox thread ${input.threadId}`,
|
|
1099
|
+
SenderId: input.message.actorUserId ?? `thread:${input.threadId}`,
|
|
1100
|
+
Provider: "lessinbox",
|
|
1101
|
+
Surface: "lessinbox",
|
|
1102
|
+
MessageSid: input.message.id,
|
|
1103
|
+
Timestamp: input.message.createdAtMs ?? Date.now(),
|
|
1104
|
+
OriginatingChannel: "lessinbox",
|
|
1105
|
+
OriginatingTo: `thread:${input.threadId}`,
|
|
1106
|
+
CommandAuthorized: true
|
|
1107
|
+
});
|
|
1108
|
+
const sessionStore = asRecord(input.cfg)?.session;
|
|
1109
|
+
const resolveStorePath = input.runtime.channel?.session?.resolveStorePath;
|
|
1110
|
+
const recordInboundSession = input.runtime.channel?.session?.recordInboundSession;
|
|
1111
|
+
if (resolveStorePath && recordInboundSession) {
|
|
1112
|
+
const storePath = resolveStorePath(asRecord(sessionStore)?.store, { agentId: routeAgentId });
|
|
1113
|
+
await recordInboundSession({
|
|
1114
|
+
storePath,
|
|
1115
|
+
sessionKey: routeSessionKey,
|
|
1116
|
+
ctx: ctxPayload,
|
|
1117
|
+
onRecordError: (err) => {
|
|
1118
|
+
input.log?.error?.(`lessinbox failed recording inbound session metadata: ${formatError(err)}`);
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
input.runtime.system?.enqueueSystemEvent?.(`Lessinbox message in thread ${input.threadId}: ${input.message.text.slice(0, 160)}`, {
|
|
1123
|
+
sessionKey: routeSessionKey,
|
|
1124
|
+
contextKey: `lessinbox:message:${input.threadId}:${input.message.id}`
|
|
1125
|
+
});
|
|
1126
|
+
await replyRuntime.dispatchReplyWithBufferedBlockDispatcher({
|
|
1127
|
+
ctx: ctxPayload,
|
|
1128
|
+
cfg: input.cfg,
|
|
1129
|
+
dispatcherOptions: {
|
|
1130
|
+
deliver: async (payload) => {
|
|
1131
|
+
await sendReplyPayloadToLessinbox({
|
|
1132
|
+
payload,
|
|
1133
|
+
accountId: input.accountId,
|
|
1134
|
+
config: input.cfg,
|
|
1135
|
+
threadId: input.threadId,
|
|
1136
|
+
runId: input.runId
|
|
1137
|
+
});
|
|
1138
|
+
input.runtime.channel?.activity?.record?.({
|
|
1139
|
+
channel: "lessinbox",
|
|
1140
|
+
accountId: input.accountId,
|
|
1141
|
+
direction: "outbound"
|
|
1142
|
+
});
|
|
1143
|
+
},
|
|
1144
|
+
onError: (err, info) => {
|
|
1145
|
+
input.log?.error?.(`lessinbox ${info.kind} reply failed: ${formatError(err)}`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
async function handleGatewayInboundEvent(input) {
|
|
1151
|
+
if (input.event.kind !== "message.created") {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const threadId = input.event.threadId;
|
|
1155
|
+
if (!threadId) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const payloadMessage = resolveEventPayloadMessage(input.event);
|
|
1159
|
+
const messageId = toOptionalString(payloadMessage?.id);
|
|
1160
|
+
if (!messageId) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
const handledKey = deriveHandledInboundKey(input.accountId, messageId);
|
|
1164
|
+
if (hasHandledInboundMessage(handledKey)) {
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const feedMessage = await findFeedMessageById(input.api, threadId, messageId);
|
|
1168
|
+
const inboundMessage = coalesceInboundMessage(messageId, payloadMessage, feedMessage);
|
|
1169
|
+
if (!inboundMessage) {
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
if (inboundMessage.actorType !== "user") {
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
markHandledInboundMessage(handledKey);
|
|
1176
|
+
const conversationId = input.event.conversationId ?? `thread:${threadId}`;
|
|
1177
|
+
const runId = inboundMessage.runId ?? input.event.runId;
|
|
1178
|
+
if (runId) {
|
|
1179
|
+
await input.mappingStore.setConversationRun(conversationId, {
|
|
1180
|
+
threadId,
|
|
1181
|
+
runId,
|
|
1182
|
+
status: "running"
|
|
1183
|
+
});
|
|
1184
|
+
await input.mappingStore.setConversationKeyForRun(runId, conversationId);
|
|
1185
|
+
}
|
|
1186
|
+
const at = inboundMessage.createdAtMs ?? Date.now();
|
|
1187
|
+
input.runtime.channel?.activity?.record?.({
|
|
1188
|
+
channel: "lessinbox",
|
|
1189
|
+
accountId: input.accountId,
|
|
1190
|
+
direction: "inbound",
|
|
1191
|
+
at
|
|
1192
|
+
});
|
|
1193
|
+
input.setStatus({
|
|
1194
|
+
accountId: input.accountId,
|
|
1195
|
+
connected: true,
|
|
1196
|
+
lastInboundAt: at,
|
|
1197
|
+
lastError: null
|
|
1198
|
+
});
|
|
1199
|
+
await dispatchInboundMessageToRuntime({
|
|
1200
|
+
runtime: input.runtime,
|
|
1201
|
+
cfg: input.cfg,
|
|
1202
|
+
accountId: input.accountId,
|
|
1203
|
+
threadId,
|
|
1204
|
+
runId,
|
|
1205
|
+
message: inboundMessage,
|
|
1206
|
+
log: input.log
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
function cleanupConsumerIfUnused(consumerKey) {
|
|
1210
|
+
const consumer = workspaceConsumers.get(consumerKey);
|
|
1211
|
+
if (!consumer) {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
if (consumer.listenerCount() > 0) {
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
consumer.stop();
|
|
1218
|
+
workspaceConsumers.delete(consumerKey);
|
|
1219
|
+
}
|
|
1220
|
+
async function startLessinboxGatewayAccount(ctx) {
|
|
1221
|
+
const runtime = getLessinboxPluginRuntime();
|
|
1222
|
+
const accountId = deriveAccountRuntimeId(ctx.accountId);
|
|
1223
|
+
if (!runtime) {
|
|
1224
|
+
ctx.setStatus({
|
|
1225
|
+
accountId,
|
|
1226
|
+
running: false,
|
|
1227
|
+
connected: false,
|
|
1228
|
+
lastError: "Lessinbox plugin runtime not initialized"
|
|
1229
|
+
});
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (ctx.account.enabled === false) {
|
|
1233
|
+
ctx.setStatus({
|
|
1234
|
+
accountId,
|
|
1235
|
+
running: false,
|
|
1236
|
+
connected: false,
|
|
1237
|
+
lastError: "Lessinbox account is disabled"
|
|
1238
|
+
});
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const api = new LessinboxApi(ctx.account);
|
|
1242
|
+
const mappingStore = getConversationMappingStore(accountId, ctx.account);
|
|
1243
|
+
const consumer = ensureWorkspaceConsumer(accountId, ctx.account, api, mappingStore);
|
|
1244
|
+
const consumerKey = deriveWorkspaceConsumerRegistryKey(accountId, ctx.account);
|
|
1245
|
+
gatewayUnsubscribers.get(consumerKey)?.();
|
|
1246
|
+
gatewayUnsubscribers.delete(consumerKey);
|
|
1247
|
+
if (!consumer) {
|
|
1248
|
+
ctx.setStatus({
|
|
1249
|
+
accountId,
|
|
1250
|
+
running: false,
|
|
1251
|
+
connected: false,
|
|
1252
|
+
lastError: "Lessinbox workspace stream is disabled"
|
|
1253
|
+
});
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const unsubscribe = consumer.addListener(async (event) => {
|
|
1257
|
+
await handleGatewayInboundEvent({
|
|
1258
|
+
runtime,
|
|
1259
|
+
cfg: ctx.cfg,
|
|
1260
|
+
api,
|
|
1261
|
+
accountId,
|
|
1262
|
+
mappingStore,
|
|
1263
|
+
event,
|
|
1264
|
+
setStatus: ctx.setStatus,
|
|
1265
|
+
log: ctx.log
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
gatewayUnsubscribers.set(consumerKey, unsubscribe);
|
|
1269
|
+
consumer.start();
|
|
1270
|
+
ctx.setStatus({
|
|
1271
|
+
accountId,
|
|
1272
|
+
running: true,
|
|
1273
|
+
connected: true,
|
|
1274
|
+
lastStartAt: Date.now(),
|
|
1275
|
+
lastError: null
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
async function stopLessinboxGatewayAccount(ctx) {
|
|
1279
|
+
const accountId = deriveAccountRuntimeId(ctx.accountId);
|
|
1280
|
+
const consumerKey = deriveWorkspaceConsumerRegistryKey(accountId, ctx.account);
|
|
1281
|
+
const unsubscribe = gatewayUnsubscribers.get(consumerKey);
|
|
1282
|
+
if (unsubscribe) {
|
|
1283
|
+
unsubscribe();
|
|
1284
|
+
gatewayUnsubscribers.delete(consumerKey);
|
|
1285
|
+
}
|
|
1286
|
+
cleanupConsumerIfUnused(consumerKey);
|
|
1287
|
+
ctx.setStatus({
|
|
1288
|
+
accountId,
|
|
1289
|
+
running: false,
|
|
1290
|
+
connected: false,
|
|
1291
|
+
lastStopAt: Date.now()
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
667
1294
|
export function subscribeToLessinboxEvents(input) {
|
|
668
1295
|
const accountRuntimeId = deriveAccountRuntimeId(input.accountId);
|
|
669
1296
|
const account = resolveAccountConfig(input.config, accountRuntimeId);
|
|
@@ -678,16 +1305,6 @@ export function subscribeToLessinboxEvents(input) {
|
|
|
678
1305
|
}
|
|
679
1306
|
return consumer.addListener(input.onEvent);
|
|
680
1307
|
}
|
|
681
|
-
function subscribeFromPlugin(input) {
|
|
682
|
-
if (!input.config) {
|
|
683
|
-
throw new Error("Plugin inbound.subscribe did not receive runtime config");
|
|
684
|
-
}
|
|
685
|
-
return subscribeToLessinboxEvents({
|
|
686
|
-
config: input.config,
|
|
687
|
-
accountId: input.accountId,
|
|
688
|
-
onEvent: input.onEvent
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
1308
|
const plugin = {
|
|
692
1309
|
id: "lessinbox",
|
|
693
1310
|
meta: {
|
|
@@ -699,7 +1316,12 @@ const plugin = {
|
|
|
699
1316
|
aliases: ["li", "lessinbox"]
|
|
700
1317
|
},
|
|
701
1318
|
capabilities: {
|
|
702
|
-
chatTypes: ["direct", "thread"]
|
|
1319
|
+
chatTypes: ["direct", "thread"],
|
|
1320
|
+
threads: true,
|
|
1321
|
+
media: true
|
|
1322
|
+
},
|
|
1323
|
+
reload: {
|
|
1324
|
+
configPrefixes: ["channels.lessinbox"]
|
|
703
1325
|
},
|
|
704
1326
|
config: {
|
|
705
1327
|
listAccountIds: (cfg) => {
|
|
@@ -709,14 +1331,36 @@ const plugin = {
|
|
|
709
1331
|
}
|
|
710
1332
|
return Object.keys(accounts);
|
|
711
1333
|
},
|
|
712
|
-
resolveAccount: (cfg, accountId) => resolveAccountConfig(cfg, accountId)
|
|
1334
|
+
resolveAccount: (cfg, accountId) => resolveAccountConfig(cfg, accountId),
|
|
1335
|
+
isConfigured: (account) => Boolean(account.apiUrl && account.apiKey && account.workspaceId && account.defaultChannelId),
|
|
1336
|
+
describeAccount: (account) => ({
|
|
1337
|
+
accountId: account.accountId ?? "default",
|
|
1338
|
+
enabled: account.enabled !== false,
|
|
1339
|
+
configured: Boolean(account.apiUrl && account.apiKey && account.workspaceId && account.defaultChannelId),
|
|
1340
|
+
workspaceId: account.workspaceId,
|
|
1341
|
+
apiUrl: account.apiUrl
|
|
1342
|
+
})
|
|
713
1343
|
},
|
|
714
1344
|
outbound: {
|
|
715
1345
|
deliveryMode: "direct",
|
|
716
|
-
sendText: sendTextToLessinbox
|
|
1346
|
+
sendText: sendTextToLessinbox,
|
|
1347
|
+
sendMedia: sendMediaToLessinbox
|
|
1348
|
+
},
|
|
1349
|
+
gateway: {
|
|
1350
|
+
startAccount: startLessinboxGatewayAccount,
|
|
1351
|
+
stopAccount: stopLessinboxGatewayAccount
|
|
717
1352
|
},
|
|
718
|
-
|
|
719
|
-
|
|
1353
|
+
status: {
|
|
1354
|
+
defaultRuntime: {
|
|
1355
|
+
accountId: "default",
|
|
1356
|
+
running: false,
|
|
1357
|
+
connected: false,
|
|
1358
|
+
lastConnectedAt: null,
|
|
1359
|
+
lastDisconnect: null,
|
|
1360
|
+
lastStartAt: null,
|
|
1361
|
+
lastStopAt: null,
|
|
1362
|
+
lastError: null
|
|
1363
|
+
}
|
|
720
1364
|
}
|
|
721
1365
|
};
|
|
722
1366
|
export function createLessinboxPlugin() {
|
|
@@ -726,10 +1370,15 @@ export default function register(api) {
|
|
|
726
1370
|
if (!api || typeof api.registerChannel !== "function") {
|
|
727
1371
|
throw new Error("Openclaw plugin runtime did not expose registerChannel");
|
|
728
1372
|
}
|
|
1373
|
+
setLessinboxPluginRuntime(api.runtime);
|
|
729
1374
|
api.registerChannel({ plugin });
|
|
730
|
-
if (typeof api.
|
|
731
|
-
api.
|
|
732
|
-
|
|
1375
|
+
if (typeof api.registerService === "function") {
|
|
1376
|
+
api.registerService({
|
|
1377
|
+
id: "lessinbox-channel-runtime",
|
|
1378
|
+
start: () => undefined,
|
|
1379
|
+
stop: async () => {
|
|
1380
|
+
await shutdownLessinboxPluginResources();
|
|
1381
|
+
}
|
|
733
1382
|
});
|
|
734
1383
|
}
|
|
735
1384
|
}
|