TypeScript: add extensions to tsconfig and fix type errors (#12781)

* TypeScript: add extensions to tsconfig and fix type errors

- Add extensions/**/* to tsconfig.json includes
- Export ProviderAuthResult, AnyAgentTool from plugin-sdk
- Fix optional chaining for messageActions across channels
- Add missing type imports (MSTeamsConfig, GroupPolicy, etc.)
- Add type annotations for provider auth handlers
- Fix undici/fetch type compatibility in zalo proxy
- Correct ChannelAccountSnapshot property usage
- Add type casts for tool registrations
- Extract usage view styles and types to separate files

* TypeScript: fix optional debug calls and handleAction guards
This commit is contained in:
max
2026-02-09 10:05:38 -08:00
committed by GitHub
parent 2e4334c32c
commit 40b11db80e
87 changed files with 2947 additions and 2706 deletions

View File

@@ -86,7 +86,7 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
if (!spec?.gate) {
continue;
}
if (spec.unsupportedOnMacOS26 && macOS26) {
if ("unsupportedOnMacOS26" in spec && spec.unsupportedOnMacOS26 && macOS26) {
continue;
}
if (gate(spec.gate)) {

View File

@@ -361,14 +361,16 @@ function combineDebounceEntries(entries: BlueBubblesDebounceEntry[]): Normalized
const webhookTargets = new Map<string, WebhookTarget[]>();
type BlueBubblesDebouncer = {
enqueue: (item: BlueBubblesDebounceEntry) => Promise<void>;
flushKey: (key: string) => Promise<void>;
};
/**
* Maps webhook targets to their inbound debouncers.
* Each target gets its own debouncer keyed by a unique identifier.
*/
const targetDebouncers = new Map<
WebhookTarget,
ReturnType<BlueBubblesCoreRuntime["channel"]["debounce"]["createInboundDebouncer"]>
>();
const targetDebouncers = new Map<WebhookTarget, BlueBubblesDebouncer>();
function resolveBlueBubblesDebounceMs(
config: OpenClawConfig,
@@ -1917,7 +1919,7 @@ async function processMessage(
maxBytes,
});
const saved = await core.channel.media.saveMediaBuffer(
downloaded.buffer,
Buffer.from(downloaded.buffer),
downloaded.contentType,
"inbound",
maxBytes,
@@ -2349,7 +2351,7 @@ async function processMessage(
},
});
}
if (shouldStopTyping) {
if (shouldStopTyping && chatGuidForActions) {
// Stop typing after streaming completes to avoid a stuck indicator.
sendBlueBubblesTyping(chatGuidForActions, false, {
cfg: config,

View File

@@ -1,4 +1,5 @@
export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk";
import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk";
export type { DmPolicy, GroupPolicy };
export type BlueBubblesGroupConfig = {
/** If true, only respond in this group when mentioned. */

View File

@@ -1,4 +1,9 @@
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
type ProviderAuthContext,
type ProviderAuthResult,
} from "openclaw/plugin-sdk";
const DEFAULT_BASE_URL = "http://localhost:3000/v1";
const DEFAULT_API_KEY = "n/a";
@@ -57,9 +62,9 @@ function buildModelDefinition(modelId: string) {
return {
id: modelId,
name: modelId,
api: "openai-completions",
api: "openai-completions" as const,
reasoning: false,
input: ["text", "image"],
input: ["text", "image"] as Array<"text" | "image">,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
@@ -71,7 +76,7 @@ const copilotProxyPlugin = {
name: "Copilot Proxy",
description: "Local Copilot Proxy (VS Code LM) provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api) {
register(api: OpenClawPluginApi) {
api.registerProvider({
id: "copilot-proxy",
label: "Copilot Proxy",
@@ -82,7 +87,7 @@ const copilotProxyPlugin = {
label: "Local proxy",
hint: "Configure base URL + models for the Copilot Proxy server",
kind: "custom",
run: async (ctx) => {
run: async (ctx: ProviderAuthContext): Promise<ProviderAuthResult> => {
const baseUrlInput = await ctx.prompter.text({
message: "Copilot Proxy base URL",
initialValue: DEFAULT_BASE_URL,
@@ -92,7 +97,7 @@ const copilotProxyPlugin = {
const modelInput = await ctx.prompter.text({
message: "Model IDs (comma-separated)",
initialValue: DEFAULT_MODEL_IDS.join(", "),
validate: (value) =>
validate: (value: string) =>
parseModelIds(value).length > 0 ? undefined : "Enter at least one model id",
});

View File

@@ -128,7 +128,8 @@ function pickLanIPv4(): string | null {
}
for (const entry of entries) {
const family = entry?.family;
const isIpv4 = family === "IPv4" || family === 4;
// Check for IPv4 (string "IPv4" on Node 18+, number 4 on older)
const isIpv4 = family === "IPv4" || String(family) === "4";
if (!entry || entry.internal || !isIpv4) {
continue;
}
@@ -152,7 +153,8 @@ function pickTailnetIPv4(): string | null {
}
for (const entry of entries) {
const family = entry?.family;
const isIpv4 = family === "IPv4" || family === 4;
// Check for IPv4 (string "IPv4" on Node 18+, number 4 on older)
const isIpv4 = family === "IPv4" || String(family) === "4";
if (!entry || entry.internal || !isIpv4) {
continue;
}

View File

@@ -4,7 +4,7 @@ import { metrics, trace, SpanStatusCode } from "@opentelemetry/api";
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Resource } from "@opentelemetry/resources";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { NodeSDK } from "@opentelemetry/sdk-node";
@@ -73,7 +73,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
return;
}
const resource = new Resource({
const resource = resourceFromAttributes({
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
});
@@ -210,15 +210,13 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
...(logUrl ? { url: logUrl } : {}),
...(headers ? { headers } : {}),
});
logProvider = new LoggerProvider({ resource });
logProvider.addLogRecordProcessor(
new BatchLogRecordProcessor(
logExporter,
typeof otel.flushIntervalMs === "number"
? { scheduledDelayMillis: Math.max(1000, otel.flushIntervalMs) }
: {},
),
const processor = new BatchLogRecordProcessor(
logExporter,
typeof otel.flushIntervalMs === "number"
? { scheduledDelayMillis: Math.max(1000, otel.flushIntervalMs) }
: {},
);
logProvider = new LoggerProvider({ resource, processors: [processor] });
const otelLogger = logProvider.getLogger("openclaw");
stopLogTransport = registerLogTransport((logObj) => {

View File

@@ -31,10 +31,17 @@ import { getDiscordRuntime } from "./runtime.js";
const meta = getChatChannelMeta("discord");
const discordMessageActions: ChannelMessageActionAdapter = {
listActions: (ctx) => getDiscordRuntime().channel.discord.messageActions.listActions(ctx),
extractToolSend: (ctx) => getDiscordRuntime().channel.discord.messageActions.extractToolSend(ctx),
handleAction: async (ctx) =>
await getDiscordRuntime().channel.discord.messageActions.handleAction(ctx),
listActions: (ctx) =>
getDiscordRuntime().channel.discord.messageActions?.listActions?.(ctx) ?? [],
extractToolSend: (ctx) =>
getDiscordRuntime().channel.discord.messageActions?.extractToolSend?.(ctx) ?? null,
handleAction: async (ctx) => {
const ma = getDiscordRuntime().channel.discord.messageActions;
if (!ma?.handleAction) {
throw new Error("Discord message actions not available");
}
return ma.handleAction(ctx);
},
};
export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {

View File

@@ -212,7 +212,8 @@ async function createRecord(
) {
const res = await client.bitable.appTableRecord.create({
path: { app_token: appToken, table_id: tableId },
data: { fields },
// oxlint-disable-next-line typescript/no-explicit-any
data: { fields: fields as any },
});
if (res.code !== 0) {
throw new Error(res.msg);
@@ -232,7 +233,8 @@ async function updateRecord(
) {
const res = await client.bitable.appTableRecord.update({
path: { app_token: appToken, table_id: tableId, record_id: recordId },
data: { fields },
// oxlint-disable-next-line typescript/no-explicit-any
data: { fields: fields as any },
});
if (res.code !== 0) {
throw new Error(res.msg);

View File

@@ -1,4 +1,4 @@
import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
import {
@@ -19,7 +19,7 @@ import { probeFeishu } from "./probe.js";
import { sendMessageFeishu } from "./send.js";
import { normalizeFeishuTarget, looksLikeFeishuId } from "./targets.js";
const meta = {
const meta: ChannelMeta = {
id: "feishu",
label: "Feishu",
selectionLabel: "Feishu/Lark (飞书)",
@@ -28,7 +28,7 @@ const meta = {
blurb: "飞书/Lark enterprise messaging.",
aliases: ["lark"],
order: 70,
} as const;
};
export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
id: "feishu",
@@ -38,12 +38,11 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
pairing: {
idLabel: "feishuUserId",
normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ""),
notifyApproval: async ({ cfg, id, accountId }) => {
notifyApproval: async ({ cfg, id }) => {
await sendMessageFeishu({
cfg,
to: id,
text: PAIRING_APPROVED_MESSAGE,
accountId,
});
},
},
@@ -202,7 +201,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
}),
resolveAllowFrom: ({ cfg, accountId }) => {
const account = resolveFeishuAccount({ cfg, accountId });
return account.config?.allowFrom ?? [];
return (account.config?.allowFrom ?? []).map((entry) => String(entry));
},
formatAllowFrom: ({ allowFrom }) =>
allowFrom
@@ -265,7 +264,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
},
onboarding: feishuOnboardingAdapter,
messaging: {
normalizeTarget: normalizeFeishuTarget,
normalizeTarget: (raw) => normalizeFeishuTarget(raw) ?? undefined,
targetResolver: {
looksLikeId: looksLikeFeishuId,
hint: "<chatId|user:openId|chat:chatId>",
@@ -274,13 +273,33 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
directory: {
self: async () => null,
listPeers: async ({ cfg, query, limit, accountId }) =>
listFeishuDirectoryPeers({ cfg, query, limit, accountId }),
listFeishuDirectoryPeers({
cfg,
query: query ?? undefined,
limit: limit ?? undefined,
accountId: accountId ?? undefined,
}),
listGroups: async ({ cfg, query, limit, accountId }) =>
listFeishuDirectoryGroups({ cfg, query, limit, accountId }),
listFeishuDirectoryGroups({
cfg,
query: query ?? undefined,
limit: limit ?? undefined,
accountId: accountId ?? undefined,
}),
listPeersLive: async ({ cfg, query, limit, accountId }) =>
listFeishuDirectoryPeersLive({ cfg, query, limit, accountId }),
listFeishuDirectoryPeersLive({
cfg,
query: query ?? undefined,
limit: limit ?? undefined,
accountId: accountId ?? undefined,
}),
listGroupsLive: async ({ cfg, query, limit, accountId }) =>
listFeishuDirectoryGroupsLive({ cfg, query, limit, accountId }),
listFeishuDirectoryGroupsLive({
cfg,
query: query ?? undefined,
limit: limit ?? undefined,
accountId: accountId ?? undefined,
}),
},
outbound: feishuOutbound,
status: {
@@ -302,8 +321,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
probe: snapshot.probe,
lastProbeAt: snapshot.lastProbeAt ?? null,
}),
probeAccount: async ({ cfg, accountId }) => {
const account = resolveFeishuAccount({ cfg, accountId });
probeAccount: async ({ account }) => {
return await probeFeishu(account);
},
buildAccountSnapshot: ({ account, runtime, probe }) => ({

View File

@@ -80,7 +80,10 @@ async function promptFeishuAllowFrom(params: {
}
const unique = [
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...parts]),
...new Set([
...existing.map((v: string | number) => String(v).trim()).filter(Boolean),
...parts,
]),
];
return setFeishuAllowFrom(params.cfg, unique);
}

View File

@@ -9,32 +9,47 @@ export const feishuOutbound: ChannelOutboundAdapter = {
chunkerMode: "markdown",
textChunkLimit: 4000,
sendText: async ({ cfg, to, text, accountId }) => {
const result = await sendMessageFeishu({ cfg, to, text, accountId });
const result = await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined });
return { channel: "feishu", ...result };
},
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
// Send text first if provided
if (text?.trim()) {
await sendMessageFeishu({ cfg, to, text, accountId });
await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined });
}
// Upload and send media if URL provided
if (mediaUrl) {
try {
const result = await sendMediaFeishu({ cfg, to, mediaUrl, accountId });
const result = await sendMediaFeishu({
cfg,
to,
mediaUrl,
accountId: accountId ?? undefined,
});
return { channel: "feishu", ...result };
} catch (err) {
// Log the error for debugging
console.error(`[feishu] sendMediaFeishu failed:`, err);
// Fallback to URL link if upload fails
const fallbackText = `📎 ${mediaUrl}`;
const result = await sendMessageFeishu({ cfg, to, text: fallbackText, accountId });
const result = await sendMessageFeishu({
cfg,
to,
text: fallbackText,
accountId: accountId ?? undefined,
});
return { channel: "feishu", ...result };
}
}
// No media URL, just return text result
const result = await sendMessageFeishu({ cfg, to, text: text ?? "", accountId });
const result = await sendMessageFeishu({
cfg,
to,
text: text ?? "",
accountId: accountId ?? undefined,
});
return { channel: "feishu", ...result };
},
};

View File

@@ -90,16 +90,11 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
},
});
const textChunkLimit = core.channel.text.resolveTextChunkLimit({
cfg,
channel: "feishu",
defaultLimit: 4000,
const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "feishu", accountId, {
fallbackLimit: 4000,
});
const chunkMode = core.channel.text.resolveChunkMode(cfg, "feishu");
const tableMode = core.channel.text.resolveMarkdownTableMode({
cfg,
channel: "feishu",
});
const tableMode = core.channel.text.resolveMarkdownTableMode({ cfg, channel: "feishu" });
const { dispatcher, replyOptions, markDispatchIdle } =
core.channel.reply.createReplyDispatcherWithTyping({

View File

@@ -1,7 +1,11 @@
import { createHash, randomBytes } from "node:crypto";
import { readFileSync } from "node:fs";
import { createServer } from "node:http";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
type ProviderAuthContext,
} from "openclaw/plugin-sdk";
// OAuth constants - decoded from pi-ai's base64 encoded values to stay in sync
const decode = (s: string) => Buffer.from(s, "base64").toString();
@@ -392,7 +396,7 @@ const antigravityPlugin = {
name: "Google Antigravity Auth",
description: "OAuth flow for Google Antigravity (Cloud Code Assist)",
configSchema: emptyPluginConfigSchema(),
register(api) {
register(api: OpenClawPluginApi) {
api.registerProvider({
id: "google-antigravity",
label: "Google Antigravity",
@@ -404,7 +408,7 @@ const antigravityPlugin = {
label: "Google OAuth",
hint: "PKCE + localhost callback",
kind: "oauth",
run: async (ctx) => {
run: async (ctx: ProviderAuthContext) => {
const spin = ctx.prompter.progress("Starting Antigravity OAuth…");
try {
const result = await loginAntigravity({

View File

@@ -1,4 +1,8 @@
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
type ProviderAuthContext,
} from "openclaw/plugin-sdk";
import { loginGeminiCliOAuth } from "./oauth.js";
const PROVIDER_ID = "google-gemini-cli";
@@ -16,7 +20,7 @@ const geminiCliPlugin = {
name: "Google Gemini CLI Auth",
description: "OAuth flow for Gemini CLI (Google Code Assist)",
configSchema: emptyPluginConfigSchema(),
register(api) {
register(api: OpenClawPluginApi) {
api.registerProvider({
id: PROVIDER_ID,
label: PROVIDER_LABEL,
@@ -29,7 +33,7 @@ const geminiCliPlugin = {
label: "Google OAuth",
hint: "PKCE + localhost callback",
kind: "oauth",
run: async (ctx) => {
run: async (ctx: ProviderAuthContext) => {
const spin = ctx.prompter.progress("Starting Gemini CLI OAuth…");
try {
const result = await loginGeminiCliOAuth({

View File

@@ -97,11 +97,11 @@ export const googlechatMessageActions: ChannelMessageActionAdapter = {
if (mediaUrl) {
const core = getGoogleChatRuntime();
const maxBytes = (account.config.mediaMaxMb ?? 20) * 1024 * 1024;
const loaded = await core.channel.media.fetchRemoteMedia(mediaUrl, { maxBytes });
const loaded = await core.channel.media.fetchRemoteMedia({ url: mediaUrl, maxBytes });
const upload = await uploadGoogleChatAttachment({
account,
space,
filename: loaded.filename ?? "attachment",
filename: loaded.fileName ?? "attachment",
buffer: loaded.buffer,
contentType: loaded.contentType,
});
@@ -114,7 +114,7 @@ export const googlechatMessageActions: ChannelMessageActionAdapter = {
? [
{
attachmentUploadToken: upload.attachmentUploadToken,
contentName: loaded.filename,
contentName: loaded.fileName,
},
]
: undefined,

View File

@@ -15,6 +15,7 @@ import {
type ChannelDock,
type ChannelMessageActionAdapter,
type ChannelPlugin,
type ChannelStatusIssue,
type OpenClawConfig,
} from "openclaw/plugin-sdk";
import { GoogleChatConfigSchema } from "openclaw/plugin-sdk";
@@ -451,13 +452,14 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
(cfg.channels?.["googlechat"] as { mediaMaxMb?: number } | undefined)?.mediaMaxMb,
accountId,
});
const loaded = await runtime.channel.media.fetchRemoteMedia(mediaUrl, {
const loaded = await runtime.channel.media.fetchRemoteMedia({
url: mediaUrl,
maxBytes: maxBytes ?? (account.config.mediaMaxMb ?? 20) * 1024 * 1024,
});
const upload = await uploadGoogleChatAttachment({
account,
space,
filename: loaded.filename ?? "attachment",
filename: loaded.fileName ?? "attachment",
buffer: loaded.buffer,
contentType: loaded.contentType,
});
@@ -467,7 +469,7 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
text,
thread,
attachments: upload.attachmentUploadToken
? [{ attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.filename }]
? [{ attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.fileName }]
: undefined,
});
return {
@@ -485,7 +487,7 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
lastStopAt: null,
lastError: null,
},
collectStatusIssues: (accounts) =>
collectStatusIssues: (accounts): ChannelStatusIssue[] =>
accounts.flatMap((entry) => {
const accountId = String(entry.accountId ?? DEFAULT_ACCOUNT_ID);
const enabled = entry.enabled !== false;
@@ -493,7 +495,7 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
if (!enabled || !configured) {
return [];
}
const issues = [];
const issues: ChannelStatusIssue[] = [];
if (!entry.audience) {
issues.push({
channel: "googlechat",

View File

@@ -835,7 +835,8 @@ async function deliverGoogleChatReply(params: {
const caption = first && !suppressCaption ? payload.text : undefined;
first = false;
try {
const loaded = await core.channel.media.fetchRemoteMedia(mediaUrl, {
const loaded = await core.channel.media.fetchRemoteMedia({
url: mediaUrl,
maxBytes: (account.config.mediaMaxMb ?? 20) * 1024 * 1024,
});
const upload = await uploadAttachmentForReply({
@@ -843,7 +844,7 @@ async function deliverGoogleChatReply(params: {
spaceId,
buffer: loaded.buffer,
contentType: loaded.contentType,
filename: loaded.filename ?? "attachment",
filename: loaded.fileName ?? "attachment",
});
if (!upload.attachmentUploadToken) {
throw new Error("missing attachment upload token");
@@ -854,7 +855,7 @@ async function deliverGoogleChatReply(params: {
text: caption,
thread: payload.replyToId,
attachments: [
{ attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.filename },
{ attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.fileName },
],
});
statusSink?.({ lastOutboundAt: Date.now() });

View File

@@ -60,7 +60,7 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
config: {
listAccountIds: (cfg) => getLineRuntime().channel.line.listLineAccountIds(cfg),
resolveAccount: (cfg, accountId) =>
getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId }),
getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId: accountId ?? undefined }),
defaultAccountId: (cfg) => getLineRuntime().channel.line.resolveDefaultLineAccountId(cfg),
setAccountEnabled: ({ cfg, accountId, enabled }) => {
const lineConfig = (cfg.channels?.line ?? {}) as LineConfig;
@@ -125,11 +125,12 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
name: account.name,
enabled: account.enabled,
configured: Boolean(account.channelAccessToken?.trim()),
tokenSource: account.tokenSource,
tokenSource: account.tokenSource ?? undefined,
}),
resolveAllowFrom: ({ cfg, accountId }) =>
(
getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId }).config.allowFrom ?? []
getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId: accountId ?? undefined })
.config.allowFrom ?? []
).map((entry) => String(entry)),
formatAllowFrom: ({ allowFrom }) =>
allowFrom
@@ -172,9 +173,12 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
},
groups: {
resolveRequireMention: ({ cfg, accountId, groupId }) => {
const account = getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId });
const account = getLineRuntime().channel.line.resolveLineAccount({
cfg,
accountId: accountId ?? undefined,
});
const groups = account.config.groups;
if (!groups) {
if (!groups || !groupId) {
return false;
}
const groupConfig = groups[groupId] ?? groups["*"];
@@ -185,7 +189,7 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
normalizeTarget: (target) => {
const trimmed = target.trim();
if (!trimmed) {
return null;
return undefined;
}
return trimmed.replace(/^line:(group|room|user):/i, "").replace(/^line:/i, "");
},
@@ -351,12 +355,15 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
const hasQuickReplies = quickReplies.length > 0;
const quickReply = hasQuickReplies ? createQuickReplyItems(quickReplies) : undefined;
// oxlint-disable-next-line typescript/no-explicit-any
const sendMessageBatch = async (messages: Array<Record<string, unknown>>) => {
if (messages.length === 0) {
return;
}
for (let i = 0; i < messages.length; i += 5) {
const result = await sendBatch(to, messages.slice(i, i + 5), {
// LINE SDK expects Message[] but we build dynamically
const batch = messages.slice(i, i + 5) as unknown as Parameters<typeof sendBatch>[1];
const result = await sendBatch(to, batch, {
verbose: false,
accountId: accountId ?? undefined,
});
@@ -381,15 +388,12 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
if (!shouldSendQuickRepliesInline) {
if (lineData.flexMessage) {
lastResult = await sendFlex(
to,
lineData.flexMessage.altText,
lineData.flexMessage.contents,
{
verbose: false,
accountId: accountId ?? undefined,
},
);
// LINE SDK expects FlexContainer but we receive contents as unknown
const flexContents = lineData.flexMessage.contents as Parameters<typeof sendFlex>[2];
lastResult = await sendFlex(to, lineData.flexMessage.altText, flexContents, {
verbose: false,
accountId: accountId ?? undefined,
});
}
if (lineData.templateMessage) {
@@ -410,7 +414,9 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
}
for (const flexMsg of processed.flexMessages) {
lastResult = await sendFlex(to, flexMsg.altText, flexMsg.contents, {
// LINE SDK expects FlexContainer but we receive contents as unknown
const flexContents = flexMsg.contents as Parameters<typeof sendFlex>[2];
lastResult = await sendFlex(to, flexMsg.altText, flexContents, {
verbose: false,
accountId: accountId ?? undefined,
});
@@ -532,7 +538,9 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
// Send flex messages for tables/code blocks
for (const flexMsg of processed.flexMessages) {
await sendFlex(to, flexMsg.altText, flexMsg.contents, {
// LINE SDK expects FlexContainer but we receive contents as unknown
const flexContents = flexMsg.contents as Parameters<typeof sendFlex>[2];
await sendFlex(to, flexMsg.altText, flexContents, {
verbose: false,
accountId: accountId ?? undefined,
});

View File

@@ -1,6 +1,6 @@
import type { OpenClawPluginApi } from "../../src/plugins/types.js";
import type { AnyAgentTool, OpenClawPluginApi } from "../../src/plugins/types.js";
import { createLlmTaskTool } from "./src/llm-task-tool.js";
export default function register(api: OpenClawPluginApi) {
api.registerTool(createLlmTaskTool(api), { optional: true });
api.registerTool(createLlmTaskTool(api) as unknown as AnyAgentTool, { optional: true });
}

View File

@@ -25,11 +25,11 @@ async function loadRunEmbeddedPiAgent(): Promise<RunEmbeddedPiAgentFn> {
}
// Bundled install (built)
const mod = await import("../../../agents/pi-embedded-runner.js");
const mod = await import("../../../src/agents/pi-embedded-runner.js");
if (typeof mod.runEmbeddedPiAgent !== "function") {
throw new Error("Internal error: runEmbeddedPiAgent not available");
}
return mod.runEmbeddedPiAgent;
return mod.runEmbeddedPiAgent as RunEmbeddedPiAgentFn;
}
function stripCodeFences(s: string): string {
@@ -69,6 +69,7 @@ type PluginCfg = {
export function createLlmTaskTool(api: OpenClawPluginApi) {
return {
name: "llm-task",
label: "LLM Task",
description:
"Run a generic JSON-only LLM task and return schema-validated JSON. Designed for orchestration from Lobster workflows via openclaw.invoke.",
parameters: Type.Object({
@@ -214,14 +215,17 @@ export function createLlmTaskTool(api: OpenClawPluginApi) {
// oxlint-disable-next-line typescript/no-explicit-any
const schema = (params as any).schema as unknown;
if (schema && typeof schema === "object" && !Array.isArray(schema)) {
const ajv = new Ajv({ allErrors: true, strict: false });
const ajv = new Ajv.default({ allErrors: true, strict: false });
// oxlint-disable-next-line typescript/no-explicit-any
const validate = ajv.compile(schema as any);
const ok = validate(parsed);
if (!ok) {
const msg =
validate.errors
?.map((e) => `${e.instancePath || "<root>"} ${e.message || "invalid"}`)
?.map(
(e: { instancePath?: string; message?: string }) =>
`${e.instancePath || "<root>"} ${e.message || "invalid"}`,
)
.join("; ") ?? "invalid";
throw new Error(`LLM JSON did not match schema: ${msg}`);
}

View File

@@ -1,14 +1,18 @@
import type { OpenClawPluginApi } from "../../src/plugins/types.js";
import type {
AnyAgentTool,
OpenClawPluginApi,
OpenClawPluginToolFactory,
} from "../../src/plugins/types.js";
import { createLobsterTool } from "./src/lobster-tool.js";
export default function register(api: OpenClawPluginApi) {
api.registerTool(
(ctx) => {
((ctx) => {
if (ctx.sandboxed) {
return null;
}
return createLobsterTool(api);
},
return createLobsterTool(api) as AnyAgentTool;
}) as OpenClawPluginToolFactory,
{ optional: true },
);
}

View File

@@ -232,6 +232,7 @@ function parseEnvelope(stdout: string): LobsterEnvelope {
export function createLobsterTool(api: OpenClawPluginApi) {
return {
name: "lobster",
label: "Lobster Workflow",
description:
"Run Lobster pipelines as a local-first workflow runtime (typed JSON envelope + resumable approvals).",
parameters: Type.Object({

View File

@@ -78,7 +78,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
replyToId: replyTo ?? undefined,
threadId: threadId ?? undefined,
},
cfg,
cfg as CoreConfig,
);
}
@@ -94,7 +94,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
emoji,
remove,
},
cfg,
cfg as CoreConfig,
);
}
@@ -108,7 +108,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
messageId,
limit,
},
cfg,
cfg as CoreConfig,
);
}
@@ -122,7 +122,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
before: readStringParam(params, "before"),
after: readStringParam(params, "after"),
},
cfg,
cfg as CoreConfig,
);
}
@@ -136,7 +136,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
messageId,
content,
},
cfg,
cfg as CoreConfig,
);
}
@@ -148,7 +148,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
roomId: resolveRoomId(),
messageId,
},
cfg,
cfg as CoreConfig,
);
}
@@ -164,7 +164,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
roomId: resolveRoomId(),
messageId,
},
cfg,
cfg as CoreConfig,
);
}
@@ -176,7 +176,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
userId,
roomId: readStringParam(params, "roomId") ?? readStringParam(params, "channelId"),
},
cfg,
cfg as CoreConfig,
);
}
@@ -186,7 +186,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
action: "channelInfo",
roomId: resolveRoomId(),
},
cfg,
cfg as CoreConfig,
);
}

View File

@@ -1,4 +1,4 @@
import type { CoreConfig } from "../types.js";
import type { CoreConfig } from "../../types.js";
import type { MatrixActionClient, MatrixActionClientOpts } from "./types.js";
import { getMatrixRuntime } from "../../runtime.js";
import { getActiveMatrixClient } from "../active-client.js";
@@ -47,7 +47,9 @@ export async function resolveActionClient(
if (auth.encryption && client.crypto) {
try {
const joinedRooms = await client.getJoinedRooms();
await client.crypto.prepare(joinedRooms);
await (client.crypto as { prepare: (rooms?: string[]) => Promise<void> }).prepare(
joinedRooms,
);
} catch {
// Ignore crypto prep failures for one-off actions.
}

View File

@@ -63,7 +63,7 @@ export async function fetchEventSummary(
eventId: string,
): Promise<MatrixMessageSummary | null> {
try {
const raw = (await client.getEvent(roomId, eventId)) as MatrixRawEvent;
const raw = (await client.getEvent(roomId, eventId)) as unknown as MatrixRawEvent;
if (raw.unsigned?.redacted_because) {
return null;
}

View File

@@ -1,5 +1,5 @@
import { MatrixClient } from "@vector-im/matrix-bot-sdk";
import type { CoreConfig } from "../types.js";
import type { CoreConfig } from "../../types.js";
import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
import { getMatrixRuntime } from "../../runtime.js";
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";

View File

@@ -1,6 +1,6 @@
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
import { LogService } from "@vector-im/matrix-bot-sdk";
import type { CoreConfig } from "../types.js";
import type { CoreConfig } from "../../types.js";
import type { MatrixAuth } from "./types.js";
import { resolveMatrixAuth } from "./config.js";
import { createMatrixClient } from "./create-client.js";
@@ -69,7 +69,9 @@ async function ensureSharedClientStarted(params: {
try {
const joinedRooms = await client.getJoinedRooms();
if (client.crypto) {
await client.crypto.prepare(joinedRooms);
await (client.crypto as { prepare: (rooms?: string[]) => Promise<void> }).prepare(
joinedRooms,
);
params.state.cryptoReady = true;
}
} catch (err) {

View File

@@ -1,5 +1,5 @@
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
import type { PluginRuntime } from "openclaw/plugin-sdk";
import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk";
import type { MatrixAuth } from "../client.js";
import type { MatrixRawEvent } from "./types.js";
import { EventType } from "./types.js";
@@ -10,7 +10,7 @@ export function registerMatrixMonitorEvents(params: {
logVerboseMessage: (message: string) => void;
warnedEncryptedRooms: Set<string>;
warnedCryptoMissingRooms: Set<string>;
logger: { warn: (meta: Record<string, unknown>, message: string) => void };
logger: RuntimeLogger;
formatNativeDependencyHint: PluginRuntime["system"]["formatNativeDependencyHint"];
onRoomMessage: (roomId: string, event: MatrixRawEvent) => void | Promise<void>;
}): void {
@@ -42,10 +42,11 @@ export function registerMatrixMonitorEvents(params: {
client.on(
"room.failed_decryption",
async (roomId: string, event: MatrixRawEvent, error: Error) => {
logger.warn(
{ roomId, eventId: event.event_id, error: error.message },
"Failed to decrypt message",
);
logger.warn("Failed to decrypt message", {
roomId,
eventId: event.event_id,
error: error.message,
});
logVerboseMessage(
`matrix: failed decrypt room=${roomId} id=${event.event_id ?? "unknown"} error=${error.message}`,
);
@@ -76,7 +77,7 @@ export function registerMatrixMonitorEvents(params: {
warnedEncryptedRooms.add(roomId);
const warning =
"matrix: encrypted event received without encryption enabled; set channels.matrix.encryption=true and verify the device to decrypt";
logger.warn({ roomId }, warning);
logger.warn(warning, { roomId });
}
if (auth.encryption === true && !client.crypto && !warnedCryptoMissingRooms.has(roomId)) {
warnedCryptoMissingRooms.add(roomId);
@@ -86,7 +87,7 @@ export function registerMatrixMonitorEvents(params: {
downloadCommand: "node node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js",
});
const warning = `matrix: encryption enabled but crypto is unavailable; ${hint}`;
logger.warn({ roomId }, warning);
logger.warn(warning, { roomId });
}
return;
}

View File

@@ -6,9 +6,11 @@ import {
logInboundDrop,
logTypingFailure,
resolveControlCommandGate,
type PluginRuntime,
type RuntimeEnv,
type RuntimeLogger,
} from "openclaw/plugin-sdk";
import type { CoreConfig, ReplyToMode } from "../../types.js";
import type { CoreConfig, MatrixRoomConfig, ReplyToMode } from "../../types.js";
import type { MatrixRawEvent, RoomMessageEventContent } from "./types.js";
import {
formatPollAsText,
@@ -37,34 +39,14 @@ import { EventType, RelationType } from "./types.js";
export type MatrixMonitorHandlerParams = {
client: MatrixClient;
core: {
logging: {
shouldLogVerbose: () => boolean;
};
channel: (typeof import("openclaw/plugin-sdk"))["channel"];
system: {
enqueueSystemEvent: (
text: string,
meta: { sessionKey?: string | null; contextKey?: string | null },
) => void;
};
};
core: PluginRuntime;
cfg: CoreConfig;
runtime: RuntimeEnv;
logger: {
info: (message: string | Record<string, unknown>, ...meta: unknown[]) => void;
warn: (meta: Record<string, unknown>, message: string) => void;
};
logger: RuntimeLogger;
logVerboseMessage: (message: string) => void;
allowFrom: string[];
roomsConfig: CoreConfig["channels"] extends { matrix?: infer MatrixConfig }
? MatrixConfig extends { groups?: infer Groups }
? Groups
: Record<string, unknown> | undefined
: Record<string, unknown> | undefined;
mentionRegexes: ReturnType<
(typeof import("openclaw/plugin-sdk"))["channel"]["mentions"]["buildMentionRegexes"]
>;
roomsConfig: Record<string, MatrixRoomConfig> | undefined;
mentionRegexes: ReturnType<PluginRuntime["channel"]["mentions"]["buildMentionRegexes"]>;
groupPolicy: "open" | "allowlist" | "disabled";
replyToMode: ReplyToMode;
threadReplies: "off" | "inbound" | "always";
@@ -121,7 +103,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
}
const isPollEvent = isPollStartType(eventType);
const locationContent = event.content as LocationMessageEventContent;
const locationContent = event.content as unknown as LocationMessageEventContent;
const isLocationEvent =
eventType === EventType.Location ||
(eventType === EventType.RoomMessage && locationContent.msgtype === EventType.Location);
@@ -159,9 +141,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
const roomName = roomInfo.name;
const roomAliases = [roomInfo.canonicalAlias ?? "", ...roomInfo.altAliases].filter(Boolean);
let content = event.content as RoomMessageEventContent;
let content = event.content as unknown as RoomMessageEventContent;
if (isPollEvent) {
const pollStartContent = event.content as PollStartContent;
const pollStartContent = event.content as unknown as PollStartContent;
const pollSummary = parsePollStartContent(pollStartContent);
if (pollSummary) {
pollSummary.eventId = event.event_id ?? "";
@@ -435,7 +417,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
hasControlCommandInMessage;
const canDetectMention = mentionRegexes.length > 0 || hasExplicitMention;
if (isRoom && shouldRequireMention && !wasMentioned && !shouldBypassMention) {
logger.info({ roomId, reason: "no-mention" }, "skipping room message");
logger.info("skipping room message", { roomId, reason: "no-mention" });
return;
}
@@ -523,14 +505,11 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
}
: undefined,
onRecordError: (err) => {
logger.warn(
{
error: String(err),
storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
},
"failed updating session meta",
);
logger.warn("failed updating session meta", {
error: String(err),
storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
});
},
});

View File

@@ -55,7 +55,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
if (!core.logging.shouldLogVerbose()) {
return;
}
logger.debug(message);
logger.debug?.(message);
};
const normalizeUserEntry = (raw: string) =>
@@ -75,13 +75,13 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
): Promise<string[]> => {
let allowList = list ?? [];
if (allowList.length === 0) {
return allowList;
return allowList.map(String);
}
const entries = allowList
.map((entry) => normalizeUserEntry(String(entry)))
.filter((entry) => entry && entry !== "*");
if (entries.length === 0) {
return allowList;
return allowList.map(String);
}
const mapping: string[] = [];
const unresolved: string[] = [];
@@ -118,12 +118,12 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
`${label} entries must be full Matrix IDs (example: @user:server). Unresolved entries are ignored.`,
);
}
return allowList;
return allowList.map(String);
};
const allowlistOnly = cfg.channels?.matrix?.allowlistOnly === true;
let allowFrom = cfg.channels?.matrix?.dm?.allowFrom ?? [];
let groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
let allowFrom: string[] = (cfg.channels?.matrix?.dm?.allowFrom ?? []).map(String);
let groupAllowFrom: string[] = (cfg.channels?.matrix?.groupAllowFrom ?? []).map(String);
let roomsConfig = cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms;
allowFrom = await resolveUserAllowlist("matrix dm allowlist", allowFrom);
@@ -307,15 +307,16 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
if (auth.encryption && client.crypto) {
try {
// Request verification from other sessions
const verificationRequest = await client.crypto.requestOwnUserVerification();
const verificationRequest = await (
client.crypto as { requestOwnUserVerification?: () => Promise<unknown> }
).requestOwnUserVerification?.();
if (verificationRequest) {
logger.info("matrix: device verification requested - please verify in another client");
}
} catch (err) {
logger.debug(
{ error: String(err) },
"Device verification request failed (may already be verified)",
);
logger.debug?.("Device verification request failed (may already be verified)", {
error: String(err),
});
}
}

View File

@@ -29,7 +29,8 @@ async function fetchMatrixMediaBuffer(params: {
// Use the client's download method which handles auth
try {
const buffer = await params.client.downloadContent(params.mxcUrl);
const result = await params.client.downloadContent(params.mxcUrl);
const buffer = result.data;
if (buffer.byteLength > params.maxBytes) {
throw new Error("Matrix media exceeds configured size limit");
}
@@ -53,7 +54,9 @@ async function fetchEncryptedMediaBuffer(params: {
}
// decryptMedia handles downloading and decrypting the encrypted content internally
const decrypted = await params.client.crypto.decryptMedia(params.file);
const decrypted = await params.client.crypto.decryptMedia(
params.file as Parameters<typeof params.client.crypto.decryptMedia>[0],
);
if (decrypted.byteLength > params.maxBytes) {
throw new Error("Matrix media exceeds configured size limit");

View File

@@ -73,7 +73,7 @@ export type PollSummary = {
};
export function isPollStartType(eventType: string): boolean {
return POLL_START_TYPES.includes(eventType);
return (POLL_START_TYPES as readonly string[]).includes(eventType);
}
export function getTextContent(text?: TextContent): string {
@@ -147,7 +147,8 @@ export function buildPollStartContent(poll: PollInput): PollStartContent {
...buildTextContent(option),
}));
const maxSelections = poll.multiple ? Math.max(1, answers.length) : 1;
const isMultiple = (poll.maxSelections ?? 1) > 1;
const maxSelections = isMultiple ? Math.max(1, answers.length) : 1;
const fallbackText = buildPollFallbackText(
question,
answers.map((answer) => getTextContent(answer)),
@@ -156,7 +157,7 @@ export function buildPollStartContent(poll: PollInput): PollStartContent {
return {
[M_POLL_START]: {
question: buildTextContent(question),
kind: poll.multiple ? "m.poll.undisclosed" : "m.poll.disclosed",
kind: isMultiple ? "m.poll.undisclosed" : "m.poll.disclosed",
max_selections: maxSelections,
answers,
},

View File

@@ -1,5 +1,5 @@
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
import type { CoreConfig } from "../types.js";
import type { CoreConfig } from "../../types.js";
import { getMatrixRuntime } from "../../runtime.js";
import { getActiveMatrixClient } from "../active-client.js";
import {
@@ -55,7 +55,9 @@ export async function resolveMatrixClient(opts: {
if (auth.encryption && client.crypto) {
try {
const joinedRooms = await client.getJoinedRooms();
await client.crypto.prepare(joinedRooms);
await (client.crypto as { prepare: (rooms?: string[]) => Promise<void> }).prepare(
joinedRooms,
);
} catch {
// Ignore crypto prep failures for one-off sends; normal sync will retry.
}

View File

@@ -70,9 +70,12 @@ async function resolveDirectRoomId(client: MatrixClient, userId: string): Promis
// 1) Fast path: use account data (m.direct) for *this* logged-in user (the bot).
try {
const directContent = await client.getAccountData(EventType.Direct);
const directContent = (await client.getAccountData(EventType.Direct)) as Record<
string,
string[] | undefined
>;
const list = Array.isArray(directContent?.[trimmed]) ? directContent[trimmed] : [];
if (list.length > 0) {
if (list && list.length > 0) {
setDirectRoomCached(trimmed, list[0]);
return list[0];
}

View File

@@ -1,4 +1,5 @@
export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk";
import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk";
export type { DmPolicy, GroupPolicy };
export type ReplyToMode = "off" | "first" | "all";
@@ -92,6 +93,19 @@ export type MatrixConfig = {
export type CoreConfig = {
channels?: {
matrix?: MatrixConfig;
defaults?: {
groupPolicy?: "open" | "allowlist" | "disabled";
};
};
commands?: {
useAccessGroups?: boolean;
};
session?: {
store?: string;
};
messages?: {
ackReaction?: string;
ackReactionScope?: "group-mentions" | "group-all" | "direct" | "all";
};
[key: string]: unknown;
};

View File

@@ -1,4 +1,9 @@
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
type ProviderAuthContext,
type ProviderAuthResult,
} from "openclaw/plugin-sdk";
import { loginMiniMaxPortalOAuth, type MiniMaxRegion } from "./oauth.js";
const PROVIDER_ID = "minimax-portal";
@@ -38,8 +43,7 @@ function createOAuthHandler(region: MiniMaxRegion) {
const defaultBaseUrl = getDefaultBaseUrl(region);
const regionLabel = region === "cn" ? "CN" : "Global";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return async (ctx: any) => {
return async (ctx: ProviderAuthContext): Promise<ProviderAuthResult> => {
const progress = ctx.prompter.progress(`Starting MiniMax OAuth (${regionLabel})…`);
try {
const result = await loginMiniMaxPortalOAuth({
@@ -126,7 +130,7 @@ const minimaxPortalPlugin = {
name: "MiniMax OAuth",
description: "OAuth flow for MiniMax models",
configSchema: emptyPluginConfigSchema(),
register(api) {
register(api: OpenClawPluginApi) {
api.registerProvider({
id: PROVIDER_ID,
label: PROVIDER_LABEL,

View File

@@ -42,6 +42,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
id: "msteams",
meta: {
...meta,
aliases: [...meta.aliases],
},
onboarding: msteamsOnboardingAdapter,
pairing: {
@@ -384,7 +385,8 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
if (!to) {
return {
isError: true,
content: [{ type: "text", text: "Card send requires a target (to)." }],
content: [{ type: "text" as const, text: "Card send requires a target (to)." }],
details: { error: "Card send requires a target (to)." },
};
}
const result = await sendAdaptiveCardMSTeams({
@@ -395,7 +397,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
return {
content: [
{
type: "text",
type: "text" as const,
text: JSON.stringify({
ok: true,
channel: "msteams",
@@ -404,6 +406,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
}),
},
],
details: { ok: true, channel: "msteams", messageId: result.messageId },
};
}
// Return null to fall through to default handler

View File

@@ -1,4 +1,4 @@
import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk";
import type { ChannelDirectoryEntry, MSTeamsConfig } from "openclaw/plugin-sdk";
import { GRAPH_ROOT } from "./attachments/shared.js";
import { loadMSTeamsSdkWithAuth } from "./sdk.js";
import { resolveMSTeamsCredentials } from "./token.js";
@@ -62,7 +62,7 @@ async function fetchGraphJson<T>(params: {
async function resolveGraphToken(cfg: unknown): Promise<string> {
const creds = resolveMSTeamsCredentials(
(cfg as { channels?: { msteams?: unknown } })?.channels?.msteams,
(cfg as { channels?: { msteams?: unknown } })?.channels?.msteams as MSTeamsConfig | undefined,
);
if (!creds) {
throw new Error("MS Teams credentials missing");

View File

@@ -49,7 +49,7 @@ async function handleFileConsentInvoke(
const consentResponse = parseFileConsentInvoke(activity);
if (!consentResponse) {
log.debug("invalid file consent invoke", { value: activity.value });
log.debug?.("invalid file consent invoke", { value: activity.value });
return false;
}
@@ -61,7 +61,7 @@ async function handleFileConsentInvoke(
if (consentResponse.action === "accept" && consentResponse.uploadInfo) {
const pendingFile = getPendingUpload(uploadId);
if (pendingFile) {
log.debug("user accepted file consent, uploading", {
log.debug?.("user accepted file consent, uploading", {
uploadId,
filename: pendingFile.filename,
size: pendingFile.buffer.length,
@@ -94,20 +94,20 @@ async function handleFileConsentInvoke(
uniqueId: consentResponse.uploadInfo.uniqueId,
});
} catch (err) {
log.debug("file upload failed", { uploadId, error: String(err) });
log.debug?.("file upload failed", { uploadId, error: String(err) });
await context.sendActivity(`File upload failed: ${String(err)}`);
} finally {
removePendingUpload(uploadId);
}
} else {
log.debug("pending file not found for consent", { uploadId });
log.debug?.("pending file not found for consent", { uploadId });
await context.sendActivity(
"The file upload request has expired. Please try sending the file again.",
);
}
} else {
// User declined
log.debug("user declined file consent", { uploadId });
log.debug?.("user declined file consent", { uploadId });
removePendingUpload(uploadId);
}
@@ -151,7 +151,7 @@ export function registerMSTeamsHandlers<T extends MSTeamsActivityHandler>(
const membersAdded = (context as MSTeamsTurnContext).activity?.membersAdded ?? [];
for (const member of membersAdded) {
if (member.id !== (context as MSTeamsTurnContext).activity?.recipient?.id) {
deps.log.debug("member added", { member: member.id });
deps.log.debug?.("member added", { member: member.id });
// Don't send welcome message - let the user initiate conversation.
}
}

View File

@@ -10,7 +10,7 @@ import {
} from "../attachments.js";
type MSTeamsLogger = {
debug: (message: string, meta?: Record<string, unknown>) => void;
debug?: (message: string, meta?: Record<string, unknown>) => void;
};
export async function resolveMSTeamsInboundMedia(params: {
@@ -66,7 +66,7 @@ export async function resolveMSTeamsInboundMedia(params: {
channelData: activity.channelData,
});
if (messageUrls.length === 0) {
log.debug("graph message url unavailable", {
log.debug?.("graph message url unavailable", {
conversationType,
hasChannelData: Boolean(activity.channelData),
messageId: activity.id ?? undefined,
@@ -107,16 +107,16 @@ export async function resolveMSTeamsInboundMedia(params: {
}
}
if (mediaList.length === 0) {
log.debug("graph media fetch empty", { attempts });
log.debug?.("graph media fetch empty", { attempts });
}
}
}
}
if (mediaList.length > 0) {
log.debug("downloaded attachments", { count: mediaList.length });
log.debug?.("downloaded attachments", { count: mediaList.length });
} else if (htmlSummary?.imgTags) {
log.debug("inline images detected but none downloaded", {
log.debug?.("inline images detected but none downloaded", {
imgTags: htmlSummary.imgTags,
srcHosts: htmlSummary.srcHosts,
dataImages: htmlSummary.dataImages,

View File

@@ -54,7 +54,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
const core = getMSTeamsRuntime();
const logVerboseMessage = (message: string) => {
if (core.logging.shouldLogVerbose()) {
log.debug(message);
log.debug?.(message);
}
};
const msteamsCfg = cfg.channels?.msteams;
@@ -105,11 +105,11 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
conversation: conversation?.id,
});
if (htmlSummary) {
log.debug("html attachment summary", htmlSummary);
log.debug?.("html attachment summary", htmlSummary);
}
if (!from?.id) {
log.debug("skipping message without from.id");
log.debug?.("skipping message without from.id");
return;
}
@@ -137,7 +137,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
const allowFrom = dmAllowFrom;
if (dmPolicy === "disabled") {
log.debug("dropping dm (dms disabled)");
log.debug?.("dropping dm (dms disabled)");
return;
}
@@ -163,7 +163,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
});
}
}
log.debug("dropping dm (not allowlisted)", {
log.debug?.("dropping dm (not allowlisted)", {
sender: senderId,
label: senderName,
allowlistMatch: formatAllowlistMatchMeta(allowMatch),
@@ -200,7 +200,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
if (!isDirectMessage && msteamsCfg) {
if (groupPolicy === "disabled") {
log.debug("dropping group message (groupPolicy: disabled)", {
log.debug?.("dropping group message (groupPolicy: disabled)", {
conversationId,
});
return;
@@ -208,7 +208,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
if (groupPolicy === "allowlist") {
if (channelGate.allowlistConfigured && !channelGate.allowed) {
log.debug("dropping group message (not in team/channel allowlist)", {
log.debug?.("dropping group message (not in team/channel allowlist)", {
conversationId,
teamKey: channelGate.teamKey ?? "none",
channelKey: channelGate.channelKey ?? "none",
@@ -218,20 +218,19 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
return;
}
if (effectiveGroupAllowFrom.length === 0 && !channelGate.allowlistConfigured) {
log.debug("dropping group message (groupPolicy: allowlist, no allowlist)", {
log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", {
conversationId,
});
return;
}
if (effectiveGroupAllowFrom.length > 0) {
const allowMatch = resolveMSTeamsAllowlistMatch({
groupPolicy,
allowFrom: effectiveGroupAllowFrom,
senderId,
senderName,
});
if (!allowMatch.allowed) {
log.debug("dropping group message (not in groupAllowFrom)", {
log.debug?.("dropping group message (not in groupAllowFrom)", {
sender: senderId,
label: senderName,
allowlistMatch: formatAllowlistMatchMeta(allowMatch),
@@ -293,7 +292,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
locale: activity.locale,
};
conversationStore.upsert(conversationId, conversationRef).catch((err) => {
log.debug("failed to save conversation reference", {
log.debug?.("failed to save conversation reference", {
error: formatUnknownError(err),
});
});
@@ -307,7 +306,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
selections: pollVote.selections,
});
if (!poll) {
log.debug("poll vote ignored (poll not found)", {
log.debug?.("poll vote ignored (poll not found)", {
pollId: pollVote.pollId,
});
} else {
@@ -327,7 +326,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
}
if (!rawBody) {
log.debug("skipping empty message after stripping mentions");
log.debug?.("skipping empty message after stripping mentions");
return;
}
@@ -377,7 +376,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
});
const mentioned = mentionGate.effectiveWasMentioned;
if (requireMention && mentionGate.shouldSkip) {
log.debug("skipping message (mention required)", {
log.debug?.("skipping message (mention required)", {
teamId,
channelId,
requireMention,
@@ -413,7 +412,8 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
channelData: activity.channelData,
},
log,
preserveFilenames: cfg.media?.preserveFilenames,
preserveFilenames: (cfg as { media?: { preserveFilenames?: boolean } }).media
?.preserveFilenames,
});
const mediaPayload = buildMSTeamsMediaPayload(mediaList);

View File

@@ -1,5 +1,5 @@
export type MSTeamsMonitorLogger = {
debug: (message: string, meta?: Record<string, unknown>) => void;
debug?: (message: string, meta?: Record<string, unknown>) => void;
info: (message: string, meta?: Record<string, unknown>) => void;
error: (message: string, meta?: Record<string, unknown>) => void;
};

View File

@@ -9,7 +9,7 @@ import type { MSTeamsConversationStore } from "./conversation-store.js";
import type { MSTeamsAdapter } from "./messenger.js";
import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
import { formatUnknownError } from "./errors.js";
import { registerMSTeamsHandlers } from "./monitor-handler.js";
import { registerMSTeamsHandlers, type MSTeamsActivityHandler } from "./monitor-handler.js";
import { createMSTeamsPollStoreFs, type MSTeamsPollStore } from "./polls.js";
import {
resolveMSTeamsChannelAllowlist,
@@ -40,7 +40,7 @@ export async function monitorMSTeamsProvider(
let cfg = opts.cfg;
let msteamsCfg = cfg.channels?.msteams;
if (!msteamsCfg?.enabled) {
log.debug("msteams provider disabled");
log.debug?.("msteams provider disabled");
return { app: null, shutdown: async () => {} };
}
@@ -224,7 +224,7 @@ export async function monitorMSTeamsProvider(
const tokenProvider = new MsalTokenProvider(authConfig);
const adapter = createMSTeamsAdapter(authConfig, sdk);
const handler = registerMSTeamsHandlers(new ActivityHandler(), {
const handler = registerMSTeamsHandlers(new ActivityHandler() as MSTeamsActivityHandler, {
cfg,
runtime,
appId,
@@ -246,7 +246,7 @@ export async function monitorMSTeamsProvider(
const configuredPath = msteamsCfg.webhook?.path ?? "/api/messages";
const messageHandler = (req: Request, res: Response) => {
void adapter
.process(req, res, (context: unknown) => handler.run(context))
.process(req, res, (context: unknown) => handler.run!(context))
.catch((err: unknown) => {
log.error("msteams webhook failed", { error: formatUnknownError(err) });
});
@@ -258,7 +258,7 @@ export async function monitorMSTeamsProvider(
expressApp.post("/api/messages", messageHandler);
}
log.debug("listening on paths", {
log.debug?.("listening on paths", {
primary: configuredPath,
fallback: "/api/messages",
});
@@ -277,7 +277,7 @@ export async function monitorMSTeamsProvider(
return new Promise<void>((resolve) => {
httpServer.close((err) => {
if (err) {
log.debug("msteams server close error", { error: String(err) });
log.debug?.("msteams server close error", { error: String(err) });
}
resolve();
});

View File

@@ -4,6 +4,7 @@ import type {
OpenClawConfig,
DmPolicy,
WizardPrompter,
MSTeamsTeamConfig,
} from "openclaw/plugin-sdk";
import {
addWildcardAllowFrom,
@@ -184,7 +185,7 @@ function setMSTeamsTeamsAllowlist(
msteams: {
...cfg.channels?.msteams,
enabled: true,
teams,
teams: teams as Record<string, MSTeamsTeamConfig>,
},
},
};

View File

@@ -49,7 +49,7 @@ export function createMSTeamsReplyDispatcher(params: {
start: sendTypingIndicator,
onStartError: (err) => {
logTypingFailure({
log: (message) => params.log.debug(message),
log: (message) => params.log.debug?.(message),
channel: "msteams",
action: "start",
error: err,
@@ -94,7 +94,7 @@ export function createMSTeamsReplyDispatcher(params: {
// Enable default retry/backoff for throttling/transient failures.
retry: {},
onRetry: (event) => {
params.log.debug("retrying send", {
params.log.debug?.("retrying send", {
replyStyle: params.replyStyle,
...event,
});

View File

@@ -1,3 +1,4 @@
import type { MSTeamsConfig } from "openclaw/plugin-sdk";
import { GRAPH_ROOT } from "./attachments/shared.js";
import { loadMSTeamsSdkWithAuth } from "./sdk.js";
import { resolveMSTeamsCredentials } from "./token.js";
@@ -155,7 +156,7 @@ async function fetchGraphJson<T>(params: {
async function resolveGraphToken(cfg: unknown): Promise<string> {
const creds = resolveMSTeamsCredentials(
(cfg as { channels?: { msteams?: unknown } })?.channels?.msteams,
(cfg as { channels?: { msteams?: unknown } })?.channels?.msteams as MSTeamsConfig | undefined,
);
if (!creds) {
throw new Error("MS Teams credentials missing");

View File

@@ -111,7 +111,7 @@ export async function sendMessageMSTeams(
sharePointSiteId,
} = ctx;
log.debug("sending proactive message", {
log.debug?.("sending proactive message", {
conversationId,
conversationType,
textLength: messageText.length,
@@ -131,7 +131,7 @@ export async function sendMessageMSTeams(
const fallbackFileName = await extractFilename(mediaUrl);
const fileName = media.fileName ?? fallbackFileName;
log.debug("processing media", {
log.debug?.("processing media", {
fileName,
contentType: media.contentType,
size: media.buffer.length,
@@ -155,7 +155,7 @@ export async function sendMessageMSTeams(
description: messageText || undefined,
});
log.debug("sending file consent card", { uploadId, fileName, size: media.buffer.length });
log.debug?.("sending file consent card", { uploadId, fileName, size: media.buffer.length });
const baseRef = buildConversationReference(ref);
const proactiveRef = { ...baseRef, activityId: undefined };
@@ -205,7 +205,7 @@ export async function sendMessageMSTeams(
try {
if (sharePointSiteId) {
// Use SharePoint upload + Graph API for native file card
log.debug("uploading to SharePoint for native file card", {
log.debug?.("uploading to SharePoint for native file card", {
fileName,
conversationType,
siteId: sharePointSiteId,
@@ -221,7 +221,7 @@ export async function sendMessageMSTeams(
usePerUserSharing: conversationType === "groupChat",
});
log.debug("SharePoint upload complete", {
log.debug?.("SharePoint upload complete", {
itemId: uploaded.itemId,
shareUrl: uploaded.shareUrl,
});
@@ -233,7 +233,7 @@ export async function sendMessageMSTeams(
tokenProvider,
});
log.debug("driveItem properties retrieved", {
log.debug?.("driveItem properties retrieved", {
eTag: driveItem.eTag,
webDavUrl: driveItem.webDavUrl,
});
@@ -265,7 +265,7 @@ export async function sendMessageMSTeams(
}
// Fallback: no SharePoint site configured, use OneDrive with markdown link
log.debug("uploading to OneDrive (no SharePoint site configured)", {
log.debug?.("uploading to OneDrive (no SharePoint site configured)", {
fileName,
conversationType,
});
@@ -277,7 +277,7 @@ export async function sendMessageMSTeams(
tokenProvider,
});
log.debug("OneDrive upload complete", {
log.debug?.("OneDrive upload complete", {
itemId: uploaded.itemId,
shareUrl: uploaded.shareUrl,
});
@@ -349,7 +349,7 @@ async function sendTextWithMedia(
messages: [{ text: text || undefined, mediaUrl }],
retry: {},
onRetry: (event) => {
log.debug("retrying send", { conversationId, ...event });
log.debug?.("retrying send", { conversationId, ...event });
},
tokenProvider,
sharePointSiteId,
@@ -392,7 +392,7 @@ export async function sendPollMSTeams(
maxSelections,
});
log.debug("sending poll", {
log.debug?.("sending poll", {
conversationId,
pollId: pollCard.pollId,
optionCount: pollCard.options.length,
@@ -452,7 +452,7 @@ export async function sendAdaptiveCardMSTeams(
to,
});
log.debug("sending adaptive card", {
log.debug?.("sending adaptive card", {
conversationId,
cardType: card.type,
cardVersion: card.version,

View File

@@ -6,7 +6,7 @@ import {
type RuntimeEnv,
} from "openclaw/plugin-sdk";
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
import type { CoreConfig, NextcloudTalkInboundMessage } from "./types.js";
import type { CoreConfig, GroupPolicy, NextcloudTalkInboundMessage } from "./types.js";
import {
normalizeNextcloudTalkAllowlist,
resolveNextcloudTalkAllowlistMatch,
@@ -84,8 +84,12 @@ export async function handleNextcloudTalkInbound(params: {
statusSink?.({ lastInboundAt: message.timestamp });
const dmPolicy = account.config.dmPolicy ?? "pairing";
const defaultGroupPolicy = config.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const defaultGroupPolicy = (config.channels as Record<string, unknown> | undefined)?.defaults as
| { groupPolicy?: string }
| undefined;
const groupPolicy = (account.config.groupPolicy ??
defaultGroupPolicy?.groupPolicy ??
"allowlist") as GroupPolicy;
const configAllowFrom = normalizeNextcloudTalkAllowlist(account.config.allowFrom);
const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom);
@@ -118,7 +122,8 @@ export async function handleNextcloudTalkInbound(params: {
cfg: config as OpenClawConfig,
surface: CHANNEL_ID,
});
const useAccessGroups = config.commands?.useAccessGroups !== false;
const useAccessGroups =
(config.commands as Record<string, unknown> | undefined)?.useAccessGroups !== false;
const senderAllowedForCommands = resolveNextcloudTalkAllowlistMatch({
allowFrom: isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom,
senderId,
@@ -234,9 +239,12 @@ export async function handleNextcloudTalkInbound(params: {
});
const fromLabel = isGroup ? `room:${roomName || roomToken}` : senderName || `user:${senderId}`;
const storePath = core.channel.session.resolveStorePath(config.session?.store, {
agentId: route.agentId,
});
const storePath = core.channel.session.resolveStorePath(
(config.session as Record<string, unknown> | undefined)?.store as string | undefined,
{
agentId: route.agentId,
},
);
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config as OpenClawConfig);
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
storePath,

View File

@@ -6,6 +6,7 @@ import {
normalizeAccountId,
type ChannelOnboardingAdapter,
type ChannelOnboardingDmPolicy,
type OpenClawConfig,
type WizardPrompter,
} from "openclaw/plugin-sdk";
import type { CoreConfig, DmPolicy } from "./types.js";
@@ -159,7 +160,11 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
allowFromKey: "channels.nextcloud-talk.allowFrom",
getCurrent: (cfg) => cfg.channels?.["nextcloud-talk"]?.dmPolicy ?? "pairing",
setPolicy: (cfg, policy) => setNextcloudTalkDmPolicy(cfg as CoreConfig, policy as DmPolicy),
promptAllowFrom: promptNextcloudTalkAllowFromForAccount,
promptAllowFrom: promptNextcloudTalkAllowFromForAccount as (params: {
cfg: OpenClawConfig;
prompter: WizardPrompter;
accountId?: string | undefined;
}) => Promise<OpenClawConfig>,
};
export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
@@ -196,7 +201,7 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
prompter,
label: "Nextcloud Talk",
currentId: accountId,
listAccountIds: listNextcloudTalkAccountIds,
listAccountIds: listNextcloudTalkAccountIds as (cfg: OpenClawConfig) => string[],
defaultAccountId,
});
}

View File

@@ -5,6 +5,8 @@ import type {
GroupPolicy,
} from "openclaw/plugin-sdk";
export type { DmPolicy, GroupPolicy };
export type NextcloudTalkRoomConfig = {
requireMention?: boolean;
/** Optional tool policy overrides for this room. */

View File

@@ -148,7 +148,11 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
const message = core.channel.text.convertMarkdownTables(text ?? "", tableMode);
const normalizedTo = normalizePubkey(to);
await bus.sendDm(normalizedTo, message);
return { channel: "nostr", to: normalizedTo };
return {
channel: "nostr" as const,
to: normalizedTo,
messageId: `nostr-${Date.now()}`,
};
},
},
@@ -224,10 +228,15 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
privateKey: account.privateKey,
relays: account.relays,
onMessage: async (senderPubkey, text, reply) => {
ctx.log?.debug(`[${account.accountId}] DM from ${senderPubkey}: ${text.slice(0, 50)}...`);
ctx.log?.debug?.(
`[${account.accountId}] DM from ${senderPubkey}: ${text.slice(0, 50)}...`,
);
// Forward to OpenClaw's message pipeline
await runtime.channel.reply.handleInboundMessage({
// TODO: Replace with proper dispatchReplyWithBufferedBlockDispatcher call
await (
runtime.channel.reply as { handleInboundMessage?: (params: unknown) => Promise<void> }
).handleInboundMessage?.({
channel: "nostr",
accountId: account.accountId,
senderId: senderPubkey,
@@ -240,31 +249,33 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
});
},
onError: (error, context) => {
ctx.log?.error(`[${account.accountId}] Nostr error (${context}): ${error.message}`);
ctx.log?.error?.(`[${account.accountId}] Nostr error (${context}): ${error.message}`);
},
onConnect: (relay) => {
ctx.log?.debug(`[${account.accountId}] Connected to relay: ${relay}`);
ctx.log?.debug?.(`[${account.accountId}] Connected to relay: ${relay}`);
},
onDisconnect: (relay) => {
ctx.log?.debug(`[${account.accountId}] Disconnected from relay: ${relay}`);
ctx.log?.debug?.(`[${account.accountId}] Disconnected from relay: ${relay}`);
},
onEose: (relays) => {
ctx.log?.debug(`[${account.accountId}] EOSE received from relays: ${relays}`);
ctx.log?.debug?.(`[${account.accountId}] EOSE received from relays: ${relays}`);
},
onMetric: (event: MetricEvent) => {
// Log significant metrics at appropriate levels
if (event.name.startsWith("event.rejected.")) {
ctx.log?.debug(`[${account.accountId}] Metric: ${event.name}`, event.labels);
ctx.log?.debug?.(
`[${account.accountId}] Metric: ${event.name} ${JSON.stringify(event.labels)}`,
);
} else if (event.name === "relay.circuit_breaker.open") {
ctx.log?.warn(
ctx.log?.warn?.(
`[${account.accountId}] Circuit breaker opened for relay: ${event.labels?.relay}`,
);
} else if (event.name === "relay.circuit_breaker.close") {
ctx.log?.info(
ctx.log?.info?.(
`[${account.accountId}] Circuit breaker closed for relay: ${event.labels?.relay}`,
);
} else if (event.name === "relay.error") {
ctx.log?.debug(`[${account.accountId}] Relay error: ${event.labels?.relay}`);
ctx.log?.debug?.(`[${account.accountId}] Relay error: ${event.labels?.relay}`);
}
// Update cached metrics snapshot
if (busHandle) {

View File

@@ -488,24 +488,28 @@ export async function startNostrBus(options: NostrBusOptions): Promise<NostrBusH
}
}
const sub = pool.subscribeMany(relays, [{ kinds: [4], "#p": [pk], since }], {
onevent: handleEvent,
oneose: () => {
// EOSE handler - called when all stored events have been received
for (const relay of relays) {
metrics.emit("relay.message.eose", 1, { relay });
}
onEose?.(relays.join(", "));
const sub = pool.subscribeMany(
relays,
[{ kinds: [4], "#p": [pk], since }] as unknown as Parameters<typeof pool.subscribeMany>[1],
{
onevent: handleEvent,
oneose: () => {
// EOSE handler - called when all stored events have been received
for (const relay of relays) {
metrics.emit("relay.message.eose", 1, { relay });
}
onEose?.(relays.join(", "));
},
onclose: (reason) => {
// Handle subscription close
for (const relay of relays) {
metrics.emit("relay.message.closed", 1, { relay });
options.onDisconnect?.(relay);
}
onError?.(new Error(`Subscription closed: ${reason.join(", ")}`), "subscription");
},
},
onclose: (reason) => {
// Handle subscription close
for (const relay of relays) {
metrics.emit("relay.message.closed", 1, { relay });
options.onDisconnect?.(relay);
}
onError?.(new Error(`Subscription closed: ${reason.join(", ")}`), "subscription");
},
});
);
// Public sendDm function
const sendDm = async (toPubkey: string, text: string): Promise<void> => {
@@ -693,7 +697,7 @@ export function normalizePubkey(input: string): string {
throw new Error("Invalid npub key");
}
// Convert Uint8Array to hex string
return Array.from(decoded.data)
return Array.from(decoded.data as unknown as Uint8Array)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}

View File

@@ -130,7 +130,7 @@ export async function importProfileFromRelays(
authors: [pubkey],
limit: 1,
},
],
] as unknown as Parameters<typeof pool.subscribeMany>[1],
{
onevent(event) {
events.push({ event, relay });

View File

@@ -92,7 +92,8 @@ function resolveStatePath(stateDir: string): string {
async function readArmState(statePath: string): Promise<ArmStateFile | null> {
try {
const raw = await fs.readFile(statePath, "utf8");
const parsed = JSON.parse(raw) as Partial<ArmStateFile>;
// Type as unknown record first to allow property access during validation
const parsed = JSON.parse(raw) as Record<string, unknown>;
if (parsed.version !== 1 && parsed.version !== 2) {
return null;
}
@@ -106,11 +107,11 @@ async function readArmState(statePath: string): Promise<ArmStateFile | null> {
if (parsed.version === 1) {
if (
!Array.isArray(parsed.removedFromDeny) ||
!parsed.removedFromDeny.every((v) => typeof v === "string")
!parsed.removedFromDeny.every((v: unknown) => typeof v === "string")
) {
return null;
}
return parsed as ArmStateFile;
return parsed as unknown as ArmStateFile;
}
const group = typeof parsed.group === "string" ? parsed.group : "";
@@ -119,23 +120,23 @@ async function readArmState(statePath: string): Promise<ArmStateFile | null> {
}
if (
!Array.isArray(parsed.armedCommands) ||
!parsed.armedCommands.every((v) => typeof v === "string")
!parsed.armedCommands.every((v: unknown) => typeof v === "string")
) {
return null;
}
if (
!Array.isArray(parsed.addedToAllow) ||
!parsed.addedToAllow.every((v) => typeof v === "string")
!parsed.addedToAllow.every((v: unknown) => typeof v === "string")
) {
return null;
}
if (
!Array.isArray(parsed.removedFromDeny) ||
!parsed.removedFromDeny.every((v) => typeof v === "string")
!parsed.removedFromDeny.every((v: unknown) => typeof v === "string")
) {
return null;
}
return parsed as ArmStateFile;
return parsed as unknown as ArmStateFile;
} catch {
return null;
}

View File

@@ -1,4 +1,8 @@
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
type ProviderAuthContext,
} from "openclaw/plugin-sdk";
import { loginQwenPortalOAuth } from "./oauth.js";
const PROVIDER_ID = "qwen-portal";
@@ -36,7 +40,7 @@ const qwenPortalPlugin = {
name: "Qwen OAuth",
description: "OAuth flow for Qwen (free-tier) models",
configSchema: emptyPluginConfigSchema(),
register(api) {
register(api: OpenClawPluginApi) {
api.registerProvider({
id: PROVIDER_ID,
label: PROVIDER_LABEL,
@@ -48,7 +52,7 @@ const qwenPortalPlugin = {
label: "Qwen OAuth",
hint: "Device code login",
kind: "device_code",
run: async (ctx) => {
run: async (ctx: ProviderAuthContext) => {
const progress = ctx.prompter.progress("Starting Qwen OAuth…");
try {
const result = await loginQwenPortalOAuth({

View File

@@ -25,10 +25,16 @@ import {
import { getSignalRuntime } from "./runtime.js";
const signalMessageActions: ChannelMessageActionAdapter = {
listActions: (ctx) => getSignalRuntime().channel.signal.messageActions.listActions(ctx),
supportsAction: (ctx) => getSignalRuntime().channel.signal.messageActions.supportsAction?.(ctx),
handleAction: async (ctx) =>
await getSignalRuntime().channel.signal.messageActions.handleAction(ctx),
listActions: (ctx) => getSignalRuntime().channel.signal.messageActions?.listActions?.(ctx) ?? [],
supportsAction: (ctx) =>
getSignalRuntime().channel.signal.messageActions?.supportsAction?.(ctx) ?? false,
handleAction: async (ctx) => {
const ma = getSignalRuntime().channel.signal.messageActions;
if (!ma?.handleAction) {
throw new Error("Signal message actions not available");
}
return ma.handleAction(ctx);
},
};
const meta = getChatChannelMeta("signal");

View File

@@ -1,4 +1,4 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import { telegramPlugin } from "./src/channel.js";
import { setTelegramRuntime } from "./src/runtime.js";
@@ -10,7 +10,7 @@ const plugin = {
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
setTelegramRuntime(api.runtime);
api.registerChannel({ plugin: telegramPlugin });
api.registerChannel({ plugin: telegramPlugin as ChannelPlugin });
},
};

View File

@@ -32,11 +32,17 @@ import { getTelegramRuntime } from "./runtime.js";
const meta = getChatChannelMeta("telegram");
const telegramMessageActions: ChannelMessageActionAdapter = {
listActions: (ctx) => getTelegramRuntime().channel.telegram.messageActions.listActions(ctx),
listActions: (ctx) =>
getTelegramRuntime().channel.telegram.messageActions?.listActions?.(ctx) ?? [],
extractToolSend: (ctx) =>
getTelegramRuntime().channel.telegram.messageActions.extractToolSend(ctx),
handleAction: async (ctx) =>
await getTelegramRuntime().channel.telegram.messageActions.handleAction(ctx),
getTelegramRuntime().channel.telegram.messageActions?.extractToolSend?.(ctx) ?? null,
handleAction: async (ctx) => {
const ma = getTelegramRuntime().channel.telegram.messageActions;
if (!ma?.handleAction) {
throw new Error("Telegram message actions not available");
}
return ma.handleAction(ctx);
},
};
function parseReplyToMessageId(replyToId?: string | null) {

View File

@@ -1,4 +1,5 @@
import type {
ChannelAccountSnapshot,
ChannelOutboundAdapter,
ChannelPlugin,
ChannelSetupInput,
@@ -154,7 +155,7 @@ const tlonOutbound: ChannelOutboundAdapter = {
},
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId, threadId }) => {
const mergedText = buildMediaText(text, mediaUrl);
return await tlonOutbound.sendText({
return await tlonOutbound.sendText!({
cfg,
to,
text: mergedText,
@@ -224,9 +225,11 @@ export const tlonPlugin: ChannelPlugin = {
deleteAccount: ({ cfg, accountId }) => {
const useDefault = !accountId || accountId === "default";
if (useDefault) {
// @ts-expect-error
// oxlint-disable-next-line no-unused-vars
const { ship, code, url, name, ...rest } = cfg.channels?.tlon ?? {};
const { ship, code, url, name, ...rest } = (cfg.channels?.tlon ?? {}) as Record<
string,
unknown
>;
return {
...cfg,
channels: {
@@ -235,9 +238,9 @@ export const tlonPlugin: ChannelPlugin = {
},
} as OpenClawConfig;
}
// @ts-expect-error
// oxlint-disable-next-line no-unused-vars
const { [accountId]: removed, ...remainingAccounts } = cfg.channels?.tlon?.accounts ?? {};
const { [accountId]: removed, ...remainingAccounts } = (cfg.channels?.tlon?.accounts ??
{}) as Record<string, unknown>;
return {
...cfg,
channels: {
@@ -334,8 +337,8 @@ export const tlonPlugin: ChannelPlugin = {
},
buildChannelSummary: ({ snapshot }) => ({
configured: snapshot.configured ?? false,
ship: snapshot.ship ?? null,
url: snapshot.url ?? null,
ship: (snapshot as { ship?: string | null }).ship ?? null,
url: (snapshot as { url?: string | null }).url ?? null,
}),
probeAccount: async ({ account }) => {
if (!account.configured || !account.ship || !account.url || !account.code) {
@@ -356,7 +359,7 @@ export const tlonPlugin: ChannelPlugin = {
await api.delete();
}
} catch (error) {
return { ok: false, error: error?.message ?? String(error) };
return { ok: false, error: (error as { message?: string })?.message ?? String(error) };
}
},
buildAccountSnapshot: ({ account, runtime, probe }) => ({
@@ -380,7 +383,7 @@ export const tlonPlugin: ChannelPlugin = {
accountId: account.accountId,
ship: account.ship,
url: account.url,
});
} as ChannelAccountSnapshot);
ctx.log?.info(`[${account.accountId}] starting Tlon provider for ${account.ship ?? "tlon"}`);
return monitorTlonProvider({
runtime: ctx.runtime,

View File

@@ -17,7 +17,7 @@ export async function fetchGroupChanges(
return null;
} catch (error) {
runtime.log?.(
`[tlon] Failed to fetch changes (falling back to full init): ${error?.message ?? String(error)}`,
`[tlon] Failed to fetch changes (falling back to full init): ${(error as { message?: string })?.message ?? String(error)}`,
);
return null;
}
@@ -66,7 +66,9 @@ export async function fetchAllChannels(
return channels;
} catch (error) {
runtime.log?.(`[tlon] Auto-discovery failed: ${error?.message ?? String(error)}`);
runtime.log?.(
`[tlon] Auto-discovery failed: ${(error as { message?: string })?.message ?? String(error)}`,
);
runtime.log?.(
"[tlon] To monitor group channels, add them to config: channels.tlon.groupChannels",
);

View File

@@ -68,7 +68,9 @@ export async function fetchChannelHistory(
runtime?.log?.(`[tlon] Extracted ${messages.length} messages from history`);
return messages;
} catch (error) {
runtime?.log?.(`[tlon] Error fetching channel history: ${error?.message ?? String(error)}`);
runtime?.log?.(
`[tlon] Error fetching channel history: ${(error as { message?: string })?.message ?? String(error)}`,
);
return [];
}
}

View File

@@ -18,6 +18,11 @@ import {
isSummarizationRequest,
} from "./utils.js";
function formatError(err: unknown): string {
if (err instanceof Error) return err.message;
return String(err);
}
export type MonitorTlonOpts = {
runtime?: RuntimeEnv;
abortSignal?: AbortSignal;
@@ -35,6 +40,11 @@ type UrbitMemo = {
sent?: number;
};
type UrbitSeal = {
"parent-id"?: string;
parent?: string;
};
type UrbitUpdate = {
id?: string | number;
response?: {
@@ -42,10 +52,10 @@ type UrbitUpdate = {
post?: {
id?: string | number;
"r-post"?: {
set?: { essay?: UrbitMemo };
set?: { essay?: UrbitMemo; seal?: UrbitSeal };
reply?: {
id?: string | number;
"r-reply"?: { set?: { memo?: UrbitMemo } };
"r-reply"?: { set?: { memo?: UrbitMemo; seal?: UrbitSeal } };
};
};
};
@@ -113,7 +123,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
},
});
} catch (error) {
runtime.error?.(`[tlon] Failed to authenticate: ${error?.message ?? String(error)}`);
runtime.error?.(`[tlon] Failed to authenticate: ${formatError(error)}`);
throw error;
}
@@ -127,7 +137,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
groupChannels = discoveredChannels;
}
} catch (error) {
runtime.error?.(`[tlon] Auto-discovery failed: ${error?.message ?? String(error)}`);
runtime.error?.(`[tlon] Auto-discovery failed: ${formatError(error)}`);
}
}
@@ -179,7 +189,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
timestamp: memo.sent || Date.now(),
});
} catch (error) {
runtime.error?.(`[tlon] Error handling DM: ${error?.message ?? String(error)}`);
runtime.error?.(`[tlon] Error handling DM: ${formatError(error)}`);
}
};
@@ -198,6 +208,9 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
}
const content = memo || essay;
if (!content) {
return;
}
const isThreadReply = Boolean(memo);
const rawMessageId = isThreadReply ? post?.reply?.id : update?.response?.post?.id;
const messageId = rawMessageId != null ? String(rawMessageId) : undefined;
@@ -260,7 +273,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
parentId,
});
} catch (error) {
runtime.error?.(`[tlon] Error handling group message: ${error?.message ?? String(error)}`);
runtime.error?.(`[tlon] Error handling group message: ${formatError(error)}`);
}
};
@@ -319,7 +332,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
"3. Action items if any\n" +
"4. Notable participants";
} catch (error) {
const errorMsg = `Sorry, I encountered an error while fetching the channel history: ${error?.message ?? String(error)}`;
const errorMsg = `Sorry, I encountered an error while fetching the channel history: ${formatError(error)}`;
if (isGroup && groupChannel) {
const parsed = parseChannelNest(groupChannel);
if (parsed) {
@@ -400,10 +413,15 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
const showSignature =
account.showModelSignature ?? cfg.channels?.tlon?.showModelSignature ?? false;
if (showSignature) {
const extPayload = payload as ReplyPayload & {
metadata?: { model?: string };
model?: string;
};
const extRoute = route as typeof route & { model?: string };
const modelInfo =
payload.metadata?.model ||
payload.model ||
route.model ||
extPayload.metadata?.model ||
extPayload.model ||
extRoute.model ||
cfg.agents?.defaults?.model?.primary;
replyText = `${replyText}\n\n_[Generated by ${formatModelName(modelInfo)}]_`;
}
@@ -455,7 +473,9 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
await api!.subscribe({
app: "channels",
path: `/${channelNest}`,
event: handleIncomingGroupMessage(channelNest),
event: (data: unknown) => {
handleIncomingGroupMessage(channelNest)(data as UrbitUpdate);
},
err: (error) => {
runtime.error?.(`[tlon] Group subscription error for ${channelNest}: ${String(error)}`);
},
@@ -467,9 +487,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
subscribedChannels.add(channelNest);
runtime.log?.(`[tlon] Subscribed to group channel: ${channelNest}`);
} catch (error) {
runtime.error?.(
`[tlon] Failed to subscribe to ${channelNest}: ${error?.message ?? String(error)}`,
);
runtime.error?.(`[tlon] Failed to subscribe to ${channelNest}: ${formatError(error)}`);
}
}
@@ -481,7 +499,9 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
await api!.subscribe({
app: "chat",
path: `/dm/${dmShip}`,
event: handleIncomingDM,
event: (data: unknown) => {
handleIncomingDM(data as UrbitUpdate);
},
err: (error) => {
runtime.error?.(`[tlon] DM subscription error for ${dmShip}: ${String(error)}`);
},
@@ -493,9 +513,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
subscribedDMs.add(dmShip);
runtime.log?.(`[tlon] Subscribed to DM with ${dmShip}`);
} catch (error) {
runtime.error?.(
`[tlon] Failed to subscribe to DM with ${dmShip}: ${error?.message ?? String(error)}`,
);
runtime.error?.(`[tlon] Failed to subscribe to DM with ${dmShip}: ${formatError(error)}`);
}
}
@@ -515,7 +533,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
}
}
} catch (error) {
runtime.error?.(`[tlon] Channel refresh failed: ${error?.message ?? String(error)}`);
runtime.error?.(`[tlon] Channel refresh failed: ${formatError(error)}`);
}
}
@@ -530,7 +548,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
runtime.log?.(`[tlon] Found ${dmShips.length} DM conversation(s)`);
}
} catch (error) {
runtime.error?.(`[tlon] Failed to fetch DM list: ${error?.message ?? String(error)}`);
runtime.error?.(`[tlon] Failed to fetch DM list: ${formatError(error)}`);
}
for (const dmShip of dmShips) {
@@ -549,7 +567,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
() => {
if (!opts.abortSignal?.aborted) {
refreshChannelSubscriptions().catch((error) => {
runtime.error?.(`[tlon] Channel refresh error: ${error?.message ?? String(error)}`);
runtime.error?.(`[tlon] Channel refresh error: ${formatError(error)}`);
});
}
},
@@ -557,8 +575,9 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
);
if (opts.abortSignal) {
const signal = opts.abortSignal;
await new Promise((resolve) => {
opts.abortSignal.addEventListener(
signal.addEventListener(
"abort",
() => {
clearInterval(pollInterval);
@@ -574,7 +593,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
try {
await api?.close();
} catch (error) {
runtime.error?.(`[tlon] Cleanup error: ${error?.message ?? String(error)}`);
runtime.error?.(`[tlon] Cleanup error: ${formatError(error)}`);
}
}
}

View File

@@ -67,7 +67,7 @@ export async function sendGroupMessage({
let formattedReplyId = replyToId;
if (replyToId && /^\d+$/.test(replyToId)) {
try {
formattedReplyId = formatUd(BigInt(replyToId));
formattedReplyId = scot("ud", BigInt(replyToId));
} catch {
// Fall back to raw ID if formatting fails
}

View File

@@ -204,7 +204,8 @@ export class UrbitSSEClient {
if (!body) {
return;
}
const stream = body instanceof ReadableStream ? Readable.fromWeb(body) : body;
// oxlint-disable-next-line typescript/no-explicit-any
const stream = body instanceof ReadableStream ? Readable.fromWeb(body as any) : body;
let buffer = "";
try {

View File

@@ -15,7 +15,7 @@ function errorResponse(error: string) {
return {
content: [
{
type: "text",
type: "text" as const,
text: JSON.stringify({ ok: false, error }),
},
],
@@ -120,11 +120,12 @@ export const twitchMessageActions: ChannelMessageActionAdapter = {
* accountId: "default",
* });
*/
handleAction: async (
ctx: ChannelMessageActionContext,
): Promise<{ content: Array<{ type: string; text: string }> } | null> => {
handleAction: async (ctx: ChannelMessageActionContext) => {
if (ctx.action !== "send") {
return null;
return {
content: [{ type: "text" as const, text: "Unsupported action" }],
details: { ok: false, error: "Unsupported action" },
};
}
const message = readStringParam(ctx.params, "message", { required: true });
@@ -159,7 +160,7 @@ export const twitchMessageActions: ChannelMessageActionAdapter = {
return {
content: [
{
type: "text",
type: "text" as const,
text: JSON.stringify(result),
},
],

View File

@@ -104,7 +104,8 @@ export const twitchOutbound: ChannelOutboundAdapter = {
* });
*/
sendText: async (params: ChannelOutboundContext): Promise<OutboundDeliveryResult> => {
const { cfg, to, text, accountId, signal } = params;
const { cfg, to, text, accountId } = params;
const signal = (params as { signal?: AbortSignal }).signal;
if (signal?.aborted) {
throw new Error("Outbound delivery aborted");
@@ -142,7 +143,6 @@ export const twitchOutbound: ChannelOutboundAdapter = {
channel: "twitch",
messageId: result.messageId,
timestamp: Date.now(),
to: normalizeTwitchChannel(channel),
};
},
@@ -165,7 +165,8 @@ export const twitchOutbound: ChannelOutboundAdapter = {
* });
*/
sendMedia: async (params: ChannelOutboundContext): Promise<OutboundDeliveryResult> => {
const { text, mediaUrl, signal } = params;
const { text, mediaUrl } = params;
const signal = (params as { signal?: AbortSignal }).signal;
if (signal?.aborted) {
throw new Error("Outbound delivery aborted");

View File

@@ -27,16 +27,16 @@ export async function probeTwitch(
): Promise<ProbeTwitchResult> {
const started = Date.now();
if (!account.token || !account.username) {
if (!account.accessToken || !account.username) {
return {
ok: false,
error: "missing credentials (token, username)",
error: "missing credentials (accessToken, username)",
username: account.username,
elapsedMs: Date.now() - started,
};
}
const rawToken = normalizeToken(account.token.trim());
const rawToken = normalizeToken(account.accessToken.trim());
let client: ChatClient | undefined;

View File

@@ -51,8 +51,8 @@ export async function resolveTwitchTargets(
): Promise<ChannelResolveResult[]> {
const log = createLogger(logger);
if (!account.clientId || !account.token) {
log.error("Missing Twitch client ID or token");
if (!account.clientId || !account.accessToken) {
log.error("Missing Twitch client ID or accessToken");
return inputs.map((input) => ({
input,
resolved: false,
@@ -60,7 +60,7 @@ export async function resolveTwitchTargets(
}));
}
const normalizedToken = normalizeToken(account.token);
const normalizedToken = normalizeToken(account.accessToken);
const authProvider = new StaticAuthProvider(account.clientId, normalizedToken);
const apiClient = new ApiClient({ authProvider });

View File

@@ -4,7 +4,8 @@
* Detects and reports configuration issues for Twitch accounts.
*/
import type { ChannelAccountSnapshot, ChannelStatusIssue } from "./types.js";
import type { ChannelStatusIssue } from "openclaw/plugin-sdk";
import type { ChannelAccountSnapshot } from "./types.js";
import { getAccountConfig } from "./config.js";
import { resolveTwitchToken } from "./token.js";
import { isAccountConfigured } from "./utils/twitch.js";

View File

@@ -1,3 +1,4 @@
import type { GatewayRequestHandlerOptions, OpenClawPluginApi } from "openclaw/plugin-sdk";
import { Type } from "@sinclair/typebox";
import type { CoreConfig } from "./src/core-bridge.js";
import { registerVoiceCallCli } from "./src/cli.js";
@@ -144,7 +145,7 @@ const voiceCallPlugin = {
name: "Voice Call",
description: "Voice-call plugin with Telnyx/Twilio/Plivo providers",
configSchema: voiceCallConfigSchema,
register(api) {
register(api: OpenClawPluginApi) {
const config = resolveVoiceCallConfig(voiceCallConfigSchema.parse(api.pluginConfig));
const validation = validateProviderConfig(config);
@@ -188,142 +189,160 @@ const voiceCallPlugin = {
respond(false, { error: err instanceof Error ? err.message : String(err) });
};
api.registerGatewayMethod("voicecall.initiate", async ({ params, respond }) => {
try {
const message = typeof params?.message === "string" ? params.message.trim() : "";
if (!message) {
respond(false, { error: "message required" });
return;
api.registerGatewayMethod(
"voicecall.initiate",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
const message = typeof params?.message === "string" ? params.message.trim() : "";
if (!message) {
respond(false, { error: "message required" });
return;
}
const rt = await ensureRuntime();
const to =
typeof params?.to === "string" && params.to.trim()
? params.to.trim()
: rt.config.toNumber;
if (!to) {
respond(false, { error: "to required" });
return;
}
const mode =
params?.mode === "notify" || params?.mode === "conversation" ? params.mode : undefined;
const result = await rt.manager.initiateCall(to, undefined, {
message,
mode,
});
if (!result.success) {
respond(false, { error: result.error || "initiate failed" });
return;
}
respond(true, { callId: result.callId, initiated: true });
} catch (err) {
sendError(respond, err);
}
const rt = await ensureRuntime();
const to =
typeof params?.to === "string" && params.to.trim()
? params.to.trim()
: rt.config.toNumber;
if (!to) {
respond(false, { error: "to required" });
return;
}
const mode =
params?.mode === "notify" || params?.mode === "conversation" ? params.mode : undefined;
const result = await rt.manager.initiateCall(to, undefined, {
message,
mode,
});
if (!result.success) {
respond(false, { error: result.error || "initiate failed" });
return;
}
respond(true, { callId: result.callId, initiated: true });
} catch (err) {
sendError(respond, err);
}
});
},
);
api.registerGatewayMethod("voicecall.continue", async ({ params, respond }) => {
try {
const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
const message = typeof params?.message === "string" ? params.message.trim() : "";
if (!callId || !message) {
respond(false, { error: "callId and message required" });
return;
api.registerGatewayMethod(
"voicecall.continue",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
const message = typeof params?.message === "string" ? params.message.trim() : "";
if (!callId || !message) {
respond(false, { error: "callId and message required" });
return;
}
const rt = await ensureRuntime();
const result = await rt.manager.continueCall(callId, message);
if (!result.success) {
respond(false, { error: result.error || "continue failed" });
return;
}
respond(true, { success: true, transcript: result.transcript });
} catch (err) {
sendError(respond, err);
}
const rt = await ensureRuntime();
const result = await rt.manager.continueCall(callId, message);
if (!result.success) {
respond(false, { error: result.error || "continue failed" });
return;
}
respond(true, { success: true, transcript: result.transcript });
} catch (err) {
sendError(respond, err);
}
});
},
);
api.registerGatewayMethod("voicecall.speak", async ({ params, respond }) => {
try {
const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
const message = typeof params?.message === "string" ? params.message.trim() : "";
if (!callId || !message) {
respond(false, { error: "callId and message required" });
return;
api.registerGatewayMethod(
"voicecall.speak",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
const message = typeof params?.message === "string" ? params.message.trim() : "";
if (!callId || !message) {
respond(false, { error: "callId and message required" });
return;
}
const rt = await ensureRuntime();
const result = await rt.manager.speak(callId, message);
if (!result.success) {
respond(false, { error: result.error || "speak failed" });
return;
}
respond(true, { success: true });
} catch (err) {
sendError(respond, err);
}
const rt = await ensureRuntime();
const result = await rt.manager.speak(callId, message);
if (!result.success) {
respond(false, { error: result.error || "speak failed" });
return;
}
respond(true, { success: true });
} catch (err) {
sendError(respond, err);
}
});
},
);
api.registerGatewayMethod("voicecall.end", async ({ params, respond }) => {
try {
const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
if (!callId) {
respond(false, { error: "callId required" });
return;
api.registerGatewayMethod(
"voicecall.end",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
if (!callId) {
respond(false, { error: "callId required" });
return;
}
const rt = await ensureRuntime();
const result = await rt.manager.endCall(callId);
if (!result.success) {
respond(false, { error: result.error || "end failed" });
return;
}
respond(true, { success: true });
} catch (err) {
sendError(respond, err);
}
const rt = await ensureRuntime();
const result = await rt.manager.endCall(callId);
if (!result.success) {
respond(false, { error: result.error || "end failed" });
return;
}
respond(true, { success: true });
} catch (err) {
sendError(respond, err);
}
});
},
);
api.registerGatewayMethod("voicecall.status", async ({ params, respond }) => {
try {
const raw =
typeof params?.callId === "string"
? params.callId.trim()
: typeof params?.sid === "string"
? params.sid.trim()
: "";
if (!raw) {
respond(false, { error: "callId required" });
return;
api.registerGatewayMethod(
"voicecall.status",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
const raw =
typeof params?.callId === "string"
? params.callId.trim()
: typeof params?.sid === "string"
? params.sid.trim()
: "";
if (!raw) {
respond(false, { error: "callId required" });
return;
}
const rt = await ensureRuntime();
const call = rt.manager.getCall(raw) || rt.manager.getCallByProviderCallId(raw);
if (!call) {
respond(true, { found: false });
return;
}
respond(true, { found: true, call });
} catch (err) {
sendError(respond, err);
}
const rt = await ensureRuntime();
const call = rt.manager.getCall(raw) || rt.manager.getCallByProviderCallId(raw);
if (!call) {
respond(true, { found: false });
return;
}
respond(true, { found: true, call });
} catch (err) {
sendError(respond, err);
}
});
},
);
api.registerGatewayMethod("voicecall.start", async ({ params, respond }) => {
try {
const to = typeof params?.to === "string" ? params.to.trim() : "";
const message = typeof params?.message === "string" ? params.message.trim() : "";
if (!to) {
respond(false, { error: "to required" });
return;
api.registerGatewayMethod(
"voicecall.start",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
const to = typeof params?.to === "string" ? params.to.trim() : "";
const message = typeof params?.message === "string" ? params.message.trim() : "";
if (!to) {
respond(false, { error: "to required" });
return;
}
const rt = await ensureRuntime();
const result = await rt.manager.initiateCall(to, undefined, {
message: message || undefined,
});
if (!result.success) {
respond(false, { error: result.error || "initiate failed" });
return;
}
respond(true, { callId: result.callId, initiated: true });
} catch (err) {
sendError(respond, err);
}
const rt = await ensureRuntime();
const result = await rt.manager.initiateCall(to, undefined, {
message: message || undefined,
});
if (!result.success) {
respond(false, { error: result.error || "initiate failed" });
return;
}
respond(true, { callId: result.callId, initiated: true });
} catch (err) {
sendError(respond, err);
}
});
},
);
api.registerTool({
name: "voice_call",
@@ -332,7 +351,7 @@ const voiceCallPlugin = {
parameters: VoiceCallToolSchema,
async execute(_toolCallId, params) {
const json = (payload: unknown) => ({
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
details: payload,
});

View File

@@ -146,7 +146,7 @@ export async function generateVoiceResponse(
const text = texts.join(" ") || null;
if (!text && result.meta.aborted) {
if (!text && result.meta?.aborted) {
return { text: null, error: "Response generation was aborted" };
}

View File

@@ -30,7 +30,7 @@ type Logger = {
info: (message: string) => void;
warn: (message: string) => void;
error: (message: string) => void;
debug: (message: string) => void;
debug?: (message: string) => void;
};
function isLoopbackBind(bind: string | undefined): boolean {

View File

@@ -3,6 +3,8 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
import type { ResolvedZaloAccount, ZaloAccountConfig, ZaloConfig } from "./types.js";
import { resolveZaloToken } from "./token.js";
export type { ResolvedZaloAccount };
function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
const accounts = (cfg.channels?.zalo as ZaloConfig | undefined)?.accounts;
if (!accounts || typeof accounts !== "object") {

View File

@@ -1,4 +1,4 @@
import type { Dispatcher } from "undici";
import type { Dispatcher, RequestInit as UndiciRequestInit } from "undici";
import { ProxyAgent, fetch as undiciFetch } from "undici";
import type { ZaloFetch } from "./api.js";
@@ -15,7 +15,10 @@ export function resolveZaloProxyFetch(proxyUrl?: string | null): ZaloFetch | und
}
const agent = new ProxyAgent(trimmed);
const fetcher: ZaloFetch = (input, init) =>
undiciFetch(input, { ...init, dispatcher: agent as Dispatcher });
undiciFetch(input, {
...init,
dispatcher: agent,
} as UndiciRequestInit) as unknown as Promise<Response>;
proxyCache.set(trimmed, fetcher);
return fetcher;
}

View File

@@ -1,4 +1,4 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import { zalouserDock, zalouserPlugin } from "./src/channel.js";
import { setZalouserRuntime } from "./src/runtime.js";
@@ -24,7 +24,7 @@ const plugin = {
"friends (list/search friends), groups (list groups), me (profile info), status (auth check).",
parameters: ZalouserToolSchema,
execute: executeZalouserTool,
});
} as AnyAgentTool);
},
};

View File

@@ -625,7 +625,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
}
ctx.setStatus({
accountId: account.accountId,
user: userInfo,
profile: userInfo,
});
} catch {
// ignore probe errors

View File

@@ -3,6 +3,11 @@ import { runZca, parseJsonOutput } from "./zca.js";
const ACTIONS = ["send", "image", "link", "friends", "groups", "me", "status"] as const;
type AgentToolResult = {
content: Array<{ type: string; text: string }>;
details?: unknown;
};
function stringEnum<T extends readonly string[]>(
values: T,
options: { description?: string } = {},
@@ -38,12 +43,7 @@ type ToolParams = {
url?: string;
};
type ToolResult = {
content: Array<{ type: string; text: string }>;
details: unknown;
};
function json(payload: unknown): ToolResult {
function json(payload: unknown): AgentToolResult {
return {
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
details: payload,
@@ -53,7 +53,9 @@ function json(payload: unknown): ToolResult {
export async function executeZalouserTool(
_toolCallId: string,
params: ToolParams,
): Promise<ToolResult> {
_signal?: AbortSignal,
_onUpdate?: unknown,
): Promise<AgentToolResult> {
try {
switch (params.action) {
case "send": {

View File

@@ -125,6 +125,7 @@ export type ChannelAccountSnapshot = {
botTokenSource?: string;
appTokenSource?: string;
credentialSource?: string;
secretSource?: string;
audienceType?: string;
audience?: string;
webhookPath?: string;
@@ -139,6 +140,10 @@ export type ChannelAccountSnapshot = {
audit?: unknown;
application?: unknown;
bot?: unknown;
publicKey?: string | null;
profile?: unknown;
channelAccessToken?: string;
channelSecret?: string;
};
export type ChannelLogSink = {
@@ -328,4 +333,5 @@ export type ChannelPollContext = {
to: string;
poll: PollInput;
accountId?: string | null;
threadId?: string | null;
};

View File

@@ -23,6 +23,19 @@ export type ChannelDefaultsConfig = {
heartbeat?: ChannelHeartbeatVisibilityConfig;
};
/**
* Base type for extension channel config sections.
* Extensions can use this as a starting point for their channel config.
*/
export type ExtensionChannelConfig = {
enabled?: boolean;
allowFrom?: string | string[];
dmPolicy?: string;
groupPolicy?: GroupPolicy;
accounts?: Record<string, unknown>;
[key: string]: unknown;
};
export type ChannelsConfig = {
defaults?: ChannelDefaultsConfig;
whatsapp?: WhatsAppConfig;
@@ -33,5 +46,7 @@ export type ChannelsConfig = {
signal?: SignalConfig;
imessage?: IMessageConfig;
msteams?: MSTeamsConfig;
[key: string]: unknown;
// Extension channels use dynamic keys - use ExtensionChannelConfig in extensions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
};

View File

@@ -59,20 +59,25 @@ export type {
} from "../channels/plugins/types.js";
export type { ChannelConfigSchema, ChannelPlugin } from "../channels/plugins/types.plugin.js";
export type {
AnyAgentTool,
OpenClawPluginApi,
OpenClawPluginService,
OpenClawPluginServiceContext,
ProviderAuthContext,
ProviderAuthResult,
} from "../plugins/types.js";
export type {
GatewayRequestHandler,
GatewayRequestHandlerOptions,
RespondFn,
} from "../gateway/server-methods/types.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
export type { PluginRuntime, RuntimeLogger } from "../plugins/runtime/types.js";
export { normalizePluginHttpPath } from "../plugins/http-path.js";
export { registerPluginHttpRoute } from "../plugins/http-registry.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export type { OpenClawConfig } from "../config/config.js";
/** @deprecated Use OpenClawConfig instead */
export type { OpenClawConfig as ClawdbotConfig } from "../config/config.js";
export type { ChannelDock } from "../channels/dock.js";
export { getChatChannelMeta } from "../channels/registry.js";
export type {
@@ -130,6 +135,7 @@ export {
listDevicePairing,
rejectDevicePairing,
} from "../infra/device-pairing.js";
export { formatErrorMessage } from "../infra/errors.js";
export { resolveToolsBySender } from "../config/group-policy.js";
export {
buildPendingHistoryContextFromMap,

View File

@@ -169,10 +169,10 @@ type BuildTemplateMessageFromPayload =
type MonitorLineProvider = typeof import("../../line/monitor.js").monitorLineProvider;
export type RuntimeLogger = {
debug?: (message: string) => void;
info: (message: string) => void;
warn: (message: string) => void;
error: (message: string) => void;
debug?: (message: string, meta?: Record<string, unknown>) => void;
info: (message: string, meta?: Record<string, unknown>) => void;
warn: (message: string, meta?: Record<string, unknown>) => void;
error: (message: string, meta?: Record<string, unknown>) => void;
};
export type PluginRuntime = {

View File

@@ -17,6 +17,7 @@ import type { WizardPrompter } from "../wizard/prompts.js";
import type { PluginRuntime } from "./runtime/types.js";
export type { PluginRuntime } from "./runtime/types.js";
export type { AnyAgentTool } from "../agents/tools/common.js";
export type PluginLogger = {
debug?: (message: string) => void;

View File

@@ -13,7 +13,7 @@ import type { OutboundSendDeps } from "../src/infra/outbound/deliver.js";
import { installProcessWarningFilter } from "../src/infra/warning-filter.js";
import { setActivePluginRegistry } from "../src/plugins/runtime.js";
import { createTestRegistry } from "../src/test-utils/channel-plugins.js";
import { withIsolatedTestHome } from "./test-env";
import { withIsolatedTestHome } from "./test-env.js";
installProcessWarningFilter();
@@ -46,7 +46,8 @@ const createStubOutbound = (
sendText: async ({ deps, to, text }) => {
const send = pickSendFn(id, deps);
if (send) {
const result = await send(to, text, {});
// oxlint-disable-next-line typescript/no-explicit-any
const result = await send(to, text, { verbose: false } as any);
return { channel: id, ...result };
}
return { channel: id, messageId: "test" };
@@ -54,7 +55,8 @@ const createStubOutbound = (
sendMedia: async ({ deps, to, text, mediaUrl }) => {
const send = pickSendFn(id, deps);
if (send) {
const result = await send(to, text, { mediaUrl });
// oxlint-disable-next-line typescript/no-explicit-any
const result = await send(to, text, { verbose: false, mediaUrl } as any);
return { channel: id, ...result };
}
return { channel: id, messageId: "test" };
@@ -90,14 +92,14 @@ const createStubPlugin = (params: {
const ids = accounts ? Object.keys(accounts).filter(Boolean) : [];
return ids.length > 0 ? ids : ["default"];
},
resolveAccount: (cfg: OpenClawConfig, accountId: string) => {
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) => {
const channels = cfg.channels as Record<string, unknown> | undefined;
const entry = channels?.[params.id];
if (!entry || typeof entry !== "object") {
return {};
}
const accounts = (entry as { accounts?: Record<string, unknown> }).accounts;
const match = accounts?.[accountId];
const match = accountId ? accounts?.[accountId] : undefined;
return (match && typeof match === "object") || typeof match === "string" ? match : entry;
},
isConfigured: async (_account, cfg: OpenClawConfig) => {

View File

@@ -16,8 +16,12 @@
"skipLibCheck": true,
"strict": true,
"target": "es2023",
"useDefineForClassFields": false
"useDefineForClassFields": false,
"paths": {
"*": ["./*"],
"openclaw/plugin-sdk": ["./src/plugin-sdk/index.ts"]
}
},
"include": ["src/**/*", "ui/**/*"],
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
"include": ["src/**/*", "ui/**/*", "extensions/**/*"],
"exclude": ["node_modules", "dist", "src/**/*.test.ts", "extensions/**/*.test.ts"]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,285 @@
export type UsageSessionEntry = {
key: string;
label?: string;
sessionId?: string;
updatedAt?: number;
agentId?: string;
channel?: string;
chatType?: string;
origin?: {
label?: string;
provider?: string;
surface?: string;
chatType?: string;
from?: string;
to?: string;
accountId?: string;
threadId?: string | number;
};
modelOverride?: string;
providerOverride?: string;
modelProvider?: string;
model?: string;
usage: {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
totalTokens: number;
totalCost: number;
inputCost?: number;
outputCost?: number;
cacheReadCost?: number;
cacheWriteCost?: number;
missingCostEntries: number;
firstActivity?: number;
lastActivity?: number;
durationMs?: number;
activityDates?: string[]; // YYYY-MM-DD dates when session had activity
dailyBreakdown?: Array<{ date: string; tokens: number; cost: number }>; // Per-day breakdown
dailyMessageCounts?: Array<{
date: string;
total: number;
user: number;
assistant: number;
toolCalls: number;
toolResults: number;
errors: number;
}>;
dailyLatency?: Array<{
date: string;
count: number;
avgMs: number;
p95Ms: number;
minMs: number;
maxMs: number;
}>;
dailyModelUsage?: Array<{
date: string;
provider?: string;
model?: string;
tokens: number;
cost: number;
count: number;
}>;
messageCounts?: {
total: number;
user: number;
assistant: number;
toolCalls: number;
toolResults: number;
errors: number;
};
toolUsage?: {
totalCalls: number;
uniqueTools: number;
tools: Array<{ name: string; count: number }>;
};
modelUsage?: Array<{
provider?: string;
model?: string;
count: number;
totals: UsageTotals;
}>;
latency?: {
count: number;
avgMs: number;
p95Ms: number;
minMs: number;
maxMs: number;
};
} | null;
contextWeight?: {
systemPrompt: { chars: number; projectContextChars: number; nonProjectContextChars: number };
skills: { promptChars: number; entries: Array<{ name: string; blockChars: number }> };
tools: {
listChars: number;
schemaChars: number;
entries: Array<{ name: string; summaryChars: number; schemaChars: number }>;
};
injectedWorkspaceFiles: Array<{
name: string;
path: string;
rawChars: number;
injectedChars: number;
truncated: boolean;
}>;
} | null;
};
export type UsageTotals = {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
totalTokens: number;
totalCost: number;
inputCost: number;
outputCost: number;
cacheReadCost: number;
cacheWriteCost: number;
missingCostEntries: number;
};
export type CostDailyEntry = UsageTotals & { date: string };
export type UsageAggregates = {
messages: {
total: number;
user: number;
assistant: number;
toolCalls: number;
toolResults: number;
errors: number;
};
tools: {
totalCalls: number;
uniqueTools: number;
tools: Array<{ name: string; count: number }>;
};
byModel: Array<{
provider?: string;
model?: string;
count: number;
totals: UsageTotals;
}>;
byProvider: Array<{
provider?: string;
model?: string;
count: number;
totals: UsageTotals;
}>;
byAgent: Array<{ agentId: string; totals: UsageTotals }>;
byChannel: Array<{ channel: string; totals: UsageTotals }>;
latency?: {
count: number;
avgMs: number;
p95Ms: number;
minMs: number;
maxMs: number;
};
dailyLatency?: Array<{
date: string;
count: number;
avgMs: number;
p95Ms: number;
minMs: number;
maxMs: number;
}>;
modelDaily?: Array<{
date: string;
provider?: string;
model?: string;
tokens: number;
cost: number;
count: number;
}>;
daily: Array<{
date: string;
tokens: number;
cost: number;
messages: number;
toolCalls: number;
errors: number;
}>;
};
export type UsageColumnId =
| "channel"
| "agent"
| "provider"
| "model"
| "messages"
| "tools"
| "errors"
| "duration";
export type TimeSeriesPoint = {
timestamp: number;
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
totalTokens: number;
cost: number;
cumulativeTokens: number;
cumulativeCost: number;
};
export type UsageProps = {
loading: boolean;
error: string | null;
startDate: string;
endDate: string;
sessions: UsageSessionEntry[];
sessionsLimitReached: boolean; // True if 1000 session cap was hit
totals: UsageTotals | null;
aggregates: UsageAggregates | null;
costDaily: CostDailyEntry[];
selectedSessions: string[]; // Support multiple session selection
selectedDays: string[]; // Support multiple day selection
selectedHours: number[]; // Support multiple hour selection
chartMode: "tokens" | "cost";
dailyChartMode: "total" | "by-type";
timeSeriesMode: "cumulative" | "per-turn";
timeSeriesBreakdownMode: "total" | "by-type";
timeSeries: { points: TimeSeriesPoint[] } | null;
timeSeriesLoading: boolean;
sessionLogs: SessionLogEntry[] | null;
sessionLogsLoading: boolean;
sessionLogsExpanded: boolean;
logFilterRoles: SessionLogRole[];
logFilterTools: string[];
logFilterHasTools: boolean;
logFilterQuery: string;
query: string;
queryDraft: string;
sessionSort: "tokens" | "cost" | "recent" | "messages" | "errors";
sessionSortDir: "asc" | "desc";
recentSessions: string[];
sessionsTab: "all" | "recent";
visibleColumns: UsageColumnId[];
timeZone: "local" | "utc";
contextExpanded: boolean;
headerPinned: boolean;
onStartDateChange: (date: string) => void;
onEndDateChange: (date: string) => void;
onRefresh: () => void;
onTimeZoneChange: (zone: "local" | "utc") => void;
onToggleContextExpanded: () => void;
onToggleHeaderPinned: () => void;
onToggleSessionLogsExpanded: () => void;
onLogFilterRolesChange: (next: SessionLogRole[]) => void;
onLogFilterToolsChange: (next: string[]) => void;
onLogFilterHasToolsChange: (next: boolean) => void;
onLogFilterQueryChange: (next: string) => void;
onLogFilterClear: () => void;
onSelectSession: (key: string, shiftKey: boolean) => void;
onChartModeChange: (mode: "tokens" | "cost") => void;
onDailyChartModeChange: (mode: "total" | "by-type") => void;
onTimeSeriesModeChange: (mode: "cumulative" | "per-turn") => void;
onTimeSeriesBreakdownChange: (mode: "total" | "by-type") => void;
onSelectDay: (day: string, shiftKey: boolean) => void; // Support shift-click
onSelectHour: (hour: number, shiftKey: boolean) => void;
onClearDays: () => void;
onClearHours: () => void;
onClearSessions: () => void;
onClearFilters: () => void;
onQueryDraftChange: (query: string) => void;
onApplyQuery: () => void;
onClearQuery: () => void;
onSessionSortChange: (sort: "tokens" | "cost" | "recent" | "messages" | "errors") => void;
onSessionSortDirChange: (dir: "asc" | "desc") => void;
onSessionsTabChange: (tab: "all" | "recent") => void;
onToggleColumn: (column: UsageColumnId) => void;
};
export type SessionLogEntry = {
timestamp: number;
role: "user" | "assistant" | "tool" | "toolResult";
content: string;
tokens?: number;
cost?: number;
};
export type SessionLogRole = SessionLogEntry["role"];