- 修改 package.json pluginId: pit-bot -> zhidui-channel - 修改 channel.ts 中的 id 为 zhidui-channel - 重命名目录为 openclaw-zhidui-channel
337 lines
9.8 KiB
TypeScript
337 lines
9.8 KiB
TypeScript
/**
|
||
* 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;
|