Files
PIT_Channel/src/channel.ts
feifei.xu 3db0311b2f fix: 统一 pluginId 为 zhidui-channel
- 修改 package.json pluginId: pit-bot -> zhidui-channel
- 修改 channel.ts 中的 id 为 zhidui-channel
- 重命名目录为 openclaw-zhidui-channel
2026-03-15 12:27:19 +08:00

337 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = "zhidui-channel";
// Gateway 实例映射
const gateways = new Map<string, Gateway>();
/**
* PIT Bot Channel Plugin
*/
export const pitBotPlugin: ChannelPlugin<ResolvedPITBotAccount> = {
id: "zhidui-channel",
meta: {
id: "zhidui-channel",
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: "zhidui-channel",
messageId: "",
error: new Error(`Gateway not available for account ${accountId}`),
};
}
const result = await gateway.sendText(to, text, replyToId);
return {
channel: "zhidui-channel",
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: "zhidui-channel",
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: "zhidui-channel",
messageId: "",
error: new Error(`Gateway not available for account ${accountId}`),
};
}
const result = await gateway.sendMedia(to, mediaUrl ?? "", text, replyToId);
return {
channel: "zhidui-channel",
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: "zhidui-channel",
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: "zhidui-channel",
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,
// åˆ<C3A5>å§åŒé©å­? init: async (api: unknown) => {
const log = createLogger(MODULE);
log.info("PIT Bot plugin initializing");
registerWebUIRoutes(api as never);
log.info("PIT Bot plugin initialized");
},
// 清ç<E280A6>†é©å­<C3A5>
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;