@archipelagolab/lobi 1.0.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/CHANGELOG.md +164 -0
- package/ENDOFFILE +0 -0
- package/EOF +0 -0
- package/LICENSE +21 -0
- package/SPEC-SUPPORT.md +116 -0
- package/YAMLEND +0 -0
- package/api.ts +18 -0
- package/archipelagolab-lobi-1.0.0.tgz +0 -0
- package/auth-presence.ts +56 -0
- package/channel-plugin-api.ts +3 -0
- package/cli-metadata.ts +11 -0
- package/contract-api.ts +17 -0
- package/docs/CHECKLIST.md +83 -0
- package/docs/FORK_SDK_GUIDE.md +279 -0
- package/helper-api.ts +3 -0
- package/index.test.ts +61 -0
- package/index.ts +65 -0
- package/openclaw.plugin.json +23 -0
- package/package.json +52 -0
- package/plugin-entry.handlers.runtime.ts +1 -0
- package/runtime-api.ts +54 -0
- package/runtime-heavy-api.ts +1 -0
- package/scripts/migrate-to-lobi.sh +72 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/src/account-selection.test.ts +124 -0
- package/src/account-selection.ts +226 -0
- package/src/actions.account-propagation.test.ts +251 -0
- package/src/actions.test.ts +251 -0
- package/src/actions.ts +336 -0
- package/src/approval-auth.test.ts +23 -0
- package/src/approval-auth.ts +25 -0
- package/src/approval-handler.runtime.test.ts +46 -0
- package/src/approval-handler.runtime.ts +400 -0
- package/src/approval-ids.ts +6 -0
- package/src/approval-native.test.ts +329 -0
- package/src/approval-native.ts +336 -0
- package/src/approval-reactions.test.ts +107 -0
- package/src/approval-reactions.ts +158 -0
- package/src/auth-precedence.ts +61 -0
- package/src/channel-account-paths.ts +92 -0
- package/src/channel.account-paths.test.ts +102 -0
- package/src/channel.directory.test.ts +601 -0
- package/src/channel.resolve.test.ts +38 -0
- package/src/channel.runtime.ts +16 -0
- package/src/channel.setup.test.ts +269 -0
- package/src/channel.ts +570 -0
- package/src/cli-metadata.ts +19 -0
- package/src/cli.test.ts +1015 -0
- package/src/cli.ts +1198 -0
- package/src/config-adapter.ts +41 -0
- package/src/config-schema.test.ts +90 -0
- package/src/config-schema.ts +114 -0
- package/src/directory-live.test.ts +200 -0
- package/src/directory-live.ts +238 -0
- package/src/doctor-contract.ts +287 -0
- package/src/doctor.test.ts +440 -0
- package/src/doctor.ts +262 -0
- package/src/env-vars.ts +92 -0
- package/src/exec-approval-resolver.test.ts +68 -0
- package/src/exec-approval-resolver.ts +23 -0
- package/src/exec-approvals.test.ts +483 -0
- package/src/exec-approvals.ts +290 -0
- package/src/group-mentions.ts +41 -0
- package/src/legacy-crypto-inspector-availability.test.ts +81 -0
- package/src/legacy-crypto-inspector-availability.ts +60 -0
- package/src/legacy-crypto.test.ts +234 -0
- package/src/legacy-crypto.ts +549 -0
- package/src/legacy-state.test.ts +86 -0
- package/src/legacy-state.ts +156 -0
- package/src/matrix/account-config.ts +150 -0
- package/src/matrix/accounts.readiness.test.ts +27 -0
- package/src/matrix/accounts.test.ts +757 -0
- package/src/matrix/accounts.ts +194 -0
- package/src/matrix/actions/client.test.ts +215 -0
- package/src/matrix/actions/client.ts +31 -0
- package/src/matrix/actions/devices.test.ts +114 -0
- package/src/matrix/actions/devices.ts +34 -0
- package/src/matrix/actions/limits.test.ts +15 -0
- package/src/matrix/actions/limits.ts +6 -0
- package/src/matrix/actions/messages.test.ts +289 -0
- package/src/matrix/actions/messages.ts +123 -0
- package/src/matrix/actions/pins.test.ts +74 -0
- package/src/matrix/actions/pins.ts +64 -0
- package/src/matrix/actions/polls.test.ts +71 -0
- package/src/matrix/actions/polls.ts +109 -0
- package/src/matrix/actions/profile.test.ts +109 -0
- package/src/matrix/actions/profile.ts +37 -0
- package/src/matrix/actions/reactions.test.ts +135 -0
- package/src/matrix/actions/reactions.ts +59 -0
- package/src/matrix/actions/room.test.ts +79 -0
- package/src/matrix/actions/room.ts +71 -0
- package/src/matrix/actions/summary.test.ts +87 -0
- package/src/matrix/actions/summary.ts +88 -0
- package/src/matrix/actions/types.ts +82 -0
- package/src/matrix/actions/verification.test.ts +105 -0
- package/src/matrix/actions/verification.ts +237 -0
- package/src/matrix/actions.ts +37 -0
- package/src/matrix/active-client.ts +26 -0
- package/src/matrix/async-lock.ts +18 -0
- package/src/matrix/backup-health.ts +115 -0
- package/src/matrix/client/config-runtime-api.ts +14 -0
- package/src/matrix/client/config-secret-input.runtime.ts +1 -0
- package/src/matrix/client/config.ts +982 -0
- package/src/matrix/client/create-client.test.ts +115 -0
- package/src/matrix/client/create-client.ts +101 -0
- package/src/matrix/client/env-auth.ts +6 -0
- package/src/matrix/client/file-sync-store.test.ts +265 -0
- package/src/matrix/client/file-sync-store.ts +289 -0
- package/src/matrix/client/logging.ts +123 -0
- package/src/matrix/client/migration-snapshot.runtime.ts +1 -0
- package/src/matrix/client/private-network-host.ts +56 -0
- package/src/matrix/client/runtime.ts +4 -0
- package/src/matrix/client/shared.test.ts +344 -0
- package/src/matrix/client/shared.ts +306 -0
- package/src/matrix/client/storage.test.ts +634 -0
- package/src/matrix/client/storage.ts +544 -0
- package/src/matrix/client/types.ts +50 -0
- package/src/matrix/client-bootstrap.test.ts +84 -0
- package/src/matrix/client-bootstrap.ts +164 -0
- package/src/matrix/client-resolver.test-helpers.ts +147 -0
- package/src/matrix/client.test.ts +1521 -0
- package/src/matrix/client.ts +23 -0
- package/src/matrix/config-paths.ts +31 -0
- package/src/matrix/config-update.test.ts +237 -0
- package/src/matrix/config-update.ts +291 -0
- package/src/matrix/credentials-read.ts +206 -0
- package/src/matrix/credentials-write.runtime.ts +26 -0
- package/src/matrix/credentials.test.ts +501 -0
- package/src/matrix/credentials.ts +95 -0
- package/src/matrix/deps.test.ts +74 -0
- package/src/matrix/deps.ts +225 -0
- package/src/matrix/device-health.test.ts +45 -0
- package/src/matrix/device-health.ts +31 -0
- package/src/matrix/direct-management.test.ts +350 -0
- package/src/matrix/direct-management.ts +347 -0
- package/src/matrix/direct-room.test.ts +61 -0
- package/src/matrix/direct-room.ts +128 -0
- package/src/matrix/draft-stream.test.ts +406 -0
- package/src/matrix/draft-stream.ts +216 -0
- package/src/matrix/encryption-guidance.ts +27 -0
- package/src/matrix/errors.ts +21 -0
- package/src/matrix/format.test.ts +340 -0
- package/src/matrix/format.ts +428 -0
- package/src/matrix/legacy-crypto-inspector.ts +95 -0
- package/src/matrix/media-errors.ts +20 -0
- package/src/matrix/media-text.ts +169 -0
- package/src/matrix/monitor/access-state.test.ts +45 -0
- package/src/matrix/monitor/access-state.ts +77 -0
- package/src/matrix/monitor/ack-config.test.ts +57 -0
- package/src/matrix/monitor/ack-config.ts +26 -0
- package/src/matrix/monitor/allowlist.test.ts +45 -0
- package/src/matrix/monitor/allowlist.ts +94 -0
- package/src/matrix/monitor/auto-join.test.ts +203 -0
- package/src/matrix/monitor/auto-join.ts +86 -0
- package/src/matrix/monitor/config.test.ts +197 -0
- package/src/matrix/monitor/config.ts +303 -0
- package/src/matrix/monitor/context-summary.ts +43 -0
- package/src/matrix/monitor/direct.test.ts +529 -0
- package/src/matrix/monitor/direct.ts +270 -0
- package/src/matrix/monitor/events.test.ts +1524 -0
- package/src/matrix/monitor/events.ts +213 -0
- package/src/matrix/monitor/handler.body-for-agent.test.ts +396 -0
- package/src/matrix/monitor/handler.group-history.test.ts +648 -0
- package/src/matrix/monitor/handler.media-failure.test.ts +267 -0
- package/src/matrix/monitor/handler.test-helpers.ts +308 -0
- package/src/matrix/monitor/handler.test.ts +2952 -0
- package/src/matrix/monitor/handler.thread-root-media.test.ts +82 -0
- package/src/matrix/monitor/handler.ts +1679 -0
- package/src/matrix/monitor/inbound-dedupe.test.ts +146 -0
- package/src/matrix/monitor/inbound-dedupe.ts +267 -0
- package/src/matrix/monitor/index.test.ts +920 -0
- package/src/matrix/monitor/index.ts +434 -0
- package/src/matrix/monitor/legacy-crypto-restore.test.ts +206 -0
- package/src/matrix/monitor/legacy-crypto-restore.ts +139 -0
- package/src/matrix/monitor/location.ts +100 -0
- package/src/matrix/monitor/media.test.ts +159 -0
- package/src/matrix/monitor/media.ts +119 -0
- package/src/matrix/monitor/mentions.test.ts +289 -0
- package/src/matrix/monitor/mentions.ts +177 -0
- package/src/matrix/monitor/reaction-events.test.ts +326 -0
- package/src/matrix/monitor/reaction-events.ts +187 -0
- package/src/matrix/monitor/recent-invite.test.ts +92 -0
- package/src/matrix/monitor/recent-invite.ts +30 -0
- package/src/matrix/monitor/replies.test.ts +265 -0
- package/src/matrix/monitor/replies.ts +136 -0
- package/src/matrix/monitor/reply-context.test.ts +276 -0
- package/src/matrix/monitor/reply-context.ts +92 -0
- package/src/matrix/monitor/room-history.test.ts +258 -0
- package/src/matrix/monitor/room-history.ts +301 -0
- package/src/matrix/monitor/room-info.test.ts +201 -0
- package/src/matrix/monitor/room-info.ts +126 -0
- package/src/matrix/monitor/rooms.test.ts +121 -0
- package/src/matrix/monitor/rooms.ts +52 -0
- package/src/matrix/monitor/route.test.ts +255 -0
- package/src/matrix/monitor/route.ts +178 -0
- package/src/matrix/monitor/runtime-api.ts +31 -0
- package/src/matrix/monitor/startup-verification.test.ts +294 -0
- package/src/matrix/monitor/startup-verification.ts +237 -0
- package/src/matrix/monitor/startup.test.ts +257 -0
- package/src/matrix/monitor/startup.ts +218 -0
- package/src/matrix/monitor/status.ts +111 -0
- package/src/matrix/monitor/sync-lifecycle.test.ts +224 -0
- package/src/matrix/monitor/sync-lifecycle.ts +91 -0
- package/src/matrix/monitor/task-runner.ts +38 -0
- package/src/matrix/monitor/thread-context.test.ts +149 -0
- package/src/matrix/monitor/thread-context.ts +108 -0
- package/src/matrix/monitor/threads.test.ts +68 -0
- package/src/matrix/monitor/threads.ts +85 -0
- package/src/matrix/monitor/types.ts +30 -0
- package/src/matrix/monitor/verification-events.ts +627 -0
- package/src/matrix/monitor/verification-utils.test.ts +47 -0
- package/src/matrix/monitor/verification-utils.ts +46 -0
- package/src/matrix/outbound-media-runtime.ts +1 -0
- package/src/matrix/poll-summary.ts +110 -0
- package/src/matrix/poll-types.test.ts +205 -0
- package/src/matrix/poll-types.ts +433 -0
- package/src/matrix/probe.runtime.ts +4 -0
- package/src/matrix/probe.test.ts +154 -0
- package/src/matrix/probe.ts +96 -0
- package/src/matrix/profile.test.ts +154 -0
- package/src/matrix/profile.ts +184 -0
- package/src/matrix/reaction-common.test.ts +96 -0
- package/src/matrix/reaction-common.ts +147 -0
- package/src/matrix/sdk/crypto-bootstrap.test.ts +505 -0
- package/src/matrix/sdk/crypto-bootstrap.ts +341 -0
- package/src/matrix/sdk/crypto-facade.test.ts +197 -0
- package/src/matrix/sdk/crypto-facade.ts +207 -0
- package/src/matrix/sdk/crypto-node.runtime.test.ts +27 -0
- package/src/matrix/sdk/crypto-node.runtime.ts +9 -0
- package/src/matrix/sdk/crypto-runtime.ts +11 -0
- package/src/matrix/sdk/decrypt-bridge.ts +356 -0
- package/src/matrix/sdk/event-helpers.test.ts +60 -0
- package/src/matrix/sdk/event-helpers.ts +71 -0
- package/src/matrix/sdk/http-client.test.ts +134 -0
- package/src/matrix/sdk/http-client.ts +87 -0
- package/src/matrix/sdk/idb-persistence-lock.ts +51 -0
- package/src/matrix/sdk/idb-persistence.lock-order.test.ts +108 -0
- package/src/matrix/sdk/idb-persistence.test-helpers.ts +88 -0
- package/src/matrix/sdk/idb-persistence.test.ts +149 -0
- package/src/matrix/sdk/idb-persistence.ts +283 -0
- package/src/matrix/sdk/logger.test.ts +25 -0
- package/src/matrix/sdk/logger.ts +108 -0
- package/src/matrix/sdk/read-response-with-limit.ts +19 -0
- package/src/matrix/sdk/recovery-key-store.test.ts +385 -0
- package/src/matrix/sdk/recovery-key-store.ts +430 -0
- package/src/matrix/sdk/transport.test.ts +161 -0
- package/src/matrix/sdk/transport.ts +344 -0
- package/src/matrix/sdk/types.ts +236 -0
- package/src/matrix/sdk/verification-manager.test.ts +509 -0
- package/src/matrix/sdk/verification-manager.ts +694 -0
- package/src/matrix/sdk/verification-status.ts +23 -0
- package/src/matrix/sdk.test.ts +2568 -0
- package/src/matrix/sdk.ts +1789 -0
- package/src/matrix/send/client.test.ts +174 -0
- package/src/matrix/send/client.ts +90 -0
- package/src/matrix/send/formatting.ts +189 -0
- package/src/matrix/send/media.ts +244 -0
- package/src/matrix/send/targets.test.ts +254 -0
- package/src/matrix/send/targets.ts +104 -0
- package/src/matrix/send/types.ts +134 -0
- package/src/matrix/send.test.ts +958 -0
- package/src/matrix/send.ts +609 -0
- package/src/matrix/session-store-metadata.ts +108 -0
- package/src/matrix/startup-abort.ts +44 -0
- package/src/matrix/sync-state.ts +27 -0
- package/src/matrix/target-ids.ts +102 -0
- package/src/matrix/thread-bindings-shared.ts +201 -0
- package/src/matrix/thread-bindings.test.ts +673 -0
- package/src/matrix/thread-bindings.ts +577 -0
- package/src/matrix-migration.runtime.ts +9 -0
- package/src/migration-config.test.ts +228 -0
- package/src/migration-config.ts +243 -0
- package/src/migration-snapshot-backup.ts +117 -0
- package/src/migration-snapshot.test.ts +184 -0
- package/src/migration-snapshot.ts +55 -0
- package/src/onboarding.resolve.test.ts +55 -0
- package/src/onboarding.test-harness.ts +158 -0
- package/src/onboarding.test.ts +665 -0
- package/src/onboarding.ts +773 -0
- package/src/outbound.test.ts +173 -0
- package/src/outbound.ts +78 -0
- package/src/plugin-entry.runtime.js +159 -0
- package/src/plugin-entry.runtime.test.ts +108 -0
- package/src/plugin-entry.runtime.ts +68 -0
- package/src/profile-update.ts +68 -0
- package/src/record-shared.ts +3 -0
- package/src/resolve-targets.test.ts +178 -0
- package/src/resolve-targets.ts +175 -0
- package/src/resolver.ts +21 -0
- package/src/runtime-api.ts +144 -0
- package/src/runtime.ts +7 -0
- package/src/secret-contract.ts +174 -0
- package/src/session-route.test.ts +315 -0
- package/src/session-route.ts +113 -0
- package/src/setup-bootstrap.ts +94 -0
- package/src/setup-config.ts +222 -0
- package/src/setup-contract.ts +89 -0
- package/src/setup-core.test.ts +326 -0
- package/src/setup-core.ts +50 -0
- package/src/setup-surface.ts +4 -0
- package/src/startup-maintenance.test.ts +227 -0
- package/src/startup-maintenance.ts +114 -0
- package/src/storage-paths.ts +92 -0
- package/src/test-helpers.ts +42 -0
- package/src/test-mocks.ts +55 -0
- package/src/test-runtime.ts +72 -0
- package/src/test-support/monitor-route-test-support.ts +8 -0
- package/src/tool-actions.runtime.ts +1 -0
- package/src/tool-actions.test.ts +422 -0
- package/src/tool-actions.ts +498 -0
- package/src/types.ts +230 -0
- package/test-api.ts +2 -0
- package/thread-bindings-runtime.ts +4 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { PluginRuntime } from "../runtime-api.js";
|
|
3
|
+
|
|
4
|
+
const loadConfigMock = vi.fn(() => ({}));
|
|
5
|
+
const resolveTextChunkLimitMock = vi.fn<
|
|
6
|
+
(cfg: unknown, channel: unknown, accountId?: unknown) => number
|
|
7
|
+
>(() => 4000);
|
|
8
|
+
const resolveChunkModeMock = vi.fn<(cfg: unknown, channel: unknown, accountId?: unknown) => string>(
|
|
9
|
+
() => "length",
|
|
10
|
+
);
|
|
11
|
+
const chunkMarkdownTextWithModeMock = vi.fn((text: string) => (text ? [text] : []));
|
|
12
|
+
const convertMarkdownTablesMock = vi.fn((text: string) => text);
|
|
13
|
+
const runtimeStub = {
|
|
14
|
+
config: { loadConfig: () => loadConfigMock() },
|
|
15
|
+
channel: {
|
|
16
|
+
text: {
|
|
17
|
+
resolveTextChunkLimit: (cfg: unknown, channel: unknown, accountId?: unknown) =>
|
|
18
|
+
resolveTextChunkLimitMock(cfg, channel, accountId),
|
|
19
|
+
resolveChunkMode: (cfg: unknown, channel: unknown, accountId?: unknown) =>
|
|
20
|
+
resolveChunkModeMock(cfg, channel, accountId),
|
|
21
|
+
chunkMarkdownText: (text: string) => (text ? [text] : []),
|
|
22
|
+
chunkMarkdownTextWithMode: (text: string) => chunkMarkdownTextWithModeMock(text),
|
|
23
|
+
resolveMarkdownTableMode: () => "code",
|
|
24
|
+
convertMarkdownTables: (text: string) => convertMarkdownTablesMock(text),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
} as unknown as PluginRuntime;
|
|
28
|
+
|
|
29
|
+
let createMatrixDraftStream: typeof import("./draft-stream.js").createMatrixDraftStream;
|
|
30
|
+
|
|
31
|
+
const sendMessageMock = vi.fn();
|
|
32
|
+
const sendEventMock = vi.fn();
|
|
33
|
+
const joinedRoomsMock = vi.fn().mockResolvedValue([]);
|
|
34
|
+
|
|
35
|
+
function createMockClient() {
|
|
36
|
+
sendMessageMock.mockReset().mockResolvedValue("$evt1");
|
|
37
|
+
sendEventMock.mockReset().mockResolvedValue("$evt2");
|
|
38
|
+
joinedRoomsMock.mockReset().mockResolvedValue(["!room:test"]);
|
|
39
|
+
return {
|
|
40
|
+
sendMessage: sendMessageMock,
|
|
41
|
+
sendEvent: sendEventMock,
|
|
42
|
+
getJoinedRooms: joinedRoomsMock,
|
|
43
|
+
prepareForOneOff: vi.fn().mockResolvedValue(undefined),
|
|
44
|
+
start: vi.fn().mockResolvedValue(undefined),
|
|
45
|
+
} as unknown as import("./sdk.js").MatrixClient;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
beforeAll(async () => {
|
|
49
|
+
const runtimeModule = await import("../runtime.js");
|
|
50
|
+
runtimeModule.setMatrixRuntime(runtimeStub);
|
|
51
|
+
({ createMatrixDraftStream } = await import("./draft-stream.js"));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("createMatrixDraftStream", () => {
|
|
55
|
+
let client: ReturnType<typeof createMockClient>;
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
vi.useFakeTimers();
|
|
59
|
+
client = createMockClient();
|
|
60
|
+
resolveTextChunkLimitMock.mockReset().mockReturnValue(4000);
|
|
61
|
+
resolveChunkModeMock.mockReset().mockReturnValue("length");
|
|
62
|
+
chunkMarkdownTextWithModeMock
|
|
63
|
+
.mockReset()
|
|
64
|
+
.mockImplementation((text: string) => (text ? [text] : []));
|
|
65
|
+
convertMarkdownTablesMock.mockReset().mockImplementation((text: string) => text);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
vi.useRealTimers();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("sends a normal text preview on first partial update", async () => {
|
|
73
|
+
const stream = createMatrixDraftStream({
|
|
74
|
+
roomId: "!room:test",
|
|
75
|
+
client,
|
|
76
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
stream.update("Hello");
|
|
80
|
+
await stream.flush();
|
|
81
|
+
|
|
82
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(sendMessageMock.mock.calls[0]?.[1]).toMatchObject({
|
|
84
|
+
msgtype: "m.text",
|
|
85
|
+
});
|
|
86
|
+
expect(stream.eventId()).toBe("$evt1");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("sends quiet preview notices when quiet mode is enabled", async () => {
|
|
90
|
+
const stream = createMatrixDraftStream({
|
|
91
|
+
roomId: "!room:test",
|
|
92
|
+
client,
|
|
93
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
94
|
+
mode: "quiet",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
stream.update("Hello");
|
|
98
|
+
await stream.flush();
|
|
99
|
+
|
|
100
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
|
101
|
+
expect(sendMessageMock.mock.calls[0]?.[1]).toMatchObject({
|
|
102
|
+
msgtype: "m.notice",
|
|
103
|
+
});
|
|
104
|
+
expect(sendMessageMock.mock.calls[0]?.[1]).not.toHaveProperty("m.mentions");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("edits the message on subsequent quiet updates", async () => {
|
|
108
|
+
const stream = createMatrixDraftStream({
|
|
109
|
+
roomId: "!room:test",
|
|
110
|
+
client,
|
|
111
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
112
|
+
mode: "quiet",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
stream.update("Hello");
|
|
116
|
+
await stream.flush();
|
|
117
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
|
118
|
+
|
|
119
|
+
// Advance past throttle window so the next update fires immediately.
|
|
120
|
+
vi.advanceTimersByTime(1000);
|
|
121
|
+
|
|
122
|
+
stream.update("Hello world");
|
|
123
|
+
await stream.flush();
|
|
124
|
+
|
|
125
|
+
// First call = initial send, second call = edit (both go through sendMessage)
|
|
126
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(2);
|
|
127
|
+
expect(sendMessageMock.mock.calls[1]?.[1]).toMatchObject({
|
|
128
|
+
msgtype: "m.notice",
|
|
129
|
+
"m.new_content": { msgtype: "m.notice" },
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("coalesces rapid quiet updates within throttle window", async () => {
|
|
134
|
+
const stream = createMatrixDraftStream({
|
|
135
|
+
roomId: "!room:test",
|
|
136
|
+
client,
|
|
137
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
138
|
+
mode: "quiet",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
stream.update("A");
|
|
142
|
+
stream.update("AB");
|
|
143
|
+
stream.update("ABC");
|
|
144
|
+
await stream.flush();
|
|
145
|
+
|
|
146
|
+
// First update fires immediately (fresh throttle window), then AB/ABC
|
|
147
|
+
// coalesce into a single edit with the latest text.
|
|
148
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(2);
|
|
149
|
+
expect(sendMessageMock.mock.calls[0][1]).toMatchObject({ body: "A" });
|
|
150
|
+
// Edit uses "* <text>" prefix per Matrix m.replace spec.
|
|
151
|
+
expect(sendMessageMock.mock.calls[1][1]).toMatchObject({ body: "* ABC" });
|
|
152
|
+
expect(sendMessageMock.mock.calls[0][1]).toMatchObject({ msgtype: "m.notice" });
|
|
153
|
+
expect(sendMessageMock.mock.calls[1][1]).toMatchObject({
|
|
154
|
+
msgtype: "m.notice",
|
|
155
|
+
"m.new_content": { msgtype: "m.notice" },
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("skips no-op updates", async () => {
|
|
160
|
+
const stream = createMatrixDraftStream({
|
|
161
|
+
roomId: "!room:test",
|
|
162
|
+
client,
|
|
163
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
stream.update("Hello");
|
|
167
|
+
await stream.flush();
|
|
168
|
+
const callCount = sendMessageMock.mock.calls.length;
|
|
169
|
+
|
|
170
|
+
vi.advanceTimersByTime(1000);
|
|
171
|
+
|
|
172
|
+
// Same text again — should not send
|
|
173
|
+
stream.update("Hello");
|
|
174
|
+
await stream.flush();
|
|
175
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(callCount);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("ignores updates after stop", async () => {
|
|
179
|
+
const stream = createMatrixDraftStream({
|
|
180
|
+
roomId: "!room:test",
|
|
181
|
+
client,
|
|
182
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
stream.update("Hello");
|
|
186
|
+
await stream.stop();
|
|
187
|
+
const callCount = sendMessageMock.mock.calls.length;
|
|
188
|
+
|
|
189
|
+
stream.update("Ignored");
|
|
190
|
+
await stream.flush();
|
|
191
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(callCount);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("stop returns the event ID", async () => {
|
|
195
|
+
const stream = createMatrixDraftStream({
|
|
196
|
+
roomId: "!room:test",
|
|
197
|
+
client,
|
|
198
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
stream.update("Hello");
|
|
202
|
+
const eventId = await stream.stop();
|
|
203
|
+
expect(eventId).toBe("$evt1");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("stop does not finalize live drafts on its own", async () => {
|
|
207
|
+
const stream = createMatrixDraftStream({
|
|
208
|
+
roomId: "!room:test",
|
|
209
|
+
client,
|
|
210
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
211
|
+
mode: "partial",
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
stream.update("Hello");
|
|
215
|
+
await stream.stop();
|
|
216
|
+
|
|
217
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
|
218
|
+
expect(sendMessageMock.mock.calls[0]?.[1]).toHaveProperty("org.matrix.msc4357.live");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("finalizeLive clears the live marker at most once", async () => {
|
|
222
|
+
const stream = createMatrixDraftStream({
|
|
223
|
+
roomId: "!room:test",
|
|
224
|
+
client,
|
|
225
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
226
|
+
mode: "partial",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
stream.update("Hello");
|
|
230
|
+
await stream.stop();
|
|
231
|
+
|
|
232
|
+
await stream.finalizeLive();
|
|
233
|
+
await stream.finalizeLive();
|
|
234
|
+
|
|
235
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(2);
|
|
236
|
+
expect(sendMessageMock.mock.calls[1]?.[1]).not.toHaveProperty("org.matrix.msc4357.live");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("marks live finalize failures for normal final delivery fallback", async () => {
|
|
240
|
+
sendMessageMock.mockResolvedValueOnce("$evt1").mockRejectedValueOnce(new Error("rate limited"));
|
|
241
|
+
|
|
242
|
+
const stream = createMatrixDraftStream({
|
|
243
|
+
roomId: "!room:test",
|
|
244
|
+
client,
|
|
245
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
246
|
+
mode: "partial",
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
stream.update("Hello");
|
|
250
|
+
await stream.stop();
|
|
251
|
+
|
|
252
|
+
await expect(stream.finalizeLive()).resolves.toBe(false);
|
|
253
|
+
expect(stream.mustDeliverFinalNormally()).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("reset allows reuse for next block", async () => {
|
|
257
|
+
sendMessageMock.mockResolvedValueOnce("$first").mockResolvedValueOnce("$second");
|
|
258
|
+
|
|
259
|
+
const stream = createMatrixDraftStream({
|
|
260
|
+
roomId: "!room:test",
|
|
261
|
+
client,
|
|
262
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
263
|
+
mode: "quiet",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
stream.update("Block 1");
|
|
267
|
+
await stream.stop();
|
|
268
|
+
expect(stream.eventId()).toBe("$first");
|
|
269
|
+
|
|
270
|
+
stream.reset();
|
|
271
|
+
expect(stream.eventId()).toBeUndefined();
|
|
272
|
+
|
|
273
|
+
stream.update("Block 2");
|
|
274
|
+
await stream.stop();
|
|
275
|
+
expect(stream.eventId()).toBe("$second");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("stops retrying after send failure", async () => {
|
|
279
|
+
sendMessageMock.mockRejectedValueOnce(new Error("network error"));
|
|
280
|
+
|
|
281
|
+
const log = vi.fn();
|
|
282
|
+
const stream = createMatrixDraftStream({
|
|
283
|
+
roomId: "!room:test",
|
|
284
|
+
client,
|
|
285
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
286
|
+
log,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
stream.update("Hello");
|
|
290
|
+
await stream.flush();
|
|
291
|
+
|
|
292
|
+
// Should have logged the failure
|
|
293
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining("send/edit failed"));
|
|
294
|
+
|
|
295
|
+
vi.advanceTimersByTime(1000);
|
|
296
|
+
|
|
297
|
+
// Further updates should not attempt sends (stream is stopped)
|
|
298
|
+
stream.update("More text");
|
|
299
|
+
await stream.flush();
|
|
300
|
+
|
|
301
|
+
// Only the initial failed attempt
|
|
302
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
|
303
|
+
expect(stream.eventId()).toBeUndefined();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("skips empty/whitespace text", async () => {
|
|
307
|
+
const stream = createMatrixDraftStream({
|
|
308
|
+
roomId: "!room:test",
|
|
309
|
+
client,
|
|
310
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
stream.update(" ");
|
|
314
|
+
await stream.flush();
|
|
315
|
+
|
|
316
|
+
expect(sendMessageMock).not.toHaveBeenCalled();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("stops on edit failure mid-stream", async () => {
|
|
320
|
+
sendMessageMock
|
|
321
|
+
.mockResolvedValueOnce("$evt1") // initial send succeeds
|
|
322
|
+
.mockRejectedValueOnce(new Error("rate limited")); // edit fails
|
|
323
|
+
|
|
324
|
+
const log = vi.fn();
|
|
325
|
+
const stream = createMatrixDraftStream({
|
|
326
|
+
roomId: "!room:test",
|
|
327
|
+
client,
|
|
328
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
329
|
+
log,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
stream.update("Hello");
|
|
333
|
+
await stream.flush();
|
|
334
|
+
expect(stream.eventId()).toBe("$evt1");
|
|
335
|
+
|
|
336
|
+
vi.advanceTimersByTime(1000);
|
|
337
|
+
|
|
338
|
+
stream.update("Hello world");
|
|
339
|
+
await stream.flush();
|
|
340
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining("send/edit failed"));
|
|
341
|
+
|
|
342
|
+
vi.advanceTimersByTime(1000);
|
|
343
|
+
|
|
344
|
+
// Stream should be stopped — further updates are ignored
|
|
345
|
+
stream.update("More text");
|
|
346
|
+
await stream.flush();
|
|
347
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(2);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("bypasses newline chunking for the draft preview message", async () => {
|
|
351
|
+
resolveChunkModeMock.mockReturnValue("newline");
|
|
352
|
+
chunkMarkdownTextWithModeMock.mockImplementation((text: string) => text.split("\n"));
|
|
353
|
+
|
|
354
|
+
const stream = createMatrixDraftStream({
|
|
355
|
+
roomId: "!room:test",
|
|
356
|
+
client,
|
|
357
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
stream.update("line 1\nline 2");
|
|
361
|
+
await stream.flush();
|
|
362
|
+
|
|
363
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
|
364
|
+
expect(sendMessageMock.mock.calls[0]?.[1]).toMatchObject({ body: "line 1\nline 2" });
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("falls back to normal delivery when preview text exceeds one Matrix event", async () => {
|
|
368
|
+
const log = vi.fn();
|
|
369
|
+
resolveTextChunkLimitMock.mockReturnValue(5);
|
|
370
|
+
const stream = createMatrixDraftStream({
|
|
371
|
+
roomId: "!room:test",
|
|
372
|
+
client,
|
|
373
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
374
|
+
log,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
stream.update("123456");
|
|
378
|
+
await stream.flush();
|
|
379
|
+
|
|
380
|
+
expect(sendMessageMock).not.toHaveBeenCalled();
|
|
381
|
+
expect(stream.eventId()).toBeUndefined();
|
|
382
|
+
expect(log).toHaveBeenCalledWith(
|
|
383
|
+
expect.stringContaining("preview exceeded single-event limit"),
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("uses converted Matrix text when checking the single-event preview limit", async () => {
|
|
388
|
+
const log = vi.fn();
|
|
389
|
+
resolveTextChunkLimitMock.mockReturnValue(5);
|
|
390
|
+
convertMarkdownTablesMock.mockImplementation(() => "123456");
|
|
391
|
+
const stream = createMatrixDraftStream({
|
|
392
|
+
roomId: "!room:test",
|
|
393
|
+
client,
|
|
394
|
+
cfg: {} as import("../types.js").CoreConfig,
|
|
395
|
+
log,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
stream.update("1234");
|
|
399
|
+
await stream.flush();
|
|
400
|
+
|
|
401
|
+
expect(sendMessageMock).not.toHaveBeenCalled();
|
|
402
|
+
expect(log).toHaveBeenCalledWith(
|
|
403
|
+
expect.stringContaining("preview exceeded single-event limit"),
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createDraftStreamLoop } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
2
|
+
import type { CoreConfig } from "../types.js";
|
|
3
|
+
import type { MatrixClient } from "./sdk.js";
|
|
4
|
+
import { editMessageMatrix, prepareMatrixSingleText, sendSingleTextMessageMatrix } from "./send.js";
|
|
5
|
+
import { MsgType } from "./send/types.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_THROTTLE_MS = 1000;
|
|
8
|
+
type MatrixDraftPreviewMode = "partial" | "quiet";
|
|
9
|
+
|
|
10
|
+
function resolveDraftPreviewOptions(mode: MatrixDraftPreviewMode): {
|
|
11
|
+
msgtype: typeof MsgType.Text | typeof MsgType.Notice;
|
|
12
|
+
includeMentions?: boolean;
|
|
13
|
+
} {
|
|
14
|
+
if (mode === "quiet") {
|
|
15
|
+
return {
|
|
16
|
+
msgtype: MsgType.Notice,
|
|
17
|
+
includeMentions: false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
msgtype: MsgType.Text,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type MatrixDraftStream = {
|
|
26
|
+
/** Update the draft with the latest accumulated text for the current block. */
|
|
27
|
+
update: (text: string) => void;
|
|
28
|
+
/** Ensure the last pending update has been sent. */
|
|
29
|
+
flush: () => Promise<void>;
|
|
30
|
+
/** Flush and mark this block as done. Returns the event ID if a message was sent. */
|
|
31
|
+
stop: () => Promise<string | undefined>;
|
|
32
|
+
/** Clear the MSC4357 live marker in place when the draft is kept as final text. */
|
|
33
|
+
finalizeLive: () => Promise<boolean>;
|
|
34
|
+
/** Reset state for the next text block (after tool calls). */
|
|
35
|
+
reset: () => void;
|
|
36
|
+
/** The event ID of the current draft message, if any. */
|
|
37
|
+
eventId: () => string | undefined;
|
|
38
|
+
/** True when the provided text matches the last rendered draft payload. */
|
|
39
|
+
matchesPreparedText: (text: string) => boolean;
|
|
40
|
+
/** True when preview streaming must fall back to normal final delivery. */
|
|
41
|
+
mustDeliverFinalNormally: () => boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function createMatrixDraftStream(params: {
|
|
45
|
+
roomId: string;
|
|
46
|
+
client: MatrixClient;
|
|
47
|
+
cfg: CoreConfig;
|
|
48
|
+
mode?: MatrixDraftPreviewMode;
|
|
49
|
+
threadId?: string;
|
|
50
|
+
replyToId?: string;
|
|
51
|
+
/** When true, reset() restores the original replyToId instead of clearing it. */
|
|
52
|
+
preserveReplyId?: boolean;
|
|
53
|
+
accountId?: string;
|
|
54
|
+
log?: (message: string) => void;
|
|
55
|
+
}): MatrixDraftStream {
|
|
56
|
+
const { roomId, client, cfg, threadId, accountId, log } = params;
|
|
57
|
+
const preview = resolveDraftPreviewOptions(params.mode ?? "partial");
|
|
58
|
+
// MSC4357 live markers are only useful for "partial" mode where users see
|
|
59
|
+
// the draft evolve. "quiet" mode uses m.notice for background previews
|
|
60
|
+
// where a streaming animation would be unexpected.
|
|
61
|
+
const useLive = params.mode !== "quiet";
|
|
62
|
+
|
|
63
|
+
let currentEventId: string | undefined;
|
|
64
|
+
let lastSentText = "";
|
|
65
|
+
let stopped = false;
|
|
66
|
+
let sendFailed = false;
|
|
67
|
+
let finalizeInPlaceBlocked = false;
|
|
68
|
+
let liveFinalized = false;
|
|
69
|
+
let replyToId = params.replyToId;
|
|
70
|
+
|
|
71
|
+
const sendOrEdit = async (text: string): Promise<boolean> => {
|
|
72
|
+
const trimmed = text.trimEnd();
|
|
73
|
+
if (!trimmed) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const preparedText = prepareMatrixSingleText(trimmed, { cfg, accountId });
|
|
77
|
+
if (!preparedText.fitsInSingleEvent) {
|
|
78
|
+
finalizeInPlaceBlocked = true;
|
|
79
|
+
if (!currentEventId) {
|
|
80
|
+
sendFailed = true;
|
|
81
|
+
}
|
|
82
|
+
stopped = true;
|
|
83
|
+
log?.(
|
|
84
|
+
`draft-stream: preview exceeded single-event limit (${preparedText.convertedText.length} > ${preparedText.singleEventLimit})`,
|
|
85
|
+
);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (sendFailed) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (preparedText.trimmedText === lastSentText) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
if (!currentEventId) {
|
|
96
|
+
const result = await sendSingleTextMessageMatrix(roomId, preparedText.trimmedText, {
|
|
97
|
+
client,
|
|
98
|
+
cfg,
|
|
99
|
+
replyToId,
|
|
100
|
+
threadId,
|
|
101
|
+
accountId,
|
|
102
|
+
msgtype: preview.msgtype,
|
|
103
|
+
includeMentions: preview.includeMentions,
|
|
104
|
+
live: useLive,
|
|
105
|
+
});
|
|
106
|
+
currentEventId = result.messageId;
|
|
107
|
+
lastSentText = preparedText.trimmedText;
|
|
108
|
+
log?.(`draft-stream: created message ${currentEventId}${useLive ? " (MSC4357 live)" : ""}`);
|
|
109
|
+
} else {
|
|
110
|
+
await editMessageMatrix(roomId, currentEventId, preparedText.trimmedText, {
|
|
111
|
+
client,
|
|
112
|
+
cfg,
|
|
113
|
+
threadId,
|
|
114
|
+
accountId,
|
|
115
|
+
msgtype: preview.msgtype,
|
|
116
|
+
includeMentions: preview.includeMentions,
|
|
117
|
+
live: useLive,
|
|
118
|
+
});
|
|
119
|
+
lastSentText = preparedText.trimmedText;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
log?.(`draft-stream: send/edit failed: ${String(err)}`);
|
|
124
|
+
const isPreviewLimitError =
|
|
125
|
+
err instanceof Error && err.message.startsWith("Matrix single-message text exceeds limit");
|
|
126
|
+
if (isPreviewLimitError) {
|
|
127
|
+
finalizeInPlaceBlocked = true;
|
|
128
|
+
}
|
|
129
|
+
if (!currentEventId) {
|
|
130
|
+
sendFailed = true;
|
|
131
|
+
}
|
|
132
|
+
stopped = true;
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const loop = createDraftStreamLoop({
|
|
138
|
+
throttleMs: DEFAULT_THROTTLE_MS,
|
|
139
|
+
isStopped: () => stopped,
|
|
140
|
+
sendOrEditStreamMessage: sendOrEdit,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
log?.(`draft-stream: ready (throttleMs=${DEFAULT_THROTTLE_MS})`);
|
|
144
|
+
|
|
145
|
+
const finalizeLive = async (): Promise<boolean> => {
|
|
146
|
+
// Send a final edit without the MSC4357 live marker to signal that
|
|
147
|
+
// the stream is complete. Supporting clients will stop the streaming
|
|
148
|
+
// animation and display the final content.
|
|
149
|
+
if (useLive && !liveFinalized && currentEventId && lastSentText) {
|
|
150
|
+
liveFinalized = true;
|
|
151
|
+
try {
|
|
152
|
+
await editMessageMatrix(roomId, currentEventId, lastSentText, {
|
|
153
|
+
client,
|
|
154
|
+
cfg,
|
|
155
|
+
threadId,
|
|
156
|
+
accountId,
|
|
157
|
+
msgtype: preview.msgtype,
|
|
158
|
+
includeMentions: preview.includeMentions,
|
|
159
|
+
live: false,
|
|
160
|
+
});
|
|
161
|
+
log?.(`draft-stream: finalized ${currentEventId} (MSC4357 stream ended)`);
|
|
162
|
+
return true;
|
|
163
|
+
} catch (err) {
|
|
164
|
+
log?.(`draft-stream: finalize edit failed: ${String(err)}`);
|
|
165
|
+
// If the finalize edit fails, the live marker remains on the last
|
|
166
|
+
// successful edit. Flag the stream so callers can fall back to
|
|
167
|
+
// normal final delivery or redaction instead of leaving the message
|
|
168
|
+
// stuck in a "still streaming" state for MSC4357 clients.
|
|
169
|
+
finalizeInPlaceBlocked = true;
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const stop = async (): Promise<string | undefined> => {
|
|
177
|
+
// Flush before marking stopped so the loop can drain pending text.
|
|
178
|
+
await loop.flush();
|
|
179
|
+
stopped = true;
|
|
180
|
+
return currentEventId;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const reset = (): void => {
|
|
184
|
+
// Clear reply context unless preserveReplyId is set (replyToMode "all"),
|
|
185
|
+
// in which case subsequent blocks should keep replying to the original.
|
|
186
|
+
replyToId = params.preserveReplyId ? params.replyToId : undefined;
|
|
187
|
+
currentEventId = undefined;
|
|
188
|
+
lastSentText = "";
|
|
189
|
+
stopped = false;
|
|
190
|
+
sendFailed = false;
|
|
191
|
+
finalizeInPlaceBlocked = false;
|
|
192
|
+
liveFinalized = false;
|
|
193
|
+
loop.resetPending();
|
|
194
|
+
loop.resetThrottleWindow();
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
update: (text: string) => {
|
|
199
|
+
if (stopped) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
loop.update(text);
|
|
203
|
+
},
|
|
204
|
+
flush: loop.flush,
|
|
205
|
+
stop,
|
|
206
|
+
finalizeLive,
|
|
207
|
+
reset,
|
|
208
|
+
eventId: () => currentEventId,
|
|
209
|
+
matchesPreparedText: (text: string) =>
|
|
210
|
+
prepareMatrixSingleText(text, {
|
|
211
|
+
cfg,
|
|
212
|
+
accountId,
|
|
213
|
+
}).trimmedText === lastSentText,
|
|
214
|
+
mustDeliverFinalNormally: () => sendFailed || finalizeInPlaceBlocked,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
|
+
import { resolveMatrixDefaultOrOnlyAccountId } from "../account-selection.js";
|
|
3
|
+
import type { CoreConfig } from "../types.js";
|
|
4
|
+
import { resolveMatrixConfigFieldPath } from "./config-paths.js";
|
|
5
|
+
|
|
6
|
+
export function resolveMatrixEncryptionConfigPath(
|
|
7
|
+
cfg: CoreConfig,
|
|
8
|
+
accountId?: string | null,
|
|
9
|
+
): string {
|
|
10
|
+
const effectiveAccountId =
|
|
11
|
+
normalizeOptionalAccountId(accountId) ?? resolveMatrixDefaultOrOnlyAccountId(cfg);
|
|
12
|
+
return resolveMatrixConfigFieldPath(cfg, effectiveAccountId, "encryption");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatMatrixEncryptionUnavailableError(
|
|
16
|
+
cfg: CoreConfig,
|
|
17
|
+
accountId?: string | null,
|
|
18
|
+
): string {
|
|
19
|
+
return `Matrix encryption is not available (enable ${resolveMatrixEncryptionConfigPath(cfg, accountId)}=true)`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatMatrixEncryptedEventDisabledWarning(
|
|
23
|
+
cfg: CoreConfig,
|
|
24
|
+
accountId?: string | null,
|
|
25
|
+
): string {
|
|
26
|
+
return `matrix: encrypted event received without encryption enabled; set ${resolveMatrixEncryptionConfigPath(cfg, accountId)}=true and verify the device to decrypt`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
2
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
3
|
+
|
|
4
|
+
export function formatMatrixErrorMessage(err: unknown): string {
|
|
5
|
+
return formatErrorMessage(err);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatMatrixErrorReason(err: unknown): string {
|
|
9
|
+
return normalizeLowercaseStringOrEmpty(formatMatrixErrorMessage(err));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isMatrixNotFoundError(err: unknown): boolean {
|
|
13
|
+
const errObj = err as { statusCode?: number; body?: { errcode?: string } };
|
|
14
|
+
if (errObj?.statusCode === 404 || errObj?.body?.errcode === "M_NOT_FOUND") {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
const message = formatMatrixErrorReason(err);
|
|
18
|
+
return (
|
|
19
|
+
message.includes("m_not_found") || message.includes("[404]") || message.includes("not found")
|
|
20
|
+
);
|
|
21
|
+
}
|