mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor: modularize plugin runtime and test hooks
This commit is contained in:
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -223,8 +223,8 @@ jobs:
|
||||
# Types, lint, and format check.
|
||||
check:
|
||||
name: "check"
|
||||
needs: [docs-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true'
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true')
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
28
docs/ci.md
28
docs/ci.md
@@ -13,20 +13,20 @@ The CI runs on every push to `main` and every pull request. It uses smart scopin
|
||||
|
||||
## Job Overview
|
||||
|
||||
| Job | Purpose | When it runs |
|
||||
| ----------------- | ----------------------------------------------- | ------------------------- |
|
||||
| `docs-scope` | Detect docs-only changes | Always |
|
||||
| `changed-scope` | Detect which areas changed (node/macos/android) | Non-docs PRs |
|
||||
| `check` | TypeScript types, lint, format | Non-docs changes |
|
||||
| `check-docs` | Markdown lint + broken link check | Docs changed |
|
||||
| `code-analysis` | LOC threshold check (1000 lines) | PRs only |
|
||||
| `secrets` | Detect leaked secrets | Always |
|
||||
| `build-artifacts` | Build dist once, share with other jobs | Non-docs, node changes |
|
||||
| `release-check` | Validate npm pack contents | After build |
|
||||
| `checks` | Node/Bun tests + protocol check | Non-docs, node changes |
|
||||
| `checks-windows` | Windows-specific tests | Non-docs, node changes |
|
||||
| `macos` | Swift lint/build/test + TS tests | PRs with macos changes |
|
||||
| `android` | Gradle build + tests | Non-docs, android changes |
|
||||
| Job | Purpose | When it runs |
|
||||
| ----------------- | ----------------------------------------------- | ------------------------------------------------- |
|
||||
| `docs-scope` | Detect docs-only changes | Always |
|
||||
| `changed-scope` | Detect which areas changed (node/macos/android) | Non-docs PRs |
|
||||
| `check` | TypeScript types, lint, format | Push to `main`, or PRs with Node-relevant changes |
|
||||
| `check-docs` | Markdown lint + broken link check | Docs changed |
|
||||
| `code-analysis` | LOC threshold check (1000 lines) | PRs only |
|
||||
| `secrets` | Detect leaked secrets | Always |
|
||||
| `build-artifacts` | Build dist once, share with other jobs | Non-docs, node changes |
|
||||
| `release-check` | Validate npm pack contents | After build |
|
||||
| `checks` | Node/Bun tests + protocol check | Non-docs, node changes |
|
||||
| `checks-windows` | Windows-specific tests | Non-docs, node changes |
|
||||
| `macos` | Swift lint/build/test + TS tests | PRs with macos changes |
|
||||
| `android` | Gradle build + tests | Non-docs, android changes |
|
||||
|
||||
## Fail-Fast Order
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
||||
import { removeAckReactionAfterReply, shouldAckReaction } from "openclaw/plugin-sdk";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
|
||||
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
|
||||
import { fetchBlueBubblesHistory } from "./history.js";
|
||||
import {
|
||||
@@ -94,47 +94,15 @@ const mockResolveChunkMode = vi.fn(() => "length");
|
||||
const mockFetchBlueBubblesHistory = vi.mocked(fetchBlueBubblesHistory);
|
||||
|
||||
function createMockRuntime(): PluginRuntime {
|
||||
return {
|
||||
version: "1.0.0",
|
||||
config: {
|
||||
loadConfig: vi.fn(() => ({})) as unknown as PluginRuntime["config"]["loadConfig"],
|
||||
writeConfigFile: vi.fn() as unknown as PluginRuntime["config"]["writeConfigFile"],
|
||||
},
|
||||
return createPluginRuntimeMock({
|
||||
system: {
|
||||
enqueueSystemEvent:
|
||||
mockEnqueueSystemEvent as unknown as PluginRuntime["system"]["enqueueSystemEvent"],
|
||||
requestHeartbeatNow: vi.fn() as unknown as PluginRuntime["system"]["requestHeartbeatNow"],
|
||||
runCommandWithTimeout: vi.fn() as unknown as PluginRuntime["system"]["runCommandWithTimeout"],
|
||||
formatNativeDependencyHint: vi.fn(
|
||||
() => "",
|
||||
) as unknown as PluginRuntime["system"]["formatNativeDependencyHint"],
|
||||
},
|
||||
media: {
|
||||
loadWebMedia: vi.fn() as unknown as PluginRuntime["media"]["loadWebMedia"],
|
||||
detectMime: vi.fn() as unknown as PluginRuntime["media"]["detectMime"],
|
||||
mediaKindFromMime: vi.fn() as unknown as PluginRuntime["media"]["mediaKindFromMime"],
|
||||
isVoiceCompatibleAudio:
|
||||
vi.fn() as unknown as PluginRuntime["media"]["isVoiceCompatibleAudio"],
|
||||
getImageMetadata: vi.fn() as unknown as PluginRuntime["media"]["getImageMetadata"],
|
||||
resizeToJpeg: vi.fn() as unknown as PluginRuntime["media"]["resizeToJpeg"],
|
||||
},
|
||||
tts: {
|
||||
textToSpeechTelephony: vi.fn() as unknown as PluginRuntime["tts"]["textToSpeechTelephony"],
|
||||
},
|
||||
stt: {
|
||||
transcribeAudioFile: vi.fn() as unknown as PluginRuntime["stt"]["transcribeAudioFile"],
|
||||
},
|
||||
tools: {
|
||||
createMemoryGetTool: vi.fn() as unknown as PluginRuntime["tools"]["createMemoryGetTool"],
|
||||
createMemorySearchTool:
|
||||
vi.fn() as unknown as PluginRuntime["tools"]["createMemorySearchTool"],
|
||||
registerMemoryCli: vi.fn() as unknown as PluginRuntime["tools"]["registerMemoryCli"],
|
||||
},
|
||||
channel: {
|
||||
text: {
|
||||
chunkMarkdownText:
|
||||
mockChunkMarkdownText as unknown as PluginRuntime["channel"]["text"]["chunkMarkdownText"],
|
||||
chunkText: vi.fn() as unknown as PluginRuntime["channel"]["text"]["chunkText"],
|
||||
chunkByNewline:
|
||||
mockChunkByNewline as unknown as PluginRuntime["channel"]["text"]["chunkByNewline"],
|
||||
chunkMarkdownTextWithMode:
|
||||
@@ -143,50 +111,12 @@ function createMockRuntime(): PluginRuntime {
|
||||
mockChunkTextWithMode as unknown as PluginRuntime["channel"]["text"]["chunkTextWithMode"],
|
||||
resolveChunkMode:
|
||||
mockResolveChunkMode as unknown as PluginRuntime["channel"]["text"]["resolveChunkMode"],
|
||||
resolveTextChunkLimit: vi.fn(
|
||||
() => 4000,
|
||||
) as unknown as PluginRuntime["channel"]["text"]["resolveTextChunkLimit"],
|
||||
hasControlCommand:
|
||||
mockHasControlCommand as unknown as PluginRuntime["channel"]["text"]["hasControlCommand"],
|
||||
resolveMarkdownTableMode: vi.fn(
|
||||
() => "code",
|
||||
) as unknown as PluginRuntime["channel"]["text"]["resolveMarkdownTableMode"],
|
||||
convertMarkdownTables: vi.fn(
|
||||
(text: string) => text,
|
||||
) as unknown as PluginRuntime["channel"]["text"]["convertMarkdownTables"],
|
||||
},
|
||||
reply: {
|
||||
dispatchReplyWithBufferedBlockDispatcher:
|
||||
mockDispatchReplyWithBufferedBlockDispatcher as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
|
||||
createReplyDispatcherWithTyping:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["createReplyDispatcherWithTyping"],
|
||||
resolveEffectiveMessagesConfig:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveEffectiveMessagesConfig"],
|
||||
resolveHumanDelayConfig:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveHumanDelayConfig"],
|
||||
dispatchReplyFromConfig:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyFromConfig"],
|
||||
withReplyDispatcher: vi.fn(
|
||||
async ({
|
||||
dispatcher,
|
||||
run,
|
||||
onSettled,
|
||||
}: Parameters<PluginRuntime["channel"]["reply"]["withReplyDispatcher"]>[0]) => {
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
dispatcher.markComplete();
|
||||
try {
|
||||
await dispatcher.waitForIdle();
|
||||
} finally {
|
||||
await onSettled?.();
|
||||
}
|
||||
}
|
||||
},
|
||||
) as unknown as PluginRuntime["channel"]["reply"]["withReplyDispatcher"],
|
||||
finalizeInboundContext: vi.fn(
|
||||
(ctx: Record<string, unknown>) => ctx,
|
||||
) as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
|
||||
formatAgentEnvelope:
|
||||
mockFormatAgentEnvelope as unknown as PluginRuntime["channel"]["reply"]["formatAgentEnvelope"],
|
||||
formatInboundEnvelope:
|
||||
@@ -207,8 +137,6 @@ function createMockRuntime(): PluginRuntime {
|
||||
mockUpsertPairingRequest as unknown as PluginRuntime["channel"]["pairing"]["upsertPairingRequest"],
|
||||
},
|
||||
media: {
|
||||
fetchRemoteMedia:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"],
|
||||
saveMediaBuffer:
|
||||
mockSaveMediaBuffer as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
|
||||
},
|
||||
@@ -217,12 +145,6 @@ function createMockRuntime(): PluginRuntime {
|
||||
mockResolveStorePath as unknown as PluginRuntime["channel"]["session"]["resolveStorePath"],
|
||||
readSessionUpdatedAt:
|
||||
mockReadSessionUpdatedAt as unknown as PluginRuntime["channel"]["session"]["readSessionUpdatedAt"],
|
||||
recordInboundSession:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
|
||||
recordSessionMetaFromInbound:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordSessionMetaFromInbound"],
|
||||
updateLastRoute:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["session"]["updateLastRoute"],
|
||||
},
|
||||
mentions: {
|
||||
buildMentionRegexes:
|
||||
@@ -232,72 +154,18 @@ function createMockRuntime(): PluginRuntime {
|
||||
matchesMentionWithExplicit:
|
||||
mockMatchesMentionWithExplicit as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionWithExplicit"],
|
||||
},
|
||||
reactions: {
|
||||
shouldAckReaction,
|
||||
removeAckReactionAfterReply,
|
||||
},
|
||||
groups: {
|
||||
resolveGroupPolicy:
|
||||
mockResolveGroupPolicy as unknown as PluginRuntime["channel"]["groups"]["resolveGroupPolicy"],
|
||||
resolveRequireMention:
|
||||
mockResolveRequireMention as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"],
|
||||
},
|
||||
debounce: {
|
||||
// Create a pass-through debouncer that immediately calls onFlush
|
||||
createInboundDebouncer: vi.fn(
|
||||
(params: { onFlush: (items: unknown[]) => Promise<void> }) => ({
|
||||
enqueue: async (item: unknown) => {
|
||||
await params.onFlush([item]);
|
||||
},
|
||||
flushKey: vi.fn(),
|
||||
}),
|
||||
) as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"],
|
||||
resolveInboundDebounceMs: vi.fn(
|
||||
() => 0,
|
||||
) as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"],
|
||||
},
|
||||
commands: {
|
||||
resolveCommandAuthorizedFromAuthorizers:
|
||||
mockResolveCommandAuthorizedFromAuthorizers as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
|
||||
isControlCommandMessage:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["isControlCommandMessage"],
|
||||
shouldComputeCommandAuthorized:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldComputeCommandAuthorized"],
|
||||
shouldHandleTextCommands:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldHandleTextCommands"],
|
||||
},
|
||||
discord: {} as PluginRuntime["channel"]["discord"],
|
||||
activity: {} as PluginRuntime["channel"]["activity"],
|
||||
line: {} as PluginRuntime["channel"]["line"],
|
||||
slack: {} as PluginRuntime["channel"]["slack"],
|
||||
telegram: {} as PluginRuntime["channel"]["telegram"],
|
||||
signal: {} as PluginRuntime["channel"]["signal"],
|
||||
imessage: {} as PluginRuntime["channel"]["imessage"],
|
||||
whatsapp: {} as PluginRuntime["channel"]["whatsapp"],
|
||||
},
|
||||
events: {
|
||||
onAgentEvent: vi.fn(() => () => {}) as unknown as PluginRuntime["events"]["onAgentEvent"],
|
||||
onSessionTranscriptUpdate: vi.fn(
|
||||
() => () => {},
|
||||
) as unknown as PluginRuntime["events"]["onSessionTranscriptUpdate"],
|
||||
},
|
||||
logging: {
|
||||
shouldLogVerbose: vi.fn(
|
||||
() => false,
|
||||
) as unknown as PluginRuntime["logging"]["shouldLogVerbose"],
|
||||
getChildLogger: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
})) as unknown as PluginRuntime["logging"]["getChildLogger"],
|
||||
},
|
||||
state: {
|
||||
resolveStateDir: vi.fn(
|
||||
() => "/tmp/openclaw",
|
||||
) as unknown as PluginRuntime["state"]["resolveStateDir"],
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function createMockAccount(
|
||||
|
||||
244
extensions/test-utils/plugin-runtime-mock.ts
Normal file
244
extensions/test-utils/plugin-runtime-mock.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
||||
import { removeAckReactionAfterReply, shouldAckReaction } from "openclaw/plugin-sdk";
|
||||
import { vi } from "vitest";
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[K in keyof T]?: T[K] extends (...args: never[]) => unknown
|
||||
? T[K]
|
||||
: T[K] extends ReadonlyArray<unknown>
|
||||
? T[K]
|
||||
: T[K] extends object
|
||||
? DeepPartial<T[K]>
|
||||
: T[K];
|
||||
};
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function mergeDeep<T>(base: T, overrides: DeepPartial<T>): T {
|
||||
const result: Record<string, unknown> = { ...(base as Record<string, unknown>) };
|
||||
for (const [key, overrideValue] of Object.entries(overrides as Record<string, unknown>)) {
|
||||
if (overrideValue === undefined) {
|
||||
continue;
|
||||
}
|
||||
const baseValue = result[key];
|
||||
if (isObject(baseValue) && isObject(overrideValue)) {
|
||||
result[key] = mergeDeep(baseValue, overrideValue);
|
||||
continue;
|
||||
}
|
||||
result[key] = overrideValue;
|
||||
}
|
||||
return result as T;
|
||||
}
|
||||
|
||||
export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> = {}): PluginRuntime {
|
||||
const base: PluginRuntime = {
|
||||
version: "1.0.0-test",
|
||||
config: {
|
||||
loadConfig: vi.fn(() => ({})) as unknown as PluginRuntime["config"]["loadConfig"],
|
||||
writeConfigFile: vi.fn() as unknown as PluginRuntime["config"]["writeConfigFile"],
|
||||
},
|
||||
system: {
|
||||
enqueueSystemEvent: vi.fn() as unknown as PluginRuntime["system"]["enqueueSystemEvent"],
|
||||
requestHeartbeatNow: vi.fn() as unknown as PluginRuntime["system"]["requestHeartbeatNow"],
|
||||
runCommandWithTimeout: vi.fn() as unknown as PluginRuntime["system"]["runCommandWithTimeout"],
|
||||
formatNativeDependencyHint: vi.fn(
|
||||
() => "",
|
||||
) as unknown as PluginRuntime["system"]["formatNativeDependencyHint"],
|
||||
},
|
||||
media: {
|
||||
loadWebMedia: vi.fn() as unknown as PluginRuntime["media"]["loadWebMedia"],
|
||||
detectMime: vi.fn() as unknown as PluginRuntime["media"]["detectMime"],
|
||||
mediaKindFromMime: vi.fn() as unknown as PluginRuntime["media"]["mediaKindFromMime"],
|
||||
isVoiceCompatibleAudio:
|
||||
vi.fn() as unknown as PluginRuntime["media"]["isVoiceCompatibleAudio"],
|
||||
getImageMetadata: vi.fn() as unknown as PluginRuntime["media"]["getImageMetadata"],
|
||||
resizeToJpeg: vi.fn() as unknown as PluginRuntime["media"]["resizeToJpeg"],
|
||||
},
|
||||
tts: {
|
||||
textToSpeechTelephony: vi.fn() as unknown as PluginRuntime["tts"]["textToSpeechTelephony"],
|
||||
},
|
||||
stt: {
|
||||
transcribeAudioFile: vi.fn() as unknown as PluginRuntime["stt"]["transcribeAudioFile"],
|
||||
},
|
||||
tools: {
|
||||
createMemoryGetTool: vi.fn() as unknown as PluginRuntime["tools"]["createMemoryGetTool"],
|
||||
createMemorySearchTool:
|
||||
vi.fn() as unknown as PluginRuntime["tools"]["createMemorySearchTool"],
|
||||
registerMemoryCli: vi.fn() as unknown as PluginRuntime["tools"]["registerMemoryCli"],
|
||||
},
|
||||
channel: {
|
||||
text: {
|
||||
chunkByNewline: vi.fn((text: string) => (text ? [text] : [])),
|
||||
chunkMarkdownText: vi.fn((text: string) => [text]),
|
||||
chunkMarkdownTextWithMode: vi.fn((text: string) => (text ? [text] : [])),
|
||||
chunkText: vi.fn((text: string) => (text ? [text] : [])),
|
||||
chunkTextWithMode: vi.fn((text: string) => (text ? [text] : [])),
|
||||
resolveChunkMode: vi.fn(() => "length"),
|
||||
resolveTextChunkLimit: vi.fn(() => 4000),
|
||||
hasControlCommand: vi.fn(() => false),
|
||||
resolveMarkdownTableMode: vi.fn(() => "code"),
|
||||
convertMarkdownTables: vi.fn((text: string) => text),
|
||||
},
|
||||
reply: {
|
||||
dispatchReplyWithBufferedBlockDispatcher: vi.fn(
|
||||
async () => undefined,
|
||||
) as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
|
||||
createReplyDispatcherWithTyping:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["createReplyDispatcherWithTyping"],
|
||||
resolveEffectiveMessagesConfig:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveEffectiveMessagesConfig"],
|
||||
resolveHumanDelayConfig:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveHumanDelayConfig"],
|
||||
dispatchReplyFromConfig:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyFromConfig"],
|
||||
withReplyDispatcher: vi.fn(async ({ dispatcher, run, onSettled }) => {
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
dispatcher.markComplete();
|
||||
try {
|
||||
await dispatcher.waitForIdle();
|
||||
} finally {
|
||||
await onSettled?.();
|
||||
}
|
||||
}
|
||||
}) as unknown as PluginRuntime["channel"]["reply"]["withReplyDispatcher"],
|
||||
finalizeInboundContext: vi.fn(
|
||||
(ctx: Record<string, unknown>) => ctx,
|
||||
) as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
|
||||
formatAgentEnvelope: vi.fn(
|
||||
(opts: { body: string }) => opts.body,
|
||||
) as unknown as PluginRuntime["channel"]["reply"]["formatAgentEnvelope"],
|
||||
formatInboundEnvelope: vi.fn(
|
||||
(opts: { body: string }) => opts.body,
|
||||
) as unknown as PluginRuntime["channel"]["reply"]["formatInboundEnvelope"],
|
||||
resolveEnvelopeFormatOptions: vi.fn(() => ({
|
||||
template: "channel+name+time",
|
||||
})) as unknown as PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"],
|
||||
},
|
||||
routing: {
|
||||
resolveAgentRoute: vi.fn(() => ({
|
||||
agentId: "main",
|
||||
accountId: "default",
|
||||
sessionKey: "agent:main:test:dm:peer",
|
||||
})) as unknown as PluginRuntime["channel"]["routing"]["resolveAgentRoute"],
|
||||
},
|
||||
pairing: {
|
||||
buildPairingReply: vi.fn(
|
||||
() => "Pairing code: TESTCODE",
|
||||
) as unknown as PluginRuntime["channel"]["pairing"]["buildPairingReply"],
|
||||
readAllowFromStore: vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
[],
|
||||
) as unknown as PluginRuntime["channel"]["pairing"]["readAllowFromStore"],
|
||||
upsertPairingRequest: vi.fn().mockResolvedValue({
|
||||
code: "TESTCODE",
|
||||
created: true,
|
||||
}) as unknown as PluginRuntime["channel"]["pairing"]["upsertPairingRequest"],
|
||||
},
|
||||
media: {
|
||||
fetchRemoteMedia:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"],
|
||||
saveMediaBuffer: vi.fn().mockResolvedValue({
|
||||
path: "/tmp/test-media.jpg",
|
||||
contentType: "image/jpeg",
|
||||
}) as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
|
||||
},
|
||||
session: {
|
||||
resolveStorePath: vi.fn(
|
||||
() => "/tmp/sessions.json",
|
||||
) as unknown as PluginRuntime["channel"]["session"]["resolveStorePath"],
|
||||
readSessionUpdatedAt: vi.fn(
|
||||
() => undefined,
|
||||
) as unknown as PluginRuntime["channel"]["session"]["readSessionUpdatedAt"],
|
||||
recordSessionMetaFromInbound:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordSessionMetaFromInbound"],
|
||||
recordInboundSession:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
|
||||
updateLastRoute:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["session"]["updateLastRoute"],
|
||||
},
|
||||
mentions: {
|
||||
buildMentionRegexes: vi.fn(() => [
|
||||
/\bbert\b/i,
|
||||
]) as unknown as PluginRuntime["channel"]["mentions"]["buildMentionRegexes"],
|
||||
matchesMentionPatterns: vi.fn((text: string, regexes: RegExp[]) =>
|
||||
regexes.some((regex) => regex.test(text)),
|
||||
) as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionPatterns"],
|
||||
matchesMentionWithExplicit: vi.fn(
|
||||
(params: { text: string; mentionRegexes: RegExp[]; explicitWasMentioned?: boolean }) =>
|
||||
params.explicitWasMentioned === true
|
||||
? true
|
||||
: params.mentionRegexes.some((regex) => regex.test(params.text)),
|
||||
) as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionWithExplicit"],
|
||||
},
|
||||
reactions: {
|
||||
shouldAckReaction,
|
||||
removeAckReactionAfterReply,
|
||||
},
|
||||
groups: {
|
||||
resolveGroupPolicy: vi.fn(
|
||||
() => "open",
|
||||
) as unknown as PluginRuntime["channel"]["groups"]["resolveGroupPolicy"],
|
||||
resolveRequireMention: vi.fn(
|
||||
() => false,
|
||||
) as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"],
|
||||
},
|
||||
debounce: {
|
||||
createInboundDebouncer: vi.fn(
|
||||
(params: { onFlush: (items: unknown[]) => Promise<void> }) => ({
|
||||
enqueue: async (item: unknown) => {
|
||||
await params.onFlush([item]);
|
||||
},
|
||||
flushKey: vi.fn(),
|
||||
}),
|
||||
) as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"],
|
||||
resolveInboundDebounceMs: vi.fn(
|
||||
() => 0,
|
||||
) as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"],
|
||||
},
|
||||
commands: {
|
||||
resolveCommandAuthorizedFromAuthorizers: vi.fn(
|
||||
() => false,
|
||||
) as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
|
||||
isControlCommandMessage:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["isControlCommandMessage"],
|
||||
shouldComputeCommandAuthorized:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldComputeCommandAuthorized"],
|
||||
shouldHandleTextCommands:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldHandleTextCommands"],
|
||||
},
|
||||
discord: {} as PluginRuntime["channel"]["discord"],
|
||||
activity: {} as PluginRuntime["channel"]["activity"],
|
||||
line: {} as PluginRuntime["channel"]["line"],
|
||||
slack: {} as PluginRuntime["channel"]["slack"],
|
||||
telegram: {} as PluginRuntime["channel"]["telegram"],
|
||||
signal: {} as PluginRuntime["channel"]["signal"],
|
||||
imessage: {} as PluginRuntime["channel"]["imessage"],
|
||||
whatsapp: {} as PluginRuntime["channel"]["whatsapp"],
|
||||
},
|
||||
events: {
|
||||
onAgentEvent: vi.fn(() => () => {}) as unknown as PluginRuntime["events"]["onAgentEvent"],
|
||||
onSessionTranscriptUpdate: vi.fn(
|
||||
() => () => {},
|
||||
) as unknown as PluginRuntime["events"]["onSessionTranscriptUpdate"],
|
||||
},
|
||||
logging: {
|
||||
shouldLogVerbose: vi.fn(() => false),
|
||||
getChildLogger: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
})),
|
||||
},
|
||||
state: {
|
||||
resolveStateDir: vi.fn(() => "/tmp/openclaw"),
|
||||
},
|
||||
};
|
||||
|
||||
return mergeDeep(base, overrides);
|
||||
}
|
||||
@@ -67,7 +67,8 @@ describe("session hook context wiring", () => {
|
||||
await vi.waitFor(() => expect(hookRunnerMocks.runSessionStart).toHaveBeenCalledTimes(1));
|
||||
const [event, context] = hookRunnerMocks.runSessionStart.mock.calls[0] ?? [];
|
||||
expect(event).toMatchObject({ sessionKey });
|
||||
expect(context).toMatchObject({ sessionKey });
|
||||
expect(context).toMatchObject({ sessionKey, agentId: "main" });
|
||||
expect(context).toMatchObject({ sessionId: event?.sessionId });
|
||||
});
|
||||
|
||||
it("passes sessionKey to session_end hook context on reset", async () => {
|
||||
@@ -88,8 +89,13 @@ describe("session hook context wiring", () => {
|
||||
});
|
||||
|
||||
await vi.waitFor(() => expect(hookRunnerMocks.runSessionEnd).toHaveBeenCalledTimes(1));
|
||||
await vi.waitFor(() => expect(hookRunnerMocks.runSessionStart).toHaveBeenCalledTimes(1));
|
||||
const [event, context] = hookRunnerMocks.runSessionEnd.mock.calls[0] ?? [];
|
||||
expect(event).toMatchObject({ sessionKey });
|
||||
expect(context).toMatchObject({ sessionKey });
|
||||
expect(context).toMatchObject({ sessionKey, agentId: "main" });
|
||||
expect(context).toMatchObject({ sessionId: event?.sessionId });
|
||||
|
||||
const [startEvent] = hookRunnerMocks.runSessionStart.mock.calls[0] ?? [];
|
||||
expect(startEvent).toMatchObject({ resumedFrom: "old-session" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,6 +146,70 @@ type LegacyMainDeliveryRetirement = {
|
||||
entry: SessionEntry;
|
||||
};
|
||||
|
||||
type SessionHookContext = {
|
||||
sessionId: string;
|
||||
sessionKey: string;
|
||||
agentId: string;
|
||||
};
|
||||
|
||||
function buildSessionHookContext(params: {
|
||||
sessionId: string;
|
||||
sessionKey: string;
|
||||
cfg: OpenClawConfig;
|
||||
}): SessionHookContext {
|
||||
return {
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
agentId: resolveSessionAgentId({ sessionKey: params.sessionKey, config: params.cfg }),
|
||||
};
|
||||
}
|
||||
|
||||
function buildSessionStartHookPayload(params: {
|
||||
sessionId: string;
|
||||
sessionKey: string;
|
||||
cfg: OpenClawConfig;
|
||||
resumedFrom?: string;
|
||||
}): {
|
||||
event: { sessionId: string; sessionKey: string; resumedFrom?: string };
|
||||
context: SessionHookContext;
|
||||
} {
|
||||
return {
|
||||
event: {
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
resumedFrom: params.resumedFrom,
|
||||
},
|
||||
context: buildSessionHookContext({
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
cfg: params.cfg,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildSessionEndHookPayload(params: {
|
||||
sessionId: string;
|
||||
sessionKey: string;
|
||||
cfg: OpenClawConfig;
|
||||
messageCount?: number;
|
||||
}): {
|
||||
event: { sessionId: string; sessionKey: string; messageCount: number };
|
||||
context: SessionHookContext;
|
||||
} {
|
||||
return {
|
||||
event: {
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
messageCount: params.messageCount ?? 0,
|
||||
},
|
||||
context: buildSessionHookContext({
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
cfg: params.cfg,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveParentForkMaxTokens(cfg: OpenClawConfig): number {
|
||||
const configured = cfg.session?.parentForkMaxTokens;
|
||||
if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
|
||||
@@ -643,39 +707,24 @@ export async function initSessionState(params: {
|
||||
// If replacing an existing session, fire session_end for the old one
|
||||
if (previousSessionEntry?.sessionId && previousSessionEntry.sessionId !== effectiveSessionId) {
|
||||
if (hookRunner.hasHooks("session_end")) {
|
||||
void hookRunner
|
||||
.runSessionEnd(
|
||||
{
|
||||
sessionId: previousSessionEntry.sessionId,
|
||||
sessionKey,
|
||||
messageCount: 0,
|
||||
},
|
||||
{
|
||||
sessionId: previousSessionEntry.sessionId,
|
||||
sessionKey,
|
||||
agentId: resolveSessionAgentId({ sessionKey, config: cfg }),
|
||||
},
|
||||
)
|
||||
.catch(() => {});
|
||||
const payload = buildSessionEndHookPayload({
|
||||
sessionId: previousSessionEntry.sessionId,
|
||||
sessionKey,
|
||||
cfg,
|
||||
});
|
||||
void hookRunner.runSessionEnd(payload.event, payload.context).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Fire session_start for the new session
|
||||
if (hookRunner.hasHooks("session_start")) {
|
||||
void hookRunner
|
||||
.runSessionStart(
|
||||
{
|
||||
sessionId: effectiveSessionId,
|
||||
sessionKey,
|
||||
resumedFrom: previousSessionEntry?.sessionId,
|
||||
},
|
||||
{
|
||||
sessionId: effectiveSessionId,
|
||||
sessionKey,
|
||||
agentId: resolveSessionAgentId({ sessionKey, config: cfg }),
|
||||
},
|
||||
)
|
||||
.catch(() => {});
|
||||
const payload = buildSessionStartHookPayload({
|
||||
sessionId: effectiveSessionId,
|
||||
sessionKey,
|
||||
cfg,
|
||||
resumedFrom: previousSessionEntry?.sessionId,
|
||||
});
|
||||
void hookRunner.runSessionStart(payload.event, payload.context).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,148 +1,14 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js";
|
||||
import { createMemoryGetTool, createMemorySearchTool } from "../../agents/tools/memory-tool.js";
|
||||
import { handleSlackAction } from "../../agents/tools/slack-actions.js";
|
||||
import {
|
||||
chunkByNewline,
|
||||
chunkMarkdownText,
|
||||
chunkMarkdownTextWithMode,
|
||||
chunkText,
|
||||
chunkTextWithMode,
|
||||
resolveChunkMode,
|
||||
resolveTextChunkLimit,
|
||||
} from "../../auto-reply/chunk.js";
|
||||
import {
|
||||
hasControlCommand,
|
||||
isControlCommandMessage,
|
||||
shouldComputeCommandAuthorized,
|
||||
} from "../../auto-reply/command-detection.js";
|
||||
import { shouldHandleTextCommands } from "../../auto-reply/commands-registry.js";
|
||||
import { withReplyDispatcher } from "../../auto-reply/dispatch.js";
|
||||
import {
|
||||
formatAgentEnvelope,
|
||||
formatInboundEnvelope,
|
||||
resolveEnvelopeFormatOptions,
|
||||
} from "../../auto-reply/envelope.js";
|
||||
import {
|
||||
createInboundDebouncer,
|
||||
resolveInboundDebounceMs,
|
||||
} from "../../auto-reply/inbound-debounce.js";
|
||||
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
|
||||
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
||||
import {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
matchesMentionWithExplicit,
|
||||
} from "../../auto-reply/reply/mentions.js";
|
||||
import { dispatchReplyWithBufferedBlockDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
|
||||
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||
import { removeAckReactionAfterReply, shouldAckReaction } from "../../channels/ack-reactions.js";
|
||||
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
|
||||
import { discordMessageActions } from "../../channels/plugins/actions/discord.js";
|
||||
import { signalMessageActions } from "../../channels/plugins/actions/signal.js";
|
||||
import { telegramMessageActions } from "../../channels/plugins/actions/telegram.js";
|
||||
import { createWhatsAppLoginTool } from "../../channels/plugins/agent-tools/whatsapp-login.js";
|
||||
import { recordInboundSession } from "../../channels/session.js";
|
||||
import { registerMemoryCli } from "../../cli/memory-cli.js";
|
||||
import { loadConfig, writeConfigFile } from "../../config/config.js";
|
||||
import {
|
||||
resolveChannelGroupPolicy,
|
||||
resolveChannelGroupRequireMention,
|
||||
} from "../../config/group-policy.js";
|
||||
import { resolveMarkdownTableMode } from "../../config/markdown-tables.js";
|
||||
import { resolveStateDir } from "../../config/paths.js";
|
||||
import {
|
||||
readSessionUpdatedAt,
|
||||
recordSessionMetaFromInbound,
|
||||
resolveStorePath,
|
||||
updateLastRoute,
|
||||
} from "../../config/sessions.js";
|
||||
import { auditDiscordChannelPermissions } from "../../discord/audit.js";
|
||||
import {
|
||||
listDiscordDirectoryGroupsLive,
|
||||
listDiscordDirectoryPeersLive,
|
||||
} from "../../discord/directory-live.js";
|
||||
import { monitorDiscordProvider } from "../../discord/monitor.js";
|
||||
import { probeDiscord } from "../../discord/probe.js";
|
||||
import { resolveDiscordChannelAllowlist } from "../../discord/resolve-channels.js";
|
||||
import { resolveDiscordUserAllowlist } from "../../discord/resolve-users.js";
|
||||
import { sendMessageDiscord, sendPollDiscord } from "../../discord/send.js";
|
||||
import { shouldLogVerbose } from "../../globals.js";
|
||||
import { monitorIMessageProvider } from "../../imessage/monitor.js";
|
||||
import { probeIMessage } from "../../imessage/probe.js";
|
||||
import { sendMessageIMessage } from "../../imessage/send.js";
|
||||
import { onAgentEvent } from "../../infra/agent-events.js";
|
||||
import { getChannelActivity, recordChannelActivity } from "../../infra/channel-activity.js";
|
||||
import { requestHeartbeatNow } from "../../infra/heartbeat-wake.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import {
|
||||
listLineAccountIds,
|
||||
normalizeAccountId as normalizeLineAccountId,
|
||||
resolveDefaultLineAccountId,
|
||||
resolveLineAccount,
|
||||
} from "../../line/accounts.js";
|
||||
import { monitorLineProvider } from "../../line/monitor.js";
|
||||
import { probeLineBot } from "../../line/probe.js";
|
||||
import {
|
||||
createQuickReplyItems,
|
||||
pushMessageLine,
|
||||
pushMessagesLine,
|
||||
pushFlexMessage,
|
||||
pushTemplateMessage,
|
||||
pushLocationMessage,
|
||||
pushTextMessageWithQuickReplies,
|
||||
sendMessageLine,
|
||||
} from "../../line/send.js";
|
||||
import { buildTemplateMessageFromPayload } from "../../line/template-messages.js";
|
||||
import { getChildLogger } from "../../logging.js";
|
||||
import { normalizeLogLevel } from "../../logging/levels.js";
|
||||
import { convertMarkdownTables } from "../../markdown/tables.js";
|
||||
import { transcribeAudioFile } from "../../media-understanding/transcribe-audio.js";
|
||||
import { isVoiceCompatibleAudio } from "../../media/audio.js";
|
||||
import { mediaKindFromMime } from "../../media/constants.js";
|
||||
import { fetchRemoteMedia } from "../../media/fetch.js";
|
||||
import { getImageMetadata, resizeToJpeg } from "../../media/image-ops.js";
|
||||
import { detectMime } from "../../media/mime.js";
|
||||
import { saveMediaBuffer } from "../../media/store.js";
|
||||
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
||||
import {
|
||||
readChannelAllowFromStore,
|
||||
upsertChannelPairingRequest,
|
||||
} from "../../pairing/pairing-store.js";
|
||||
import { runCommandWithTimeout } from "../../process/exec.js";
|
||||
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||
import { onSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
|
||||
import { monitorSignalProvider } from "../../signal/index.js";
|
||||
import { probeSignal } from "../../signal/probe.js";
|
||||
import { sendMessageSignal } from "../../signal/send.js";
|
||||
import {
|
||||
listSlackDirectoryGroupsLive,
|
||||
listSlackDirectoryPeersLive,
|
||||
} from "../../slack/directory-live.js";
|
||||
import { monitorSlackProvider } from "../../slack/index.js";
|
||||
import { probeSlack } from "../../slack/probe.js";
|
||||
import { resolveSlackChannelAllowlist } from "../../slack/resolve-channels.js";
|
||||
import { resolveSlackUserAllowlist } from "../../slack/resolve-users.js";
|
||||
import { sendMessageSlack } from "../../slack/send.js";
|
||||
import {
|
||||
auditTelegramGroupMembership,
|
||||
collectTelegramUnmentionedGroupIds,
|
||||
} from "../../telegram/audit.js";
|
||||
import { monitorTelegramProvider } from "../../telegram/monitor.js";
|
||||
import { probeTelegram } from "../../telegram/probe.js";
|
||||
import { sendMessageTelegram, sendPollTelegram } from "../../telegram/send.js";
|
||||
import { resolveTelegramToken } from "../../telegram/token.js";
|
||||
import { textToSpeechTelephony } from "../../tts/tts.js";
|
||||
import { getActiveWebListener } from "../../web/active-listener.js";
|
||||
import {
|
||||
getWebAuthAgeMs,
|
||||
logoutWeb,
|
||||
logWebSelfId,
|
||||
readWebSelfId,
|
||||
webAuthExists,
|
||||
} from "../../web/auth-store.js";
|
||||
import { loadWebMedia } from "../../web/media.js";
|
||||
import { formatNativeDependencyHint } from "./native-deps.js";
|
||||
import { createRuntimeChannel } from "./runtime-channel.js";
|
||||
import { createRuntimeConfig } from "./runtime-config.js";
|
||||
import { createRuntimeEvents } from "./runtime-events.js";
|
||||
import { createRuntimeLogging } from "./runtime-logging.js";
|
||||
import { createRuntimeMedia } from "./runtime-media.js";
|
||||
import { createRuntimeSystem } from "./runtime-system.js";
|
||||
import { createRuntimeTools } from "./runtime-tools.js";
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
let cachedVersion: string | null = null;
|
||||
@@ -162,87 +28,8 @@ function resolveVersion(): string {
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessageWhatsAppLazy: PluginRuntime["channel"]["whatsapp"]["sendMessageWhatsApp"] = async (
|
||||
...args
|
||||
) => {
|
||||
const { sendMessageWhatsApp } = await loadWebOutbound();
|
||||
return sendMessageWhatsApp(...args);
|
||||
};
|
||||
|
||||
const sendPollWhatsAppLazy: PluginRuntime["channel"]["whatsapp"]["sendPollWhatsApp"] = async (
|
||||
...args
|
||||
) => {
|
||||
const { sendPollWhatsApp } = await loadWebOutbound();
|
||||
return sendPollWhatsApp(...args);
|
||||
};
|
||||
|
||||
const loginWebLazy: PluginRuntime["channel"]["whatsapp"]["loginWeb"] = async (...args) => {
|
||||
const { loginWeb } = await loadWebLogin();
|
||||
return loginWeb(...args);
|
||||
};
|
||||
|
||||
const startWebLoginWithQrLazy: PluginRuntime["channel"]["whatsapp"]["startWebLoginWithQr"] = async (
|
||||
...args
|
||||
) => {
|
||||
const { startWebLoginWithQr } = await loadWebLoginQr();
|
||||
return startWebLoginWithQr(...args);
|
||||
};
|
||||
|
||||
const waitForWebLoginLazy: PluginRuntime["channel"]["whatsapp"]["waitForWebLogin"] = async (
|
||||
...args
|
||||
) => {
|
||||
const { waitForWebLogin } = await loadWebLoginQr();
|
||||
return waitForWebLogin(...args);
|
||||
};
|
||||
|
||||
const monitorWebChannelLazy: PluginRuntime["channel"]["whatsapp"]["monitorWebChannel"] = async (
|
||||
...args
|
||||
) => {
|
||||
const { monitorWebChannel } = await loadWebChannel();
|
||||
return monitorWebChannel(...args);
|
||||
};
|
||||
|
||||
const handleWhatsAppActionLazy: PluginRuntime["channel"]["whatsapp"]["handleWhatsAppAction"] =
|
||||
async (...args) => {
|
||||
const { handleWhatsAppAction } = await loadWhatsAppActions();
|
||||
return handleWhatsAppAction(...args);
|
||||
};
|
||||
|
||||
let webOutboundPromise: Promise<typeof import("../../web/outbound.js")> | null = null;
|
||||
let webLoginPromise: Promise<typeof import("../../web/login.js")> | null = null;
|
||||
let webLoginQrPromise: Promise<typeof import("../../web/login-qr.js")> | null = null;
|
||||
let webChannelPromise: Promise<typeof import("../../channels/web/index.js")> | null = null;
|
||||
let whatsappActionsPromise: Promise<
|
||||
typeof import("../../agents/tools/whatsapp-actions.js")
|
||||
> | null = null;
|
||||
|
||||
function loadWebOutbound() {
|
||||
webOutboundPromise ??= import("../../web/outbound.js");
|
||||
return webOutboundPromise;
|
||||
}
|
||||
|
||||
function loadWebLogin() {
|
||||
webLoginPromise ??= import("../../web/login.js");
|
||||
return webLoginPromise;
|
||||
}
|
||||
|
||||
function loadWebLoginQr() {
|
||||
webLoginQrPromise ??= import("../../web/login-qr.js");
|
||||
return webLoginQrPromise;
|
||||
}
|
||||
|
||||
function loadWebChannel() {
|
||||
webChannelPromise ??= import("../../channels/web/index.js");
|
||||
return webChannelPromise;
|
||||
}
|
||||
|
||||
function loadWhatsAppActions() {
|
||||
whatsappActionsPromise ??= import("../../agents/tools/whatsapp-actions.js");
|
||||
return whatsappActionsPromise;
|
||||
}
|
||||
|
||||
export function createPluginRuntime(): PluginRuntime {
|
||||
return {
|
||||
const runtime = {
|
||||
version: resolveVersion(),
|
||||
config: createRuntimeConfig(),
|
||||
system: createRuntimeSystem(),
|
||||
@@ -251,226 +38,12 @@ export function createPluginRuntime(): PluginRuntime {
|
||||
stt: { transcribeAudioFile },
|
||||
tools: createRuntimeTools(),
|
||||
channel: createRuntimeChannel(),
|
||||
events: {
|
||||
onAgentEvent,
|
||||
onSessionTranscriptUpdate,
|
||||
},
|
||||
events: createRuntimeEvents(),
|
||||
logging: createRuntimeLogging(),
|
||||
state: { resolveStateDir },
|
||||
};
|
||||
}
|
||||
} satisfies PluginRuntime;
|
||||
|
||||
function createRuntimeConfig(): PluginRuntime["config"] {
|
||||
return {
|
||||
loadConfig,
|
||||
writeConfigFile,
|
||||
};
|
||||
}
|
||||
|
||||
function createRuntimeSystem(): PluginRuntime["system"] {
|
||||
return {
|
||||
enqueueSystemEvent,
|
||||
requestHeartbeatNow,
|
||||
runCommandWithTimeout,
|
||||
formatNativeDependencyHint,
|
||||
};
|
||||
}
|
||||
|
||||
function createRuntimeMedia(): PluginRuntime["media"] {
|
||||
return {
|
||||
loadWebMedia,
|
||||
detectMime,
|
||||
mediaKindFromMime,
|
||||
isVoiceCompatibleAudio,
|
||||
getImageMetadata,
|
||||
resizeToJpeg,
|
||||
};
|
||||
}
|
||||
|
||||
function createRuntimeTools(): PluginRuntime["tools"] {
|
||||
return {
|
||||
createMemoryGetTool,
|
||||
createMemorySearchTool,
|
||||
registerMemoryCli,
|
||||
};
|
||||
}
|
||||
|
||||
function createRuntimeChannel(): PluginRuntime["channel"] {
|
||||
return {
|
||||
text: {
|
||||
chunkByNewline,
|
||||
chunkMarkdownText,
|
||||
chunkMarkdownTextWithMode,
|
||||
chunkText,
|
||||
chunkTextWithMode,
|
||||
resolveChunkMode,
|
||||
resolveTextChunkLimit,
|
||||
hasControlCommand,
|
||||
resolveMarkdownTableMode,
|
||||
convertMarkdownTables,
|
||||
},
|
||||
reply: {
|
||||
dispatchReplyWithBufferedBlockDispatcher,
|
||||
createReplyDispatcherWithTyping,
|
||||
resolveEffectiveMessagesConfig,
|
||||
resolveHumanDelayConfig,
|
||||
dispatchReplyFromConfig,
|
||||
withReplyDispatcher,
|
||||
finalizeInboundContext,
|
||||
formatAgentEnvelope,
|
||||
/** @deprecated Prefer `BodyForAgent` + structured user-context blocks (do not build plaintext envelopes for prompts). */
|
||||
formatInboundEnvelope,
|
||||
resolveEnvelopeFormatOptions,
|
||||
},
|
||||
routing: {
|
||||
resolveAgentRoute,
|
||||
},
|
||||
pairing: {
|
||||
buildPairingReply,
|
||||
readAllowFromStore: ({ channel, accountId, env }) =>
|
||||
readChannelAllowFromStore(channel, env, accountId),
|
||||
upsertPairingRequest: ({ channel, id, accountId, meta, env, pairingAdapter }) =>
|
||||
upsertChannelPairingRequest({
|
||||
channel,
|
||||
id,
|
||||
accountId,
|
||||
meta,
|
||||
env,
|
||||
pairingAdapter,
|
||||
}),
|
||||
},
|
||||
media: {
|
||||
fetchRemoteMedia,
|
||||
saveMediaBuffer,
|
||||
},
|
||||
activity: {
|
||||
record: recordChannelActivity,
|
||||
get: getChannelActivity,
|
||||
},
|
||||
session: {
|
||||
resolveStorePath,
|
||||
readSessionUpdatedAt,
|
||||
recordSessionMetaFromInbound,
|
||||
recordInboundSession,
|
||||
updateLastRoute,
|
||||
},
|
||||
mentions: {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
matchesMentionWithExplicit,
|
||||
},
|
||||
reactions: {
|
||||
shouldAckReaction,
|
||||
removeAckReactionAfterReply,
|
||||
},
|
||||
groups: {
|
||||
resolveGroupPolicy: resolveChannelGroupPolicy,
|
||||
resolveRequireMention: resolveChannelGroupRequireMention,
|
||||
},
|
||||
debounce: {
|
||||
createInboundDebouncer,
|
||||
resolveInboundDebounceMs,
|
||||
},
|
||||
commands: {
|
||||
resolveCommandAuthorizedFromAuthorizers,
|
||||
isControlCommandMessage,
|
||||
shouldComputeCommandAuthorized,
|
||||
shouldHandleTextCommands,
|
||||
},
|
||||
discord: {
|
||||
messageActions: discordMessageActions,
|
||||
auditChannelPermissions: auditDiscordChannelPermissions,
|
||||
listDirectoryGroupsLive: listDiscordDirectoryGroupsLive,
|
||||
listDirectoryPeersLive: listDiscordDirectoryPeersLive,
|
||||
probeDiscord,
|
||||
resolveChannelAllowlist: resolveDiscordChannelAllowlist,
|
||||
resolveUserAllowlist: resolveDiscordUserAllowlist,
|
||||
sendMessageDiscord,
|
||||
sendPollDiscord,
|
||||
monitorDiscordProvider,
|
||||
},
|
||||
slack: {
|
||||
listDirectoryGroupsLive: listSlackDirectoryGroupsLive,
|
||||
listDirectoryPeersLive: listSlackDirectoryPeersLive,
|
||||
probeSlack,
|
||||
resolveChannelAllowlist: resolveSlackChannelAllowlist,
|
||||
resolveUserAllowlist: resolveSlackUserAllowlist,
|
||||
sendMessageSlack,
|
||||
monitorSlackProvider,
|
||||
handleSlackAction,
|
||||
},
|
||||
telegram: {
|
||||
auditGroupMembership: auditTelegramGroupMembership,
|
||||
collectUnmentionedGroupIds: collectTelegramUnmentionedGroupIds,
|
||||
probeTelegram,
|
||||
resolveTelegramToken,
|
||||
sendMessageTelegram,
|
||||
sendPollTelegram,
|
||||
monitorTelegramProvider,
|
||||
messageActions: telegramMessageActions,
|
||||
},
|
||||
signal: {
|
||||
probeSignal,
|
||||
sendMessageSignal,
|
||||
monitorSignalProvider,
|
||||
messageActions: signalMessageActions,
|
||||
},
|
||||
imessage: {
|
||||
monitorIMessageProvider,
|
||||
probeIMessage,
|
||||
sendMessageIMessage,
|
||||
},
|
||||
whatsapp: {
|
||||
getActiveWebListener,
|
||||
getWebAuthAgeMs,
|
||||
logoutWeb,
|
||||
logWebSelfId,
|
||||
readWebSelfId,
|
||||
webAuthExists,
|
||||
sendMessageWhatsApp: sendMessageWhatsAppLazy,
|
||||
sendPollWhatsApp: sendPollWhatsAppLazy,
|
||||
loginWeb: loginWebLazy,
|
||||
startWebLoginWithQr: startWebLoginWithQrLazy,
|
||||
waitForWebLogin: waitForWebLoginLazy,
|
||||
monitorWebChannel: monitorWebChannelLazy,
|
||||
handleWhatsAppAction: handleWhatsAppActionLazy,
|
||||
createLoginTool: createWhatsAppLoginTool,
|
||||
},
|
||||
line: {
|
||||
listLineAccountIds,
|
||||
resolveDefaultLineAccountId,
|
||||
resolveLineAccount,
|
||||
normalizeAccountId: normalizeLineAccountId,
|
||||
probeLineBot,
|
||||
sendMessageLine,
|
||||
pushMessageLine,
|
||||
pushMessagesLine,
|
||||
pushFlexMessage,
|
||||
pushTemplateMessage,
|
||||
pushLocationMessage,
|
||||
pushTextMessageWithQuickReplies,
|
||||
createQuickReplyItems,
|
||||
buildTemplateMessageFromPayload,
|
||||
monitorLineProvider,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createRuntimeLogging(): PluginRuntime["logging"] {
|
||||
return {
|
||||
shouldLogVerbose,
|
||||
getChildLogger: (bindings, opts) => {
|
||||
const logger = getChildLogger(bindings, {
|
||||
level: opts?.level ? normalizeLogLevel(opts.level) : undefined,
|
||||
});
|
||||
return {
|
||||
debug: (message) => logger.debug?.(message),
|
||||
info: (message) => logger.info(message),
|
||||
warn: (message) => logger.warn(message),
|
||||
error: (message) => logger.error(message),
|
||||
};
|
||||
},
|
||||
};
|
||||
return runtime;
|
||||
}
|
||||
|
||||
export type { PluginRuntime } from "./types.js";
|
||||
|
||||
263
src/plugins/runtime/runtime-channel.ts
Normal file
263
src/plugins/runtime/runtime-channel.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js";
|
||||
import { handleSlackAction } from "../../agents/tools/slack-actions.js";
|
||||
import {
|
||||
chunkByNewline,
|
||||
chunkMarkdownText,
|
||||
chunkMarkdownTextWithMode,
|
||||
chunkText,
|
||||
chunkTextWithMode,
|
||||
resolveChunkMode,
|
||||
resolveTextChunkLimit,
|
||||
} from "../../auto-reply/chunk.js";
|
||||
import {
|
||||
hasControlCommand,
|
||||
isControlCommandMessage,
|
||||
shouldComputeCommandAuthorized,
|
||||
} from "../../auto-reply/command-detection.js";
|
||||
import { shouldHandleTextCommands } from "../../auto-reply/commands-registry.js";
|
||||
import { withReplyDispatcher } from "../../auto-reply/dispatch.js";
|
||||
import {
|
||||
formatAgentEnvelope,
|
||||
formatInboundEnvelope,
|
||||
resolveEnvelopeFormatOptions,
|
||||
} from "../../auto-reply/envelope.js";
|
||||
import {
|
||||
createInboundDebouncer,
|
||||
resolveInboundDebounceMs,
|
||||
} from "../../auto-reply/inbound-debounce.js";
|
||||
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
|
||||
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
||||
import {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
matchesMentionWithExplicit,
|
||||
} from "../../auto-reply/reply/mentions.js";
|
||||
import { dispatchReplyWithBufferedBlockDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
|
||||
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||
import { removeAckReactionAfterReply, shouldAckReaction } from "../../channels/ack-reactions.js";
|
||||
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
|
||||
import { discordMessageActions } from "../../channels/plugins/actions/discord.js";
|
||||
import { signalMessageActions } from "../../channels/plugins/actions/signal.js";
|
||||
import { telegramMessageActions } from "../../channels/plugins/actions/telegram.js";
|
||||
import { recordInboundSession } from "../../channels/session.js";
|
||||
import {
|
||||
resolveChannelGroupPolicy,
|
||||
resolveChannelGroupRequireMention,
|
||||
} from "../../config/group-policy.js";
|
||||
import { resolveMarkdownTableMode } from "../../config/markdown-tables.js";
|
||||
import {
|
||||
readSessionUpdatedAt,
|
||||
recordSessionMetaFromInbound,
|
||||
resolveStorePath,
|
||||
updateLastRoute,
|
||||
} from "../../config/sessions.js";
|
||||
import { auditDiscordChannelPermissions } from "../../discord/audit.js";
|
||||
import {
|
||||
listDiscordDirectoryGroupsLive,
|
||||
listDiscordDirectoryPeersLive,
|
||||
} from "../../discord/directory-live.js";
|
||||
import { monitorDiscordProvider } from "../../discord/monitor.js";
|
||||
import { probeDiscord } from "../../discord/probe.js";
|
||||
import { resolveDiscordChannelAllowlist } from "../../discord/resolve-channels.js";
|
||||
import { resolveDiscordUserAllowlist } from "../../discord/resolve-users.js";
|
||||
import { sendMessageDiscord, sendPollDiscord } from "../../discord/send.js";
|
||||
import { monitorIMessageProvider } from "../../imessage/monitor.js";
|
||||
import { probeIMessage } from "../../imessage/probe.js";
|
||||
import { sendMessageIMessage } from "../../imessage/send.js";
|
||||
import { getChannelActivity, recordChannelActivity } from "../../infra/channel-activity.js";
|
||||
import {
|
||||
listLineAccountIds,
|
||||
normalizeAccountId as normalizeLineAccountId,
|
||||
resolveDefaultLineAccountId,
|
||||
resolveLineAccount,
|
||||
} from "../../line/accounts.js";
|
||||
import { monitorLineProvider } from "../../line/monitor.js";
|
||||
import { probeLineBot } from "../../line/probe.js";
|
||||
import {
|
||||
createQuickReplyItems,
|
||||
pushFlexMessage,
|
||||
pushLocationMessage,
|
||||
pushMessageLine,
|
||||
pushMessagesLine,
|
||||
pushTemplateMessage,
|
||||
pushTextMessageWithQuickReplies,
|
||||
sendMessageLine,
|
||||
} from "../../line/send.js";
|
||||
import { buildTemplateMessageFromPayload } from "../../line/template-messages.js";
|
||||
import { convertMarkdownTables } from "../../markdown/tables.js";
|
||||
import { fetchRemoteMedia } from "../../media/fetch.js";
|
||||
import { saveMediaBuffer } from "../../media/store.js";
|
||||
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
||||
import {
|
||||
readChannelAllowFromStore,
|
||||
upsertChannelPairingRequest,
|
||||
} from "../../pairing/pairing-store.js";
|
||||
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||
import { monitorSignalProvider } from "../../signal/index.js";
|
||||
import { probeSignal } from "../../signal/probe.js";
|
||||
import { sendMessageSignal } from "../../signal/send.js";
|
||||
import {
|
||||
listSlackDirectoryGroupsLive,
|
||||
listSlackDirectoryPeersLive,
|
||||
} from "../../slack/directory-live.js";
|
||||
import { monitorSlackProvider } from "../../slack/index.js";
|
||||
import { probeSlack } from "../../slack/probe.js";
|
||||
import { resolveSlackChannelAllowlist } from "../../slack/resolve-channels.js";
|
||||
import { resolveSlackUserAllowlist } from "../../slack/resolve-users.js";
|
||||
import { sendMessageSlack } from "../../slack/send.js";
|
||||
import {
|
||||
auditTelegramGroupMembership,
|
||||
collectTelegramUnmentionedGroupIds,
|
||||
} from "../../telegram/audit.js";
|
||||
import { monitorTelegramProvider } from "../../telegram/monitor.js";
|
||||
import { probeTelegram } from "../../telegram/probe.js";
|
||||
import { sendMessageTelegram, sendPollTelegram } from "../../telegram/send.js";
|
||||
import { resolveTelegramToken } from "../../telegram/token.js";
|
||||
import { createRuntimeWhatsApp } from "./runtime-whatsapp.js";
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
export function createRuntimeChannel(): PluginRuntime["channel"] {
|
||||
return {
|
||||
text: {
|
||||
chunkByNewline,
|
||||
chunkMarkdownText,
|
||||
chunkMarkdownTextWithMode,
|
||||
chunkText,
|
||||
chunkTextWithMode,
|
||||
resolveChunkMode,
|
||||
resolveTextChunkLimit,
|
||||
hasControlCommand,
|
||||
resolveMarkdownTableMode,
|
||||
convertMarkdownTables,
|
||||
},
|
||||
reply: {
|
||||
dispatchReplyWithBufferedBlockDispatcher,
|
||||
createReplyDispatcherWithTyping,
|
||||
resolveEffectiveMessagesConfig,
|
||||
resolveHumanDelayConfig,
|
||||
dispatchReplyFromConfig,
|
||||
withReplyDispatcher,
|
||||
finalizeInboundContext,
|
||||
formatAgentEnvelope,
|
||||
/** @deprecated Prefer `BodyForAgent` + structured user-context blocks (do not build plaintext envelopes for prompts). */
|
||||
formatInboundEnvelope,
|
||||
resolveEnvelopeFormatOptions,
|
||||
},
|
||||
routing: {
|
||||
resolveAgentRoute,
|
||||
},
|
||||
pairing: {
|
||||
buildPairingReply,
|
||||
readAllowFromStore: ({ channel, accountId, env }) =>
|
||||
readChannelAllowFromStore(channel, env, accountId),
|
||||
upsertPairingRequest: ({ channel, id, accountId, meta, env, pairingAdapter }) =>
|
||||
upsertChannelPairingRequest({
|
||||
channel,
|
||||
id,
|
||||
accountId,
|
||||
meta,
|
||||
env,
|
||||
pairingAdapter,
|
||||
}),
|
||||
},
|
||||
media: {
|
||||
fetchRemoteMedia,
|
||||
saveMediaBuffer,
|
||||
},
|
||||
activity: {
|
||||
record: recordChannelActivity,
|
||||
get: getChannelActivity,
|
||||
},
|
||||
session: {
|
||||
resolveStorePath,
|
||||
readSessionUpdatedAt,
|
||||
recordSessionMetaFromInbound,
|
||||
recordInboundSession,
|
||||
updateLastRoute,
|
||||
},
|
||||
mentions: {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
matchesMentionWithExplicit,
|
||||
},
|
||||
reactions: {
|
||||
shouldAckReaction,
|
||||
removeAckReactionAfterReply,
|
||||
},
|
||||
groups: {
|
||||
resolveGroupPolicy: resolveChannelGroupPolicy,
|
||||
resolveRequireMention: resolveChannelGroupRequireMention,
|
||||
},
|
||||
debounce: {
|
||||
createInboundDebouncer,
|
||||
resolveInboundDebounceMs,
|
||||
},
|
||||
commands: {
|
||||
resolveCommandAuthorizedFromAuthorizers,
|
||||
isControlCommandMessage,
|
||||
shouldComputeCommandAuthorized,
|
||||
shouldHandleTextCommands,
|
||||
},
|
||||
discord: {
|
||||
messageActions: discordMessageActions,
|
||||
auditChannelPermissions: auditDiscordChannelPermissions,
|
||||
listDirectoryGroupsLive: listDiscordDirectoryGroupsLive,
|
||||
listDirectoryPeersLive: listDiscordDirectoryPeersLive,
|
||||
probeDiscord,
|
||||
resolveChannelAllowlist: resolveDiscordChannelAllowlist,
|
||||
resolveUserAllowlist: resolveDiscordUserAllowlist,
|
||||
sendMessageDiscord,
|
||||
sendPollDiscord,
|
||||
monitorDiscordProvider,
|
||||
},
|
||||
slack: {
|
||||
listDirectoryGroupsLive: listSlackDirectoryGroupsLive,
|
||||
listDirectoryPeersLive: listSlackDirectoryPeersLive,
|
||||
probeSlack,
|
||||
resolveChannelAllowlist: resolveSlackChannelAllowlist,
|
||||
resolveUserAllowlist: resolveSlackUserAllowlist,
|
||||
sendMessageSlack,
|
||||
monitorSlackProvider,
|
||||
handleSlackAction,
|
||||
},
|
||||
telegram: {
|
||||
auditGroupMembership: auditTelegramGroupMembership,
|
||||
collectUnmentionedGroupIds: collectTelegramUnmentionedGroupIds,
|
||||
probeTelegram,
|
||||
resolveTelegramToken,
|
||||
sendMessageTelegram,
|
||||
sendPollTelegram,
|
||||
monitorTelegramProvider,
|
||||
messageActions: telegramMessageActions,
|
||||
},
|
||||
signal: {
|
||||
probeSignal,
|
||||
sendMessageSignal,
|
||||
monitorSignalProvider,
|
||||
messageActions: signalMessageActions,
|
||||
},
|
||||
imessage: {
|
||||
monitorIMessageProvider,
|
||||
probeIMessage,
|
||||
sendMessageIMessage,
|
||||
},
|
||||
whatsapp: createRuntimeWhatsApp(),
|
||||
line: {
|
||||
listLineAccountIds,
|
||||
resolveDefaultLineAccountId,
|
||||
resolveLineAccount,
|
||||
normalizeAccountId: normalizeLineAccountId,
|
||||
probeLineBot,
|
||||
sendMessageLine,
|
||||
pushMessageLine,
|
||||
pushMessagesLine,
|
||||
pushFlexMessage,
|
||||
pushTemplateMessage,
|
||||
pushLocationMessage,
|
||||
pushTextMessageWithQuickReplies,
|
||||
createQuickReplyItems,
|
||||
buildTemplateMessageFromPayload,
|
||||
monitorLineProvider,
|
||||
},
|
||||
};
|
||||
}
|
||||
9
src/plugins/runtime/runtime-config.ts
Normal file
9
src/plugins/runtime/runtime-config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { loadConfig, writeConfigFile } from "../../config/config.js";
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
export function createRuntimeConfig(): PluginRuntime["config"] {
|
||||
return {
|
||||
loadConfig,
|
||||
writeConfigFile,
|
||||
};
|
||||
}
|
||||
10
src/plugins/runtime/runtime-events.ts
Normal file
10
src/plugins/runtime/runtime-events.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { onAgentEvent } from "../../infra/agent-events.js";
|
||||
import { onSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
export function createRuntimeEvents(): PluginRuntime["events"] {
|
||||
return {
|
||||
onAgentEvent,
|
||||
onSessionTranscriptUpdate,
|
||||
};
|
||||
}
|
||||
21
src/plugins/runtime/runtime-logging.ts
Normal file
21
src/plugins/runtime/runtime-logging.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { shouldLogVerbose } from "../../globals.js";
|
||||
import { getChildLogger } from "../../logging.js";
|
||||
import { normalizeLogLevel } from "../../logging/levels.js";
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
export function createRuntimeLogging(): PluginRuntime["logging"] {
|
||||
return {
|
||||
shouldLogVerbose,
|
||||
getChildLogger: (bindings, opts) => {
|
||||
const logger = getChildLogger(bindings, {
|
||||
level: opts?.level ? normalizeLogLevel(opts.level) : undefined,
|
||||
});
|
||||
return {
|
||||
debug: (message) => logger.debug?.(message),
|
||||
info: (message) => logger.info(message),
|
||||
warn: (message) => logger.warn(message),
|
||||
error: (message) => logger.error(message),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
17
src/plugins/runtime/runtime-media.ts
Normal file
17
src/plugins/runtime/runtime-media.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { isVoiceCompatibleAudio } from "../../media/audio.js";
|
||||
import { mediaKindFromMime } from "../../media/constants.js";
|
||||
import { getImageMetadata, resizeToJpeg } from "../../media/image-ops.js";
|
||||
import { detectMime } from "../../media/mime.js";
|
||||
import { loadWebMedia } from "../../web/media.js";
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
export function createRuntimeMedia(): PluginRuntime["media"] {
|
||||
return {
|
||||
loadWebMedia,
|
||||
detectMime,
|
||||
mediaKindFromMime,
|
||||
isVoiceCompatibleAudio,
|
||||
getImageMetadata,
|
||||
resizeToJpeg,
|
||||
};
|
||||
}
|
||||
14
src/plugins/runtime/runtime-system.ts
Normal file
14
src/plugins/runtime/runtime-system.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { requestHeartbeatNow } from "../../infra/heartbeat-wake.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { runCommandWithTimeout } from "../../process/exec.js";
|
||||
import { formatNativeDependencyHint } from "./native-deps.js";
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
export function createRuntimeSystem(): PluginRuntime["system"] {
|
||||
return {
|
||||
enqueueSystemEvent,
|
||||
requestHeartbeatNow,
|
||||
runCommandWithTimeout,
|
||||
formatNativeDependencyHint,
|
||||
};
|
||||
}
|
||||
11
src/plugins/runtime/runtime-tools.ts
Normal file
11
src/plugins/runtime/runtime-tools.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createMemoryGetTool, createMemorySearchTool } from "../../agents/tools/memory-tool.js";
|
||||
import { registerMemoryCli } from "../../cli/memory-cli.js";
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
export function createRuntimeTools(): PluginRuntime["tools"] {
|
||||
return {
|
||||
createMemoryGetTool,
|
||||
createMemorySearchTool,
|
||||
registerMemoryCli,
|
||||
};
|
||||
}
|
||||
108
src/plugins/runtime/runtime-whatsapp.ts
Normal file
108
src/plugins/runtime/runtime-whatsapp.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { createWhatsAppLoginTool } from "../../channels/plugins/agent-tools/whatsapp-login.js";
|
||||
import { getActiveWebListener } from "../../web/active-listener.js";
|
||||
import {
|
||||
getWebAuthAgeMs,
|
||||
logoutWeb,
|
||||
logWebSelfId,
|
||||
readWebSelfId,
|
||||
webAuthExists,
|
||||
} from "../../web/auth-store.js";
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
const sendMessageWhatsAppLazy: PluginRuntime["channel"]["whatsapp"]["sendMessageWhatsApp"] = async (
|
||||
...args
|
||||
) => {
|
||||
const { sendMessageWhatsApp } = await loadWebOutbound();
|
||||
return sendMessageWhatsApp(...args);
|
||||
};
|
||||
|
||||
const sendPollWhatsAppLazy: PluginRuntime["channel"]["whatsapp"]["sendPollWhatsApp"] = async (
|
||||
...args
|
||||
) => {
|
||||
const { sendPollWhatsApp } = await loadWebOutbound();
|
||||
return sendPollWhatsApp(...args);
|
||||
};
|
||||
|
||||
const loginWebLazy: PluginRuntime["channel"]["whatsapp"]["loginWeb"] = async (...args) => {
|
||||
const { loginWeb } = await loadWebLogin();
|
||||
return loginWeb(...args);
|
||||
};
|
||||
|
||||
const startWebLoginWithQrLazy: PluginRuntime["channel"]["whatsapp"]["startWebLoginWithQr"] = async (
|
||||
...args
|
||||
) => {
|
||||
const { startWebLoginWithQr } = await loadWebLoginQr();
|
||||
return startWebLoginWithQr(...args);
|
||||
};
|
||||
|
||||
const waitForWebLoginLazy: PluginRuntime["channel"]["whatsapp"]["waitForWebLogin"] = async (
|
||||
...args
|
||||
) => {
|
||||
const { waitForWebLogin } = await loadWebLoginQr();
|
||||
return waitForWebLogin(...args);
|
||||
};
|
||||
|
||||
const monitorWebChannelLazy: PluginRuntime["channel"]["whatsapp"]["monitorWebChannel"] = async (
|
||||
...args
|
||||
) => {
|
||||
const { monitorWebChannel } = await loadWebChannel();
|
||||
return monitorWebChannel(...args);
|
||||
};
|
||||
|
||||
const handleWhatsAppActionLazy: PluginRuntime["channel"]["whatsapp"]["handleWhatsAppAction"] =
|
||||
async (...args) => {
|
||||
const { handleWhatsAppAction } = await loadWhatsAppActions();
|
||||
return handleWhatsAppAction(...args);
|
||||
};
|
||||
|
||||
let webOutboundPromise: Promise<typeof import("../../web/outbound.js")> | null = null;
|
||||
let webLoginPromise: Promise<typeof import("../../web/login.js")> | null = null;
|
||||
let webLoginQrPromise: Promise<typeof import("../../web/login-qr.js")> | null = null;
|
||||
let webChannelPromise: Promise<typeof import("../../channels/web/index.js")> | null = null;
|
||||
let whatsappActionsPromise: Promise<
|
||||
typeof import("../../agents/tools/whatsapp-actions.js")
|
||||
> | null = null;
|
||||
|
||||
function loadWebOutbound() {
|
||||
webOutboundPromise ??= import("../../web/outbound.js");
|
||||
return webOutboundPromise;
|
||||
}
|
||||
|
||||
function loadWebLogin() {
|
||||
webLoginPromise ??= import("../../web/login.js");
|
||||
return webLoginPromise;
|
||||
}
|
||||
|
||||
function loadWebLoginQr() {
|
||||
webLoginQrPromise ??= import("../../web/login-qr.js");
|
||||
return webLoginQrPromise;
|
||||
}
|
||||
|
||||
function loadWebChannel() {
|
||||
webChannelPromise ??= import("../../channels/web/index.js");
|
||||
return webChannelPromise;
|
||||
}
|
||||
|
||||
function loadWhatsAppActions() {
|
||||
whatsappActionsPromise ??= import("../../agents/tools/whatsapp-actions.js");
|
||||
return whatsappActionsPromise;
|
||||
}
|
||||
|
||||
export function createRuntimeWhatsApp(): PluginRuntime["channel"]["whatsapp"] {
|
||||
return {
|
||||
getActiveWebListener,
|
||||
getWebAuthAgeMs,
|
||||
logoutWeb,
|
||||
logWebSelfId,
|
||||
readWebSelfId,
|
||||
webAuthExists,
|
||||
sendMessageWhatsApp: sendMessageWhatsAppLazy,
|
||||
sendPollWhatsApp: sendPollWhatsAppLazy,
|
||||
loginWeb: loginWebLazy,
|
||||
startWebLoginWithQr: startWebLoginWithQrLazy,
|
||||
waitForWebLogin: waitForWebLoginLazy,
|
||||
monitorWebChannel: monitorWebChannelLazy,
|
||||
handleWhatsAppAction: handleWhatsAppActionLazy,
|
||||
createLoginTool: createWhatsAppLoginTool,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user