feat: PIT Channel plugin v1.0.0 - complete implementation
This commit is contained in:
338
src/channel.ts
Normal file
338
src/channel.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* PIT Channel 主文件
|
||||
* @module channel
|
||||
*/
|
||||
|
||||
import type {
|
||||
ChannelPlugin,
|
||||
ChannelMeta,
|
||||
ChannelCapabilities,
|
||||
ChannelMessagingAdapter,
|
||||
ChannelConfigAdapter,
|
||||
ChannelOutboundAdapter,
|
||||
ChannelGatewayAdapter,
|
||||
ChannelStatusAdapter,
|
||||
OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk";
|
||||
|
||||
import type {
|
||||
ResolvedPITBotAccount,
|
||||
PITUserMessage,
|
||||
PITConnectionState
|
||||
} from "./types.js";
|
||||
import type { Gateway } from "./gateway.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
listPITBotAccountIds,
|
||||
resolvePITBotAccount,
|
||||
applyPITBotAccountConfig,
|
||||
validateConfig,
|
||||
} from "./config.js";
|
||||
import { startGateway } from "./gateway.js";
|
||||
import { chunkText } from "./utils/chunker.js";
|
||||
import { createLogger } from "./utils/logger.js";
|
||||
import { registerWebUIRoutes } from "./webui/routes.js";
|
||||
|
||||
const MODULE = "pit-bot";
|
||||
|
||||
// Gateway 实例映射
|
||||
const gateways = new Map<string, Gateway>();
|
||||
|
||||
/**
|
||||
* PIT Bot Channel Plugin
|
||||
*/
|
||||
export const pitBotPlugin: ChannelPlugin<ResolvedPITBotAccount> = {
|
||||
id: "pit-bot",
|
||||
|
||||
meta: {
|
||||
id: "pit-bot",
|
||||
label: "PIT Bot",
|
||||
selectionLabel: "PIT Bot",
|
||||
docsPath: "/docs/channels/pit-bot",
|
||||
blurb: "Connect to PIT Router for multi-agent support",
|
||||
order: 60,
|
||||
} as ChannelMeta,
|
||||
|
||||
capabilities: {
|
||||
chatTypes: ["direct"],
|
||||
media: true,
|
||||
reactions: false,
|
||||
threads: false,
|
||||
blockStreaming: false,
|
||||
} as ChannelCapabilities,
|
||||
|
||||
reload: { configPrefixes: ["channels.pit-bot"] },
|
||||
|
||||
messaging: {
|
||||
normalizeTarget: (target: string) => {
|
||||
const normalized = target.replace(/^pit-bot:/i, "");
|
||||
if (normalized.startsWith("user:")) {
|
||||
return { ok: true, to: normalized };
|
||||
}
|
||||
return { ok: true, to: `user:${normalized}` };
|
||||
},
|
||||
|
||||
targetResolver: {
|
||||
looksLikeId: (id: string) => {
|
||||
const normalized = id.replace(/^pit-bot:/i, "");
|
||||
return normalized.startsWith("user:") || normalized.length > 0;
|
||||
},
|
||||
hint: "user:<userId> or <userId>",
|
||||
},
|
||||
} as ChannelMessagingAdapter,
|
||||
|
||||
config: {
|
||||
listAccountIds: (cfg: OpenClawConfig) => listPITBotAccountIds(cfg),
|
||||
resolveAccount: (cfg: OpenClawConfig, accountId: string) => resolvePITBotAccount(cfg, accountId),
|
||||
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
|
||||
applyAccountName: (cfg: OpenClawConfig, accountId: string, name: string) => {
|
||||
applyPITBotAccountConfig(cfg, accountId, { name });
|
||||
},
|
||||
|
||||
deleteAccount: (cfg: OpenClawConfig, accountId: string) => {
|
||||
const accounts = cfg.getSection<Record<string, unknown>>("channels.pit-bot.accounts");
|
||||
if (accounts && accountId in accounts) {
|
||||
delete accounts[accountId];
|
||||
cfg.set("channels.pit-bot.accounts", accounts);
|
||||
}
|
||||
},
|
||||
|
||||
setAccountEnabled: (cfg: OpenClawConfig, accountId: string, enabled: boolean) => {
|
||||
applyPITBotAccountConfig(cfg, accountId, { enabled });
|
||||
},
|
||||
|
||||
validateAccountConfig: (cfg: OpenClawConfig, accountId: string) => {
|
||||
const account = resolvePITBotAccount(cfg, accountId);
|
||||
return validateConfig(account.config);
|
||||
},
|
||||
} as ChannelConfigAdapter<ResolvedPITBotAccount>,
|
||||
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkText,
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
|
||||
sendText: async ({ to, text, accountId, replyToId }: { to: string; text: string; accountId: string; replyToId?: string }): Promise<{ channel: string; messageId: string; error?: Error }> => {
|
||||
const log = createLogger(`${MODULE}:outbound`);
|
||||
|
||||
try {
|
||||
const gateway = gateways.get(accountId);
|
||||
if (!gateway) {
|
||||
return {
|
||||
channel: "pit-bot",
|
||||
messageId: "",
|
||||
error: new Error(`Gateway not available for account ${accountId}`),
|
||||
};
|
||||
}
|
||||
|
||||
const result = await gateway.sendText(to, text, replyToId);
|
||||
|
||||
return {
|
||||
channel: "pit-bot",
|
||||
messageId: result.messageId ?? "",
|
||||
error: result.error ? new Error(result.error) : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log.error("sendText failed", { to, accountId, error: message });
|
||||
return {
|
||||
channel: "pit-bot",
|
||||
messageId: "",
|
||||
error: new Error(message),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
sendMedia: async ({ to, text, mediaUrl, accountId, replyToId }: { to: string; text?: string; mediaUrl: string; accountId: string; replyToId?: string }): Promise<{ channel: string; messageId: string; error?: Error }> => {
|
||||
const log = createLogger(`${MODULE}:outbound`);
|
||||
|
||||
try {
|
||||
const gateway = gateways.get(accountId);
|
||||
if (!gateway) {
|
||||
return {
|
||||
channel: "pit-bot",
|
||||
messageId: "",
|
||||
error: new Error(`Gateway not available for account ${accountId}`),
|
||||
};
|
||||
}
|
||||
|
||||
const result = await gateway.sendMedia(to, mediaUrl ?? "", text, replyToId);
|
||||
|
||||
return {
|
||||
channel: "pit-bot",
|
||||
messageId: result.messageId ?? "",
|
||||
error: result.error ? new Error(result.error) : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log.error("sendMedia failed", { to, accountId, error: message });
|
||||
return {
|
||||
channel: "pit-bot",
|
||||
messageId: "",
|
||||
error: new Error(message),
|
||||
};
|
||||
}
|
||||
},
|
||||
} as ChannelOutboundAdapter<ResolvedPITBotAccount>,
|
||||
|
||||
gateway: {
|
||||
startAccount: async (ctx: {
|
||||
account: ResolvedPITBotAccount;
|
||||
abortSignal?: AbortSignal;
|
||||
cfg: OpenClawConfig;
|
||||
log?: { info: (msg: string) => void; error: (msg: string) => void };
|
||||
setStatus: (state: Partial<PITConnectionState>) => void;
|
||||
getStatus: () => PITConnectionState;
|
||||
emitMessage: (msg: unknown) => void;
|
||||
}) => {
|
||||
const { account, abortSignal, cfg } = ctx;
|
||||
const log = createLogger(`${MODULE}:${account.accountId}`);
|
||||
|
||||
log.info("Starting gateway");
|
||||
|
||||
try {
|
||||
const gateway = await startGateway({
|
||||
account,
|
||||
abortSignal,
|
||||
cfg,
|
||||
log,
|
||||
setStatus: (state) => {
|
||||
const current = ctx.getStatus();
|
||||
ctx.setStatus({ ...current, ...state });
|
||||
},
|
||||
getStatus: () => ctx.getStatus(),
|
||||
onMessage: (message: PITUserMessage) => {
|
||||
ctx.emitMessage({
|
||||
channel: "pit-bot",
|
||||
accountId: account.accountId,
|
||||
chatId: `pit-bot:user:${message.userId}`,
|
||||
chatType: "direct",
|
||||
senderId: message.userId,
|
||||
text: message.content,
|
||||
attachments: message.attachments?.map(a => ({
|
||||
type: a.type,
|
||||
url: a.url,
|
||||
filename: a.filename,
|
||||
size: a.size,
|
||||
})),
|
||||
replyToId: message.replyTo,
|
||||
metadata: {
|
||||
sessionId: message.sessionId,
|
||||
timestamp: message.timestamp,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
gateways.set(account.accountId, gateway);
|
||||
|
||||
ctx.setStatus({
|
||||
...ctx.getStatus(),
|
||||
running: true,
|
||||
connected: true,
|
||||
});
|
||||
|
||||
log.info("Gateway started successfully");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log.error("Failed to start gateway", { error: message });
|
||||
ctx.setStatus({
|
||||
...ctx.getStatus(),
|
||||
running: false,
|
||||
connected: false,
|
||||
lastError: message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
stopAccount: async (ctx: { account: ResolvedPITBotAccount }) => {
|
||||
const { account } = ctx;
|
||||
const log = createLogger(`${MODULE}:${account.accountId}`);
|
||||
|
||||
log.info("Stopping gateway");
|
||||
|
||||
const gateway = gateways.get(account.accountId);
|
||||
if (gateway) {
|
||||
gateway.disconnect();
|
||||
gateways.delete(account.accountId);
|
||||
}
|
||||
|
||||
log.info("Gateway stopped");
|
||||
},
|
||||
|
||||
restartAccount: async (ctx: {
|
||||
account: ResolvedPITBotAccount;
|
||||
abortSignal?: AbortSignal;
|
||||
cfg: OpenClawConfig;
|
||||
setStatus: (state: Partial<PITConnectionState>) => void;
|
||||
getStatus: () => PITConnectionState;
|
||||
emitMessage: (msg: unknown) => void;
|
||||
}) => {
|
||||
const { account } = ctx;
|
||||
const log = createLogger(`${MODULE}:${account.accountId}`);
|
||||
|
||||
log.info("Restarting gateway");
|
||||
|
||||
const gateway = gateways.get(account.accountId);
|
||||
if (gateway) {
|
||||
gateway.disconnect();
|
||||
gateways.delete(account.accountId);
|
||||
}
|
||||
|
||||
await pitBotPlugin.gateway!.startAccount!(ctx as never);
|
||||
},
|
||||
} as ChannelGatewayAdapter<ResolvedPITBotAccount>,
|
||||
|
||||
status: {
|
||||
defaultRuntime: {
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
running: false,
|
||||
connected: false,
|
||||
},
|
||||
|
||||
formatStatus: (runtime: PITConnectionState) => {
|
||||
const parts: string[] = [];
|
||||
|
||||
parts.push(runtime.connected ? "🟢 Connected" : "🔴 Disconnected");
|
||||
|
||||
if (runtime.sessionId) {
|
||||
parts.push(`Session: ${runtime.sessionId.slice(0, 8)}...`);
|
||||
}
|
||||
|
||||
if (runtime.lastError) {
|
||||
parts.push(`Error: ${runtime.lastError}`);
|
||||
}
|
||||
|
||||
return parts.join(" | ");
|
||||
},
|
||||
} as ChannelStatusAdapter,
|
||||
|
||||
// 初始化钩子
|
||||
init: async (api: unknown) => {
|
||||
const log = createLogger(MODULE);
|
||||
log.info("PIT Bot plugin initializing");
|
||||
|
||||
registerWebUIRoutes(api as never);
|
||||
|
||||
log.info("PIT Bot plugin initialized");
|
||||
},
|
||||
|
||||
// 清理钩子
|
||||
cleanup: async () => {
|
||||
const log = createLogger(MODULE);
|
||||
log.info("PIT Bot plugin cleaning up");
|
||||
|
||||
for (const [accountId, gateway] of gateways.entries()) {
|
||||
log.info(`Disconnecting gateway: ${accountId}`);
|
||||
gateway.disconnect();
|
||||
}
|
||||
gateways.clear();
|
||||
|
||||
log.info("PIT Bot plugin cleaned up");
|
||||
},
|
||||
};
|
||||
|
||||
export default pitBotPlugin;
|
||||
Reference in New Issue
Block a user