cc工作前备份
This commit is contained in:
231
plugins/zclaw-feishu/index.ts
Normal file
231
plugins/zclaw-feishu/index.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* ZCLAW Feishu Channel Plugin
|
||||
*
|
||||
* Registers Feishu (飞书/Lark) as a messaging channel for OpenClaw Gateway.
|
||||
* Supports:
|
||||
* - Receiving messages via Feishu Event Subscription
|
||||
* - Sending text/rich messages back to Feishu
|
||||
* - OAuth token management (tenant_access_token)
|
||||
* - Webhook-based event handling
|
||||
*/
|
||||
|
||||
interface PluginAPI {
|
||||
config: Record<string, any>;
|
||||
registerChannel(opts: { plugin: ChannelPlugin }): void;
|
||||
registerHook(event: string, handler: (...args: any[]) => any, meta?: Record<string, any>): void;
|
||||
registerGatewayMethod(method: string, handler: (ctx: any) => void): void;
|
||||
registerService(id: string, service: BackgroundService): void;
|
||||
}
|
||||
|
||||
interface BackgroundService {
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
interface ChannelPlugin {
|
||||
id: string;
|
||||
meta: {
|
||||
id: string;
|
||||
label: string;
|
||||
selectionLabel: string;
|
||||
docsPath?: string;
|
||||
blurb: string;
|
||||
aliases: string[];
|
||||
detailLabel?: string;
|
||||
};
|
||||
capabilities: {
|
||||
chatTypes: string[];
|
||||
};
|
||||
config: {
|
||||
listAccountIds: (cfg: any) => string[];
|
||||
resolveAccount: (cfg: any, accountId?: string) => any;
|
||||
};
|
||||
outbound: {
|
||||
deliveryMode: string;
|
||||
sendText: (opts: SendTextOpts) => Promise<{ ok: boolean; error?: string }>;
|
||||
sendRichText?: (opts: SendRichTextOpts) => Promise<{ ok: boolean; error?: string }>;
|
||||
};
|
||||
gateway?: {
|
||||
start?: (ctx: any) => Promise<void>;
|
||||
stop?: (ctx: any) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
interface SendTextOpts {
|
||||
text: string;
|
||||
chatId: string;
|
||||
accountId?: string;
|
||||
}
|
||||
|
||||
interface SendRichTextOpts {
|
||||
content: any;
|
||||
chatId: string;
|
||||
accountId?: string;
|
||||
}
|
||||
|
||||
// Feishu API helpers
|
||||
class FeishuClient {
|
||||
private baseUrl = 'https://open.feishu.cn/open-apis';
|
||||
private appId: string;
|
||||
private appSecret: string;
|
||||
private tenantToken: string | null = null;
|
||||
private tokenExpiry: number = 0;
|
||||
|
||||
constructor(appId: string, appSecret: string) {
|
||||
this.appId = appId;
|
||||
this.appSecret = appSecret;
|
||||
}
|
||||
|
||||
async getTenantToken(): Promise<string> {
|
||||
if (this.tenantToken && Date.now() < this.tokenExpiry) {
|
||||
return this.tenantToken;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/auth/v3/tenant_access_token/internal`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
app_id: this.appId,
|
||||
app_secret: this.appSecret,
|
||||
}),
|
||||
});
|
||||
|
||||
const data: any = await response.json();
|
||||
if (data.code !== 0) {
|
||||
throw new Error(`Feishu auth failed: ${data.msg}`);
|
||||
}
|
||||
|
||||
this.tenantToken = data.tenant_access_token;
|
||||
// Token valid for 2 hours, refresh at 1.5h
|
||||
this.tokenExpiry = Date.now() + 90 * 60 * 1000;
|
||||
return this.tenantToken!;
|
||||
}
|
||||
|
||||
async sendMessage(chatId: string, msgType: string, content: any): Promise<{ ok: boolean; error?: string }> {
|
||||
try {
|
||||
const token = await this.getTenantToken();
|
||||
const response = await fetch(`${this.baseUrl}/im/v1/messages?receive_id_type=chat_id`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
receive_id: chatId,
|
||||
msg_type: msgType,
|
||||
content: JSON.stringify(content),
|
||||
}),
|
||||
});
|
||||
|
||||
const data: any = await response.json();
|
||||
if (data.code !== 0) {
|
||||
return { ok: false, error: data.msg };
|
||||
}
|
||||
return { ok: true };
|
||||
} catch (error: any) {
|
||||
return { ok: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async sendText(chatId: string, text: string): Promise<{ ok: boolean; error?: string }> {
|
||||
return this.sendMessage(chatId, 'text', { text });
|
||||
}
|
||||
|
||||
async sendRichText(chatId: string, content: any): Promise<{ ok: boolean; error?: string }> {
|
||||
return this.sendMessage(chatId, 'post', content);
|
||||
}
|
||||
}
|
||||
|
||||
// Global client instance per account
|
||||
const clients = new Map<string, FeishuClient>();
|
||||
|
||||
function getClient(account: { appId: string; appSecret: string }, accountId: string): FeishuClient {
|
||||
if (!clients.has(accountId)) {
|
||||
clients.set(accountId, new FeishuClient(account.appId, account.appSecret));
|
||||
}
|
||||
return clients.get(accountId)!;
|
||||
}
|
||||
|
||||
// Channel definition
|
||||
const feishuChannel: ChannelPlugin = {
|
||||
id: 'feishu',
|
||||
meta: {
|
||||
id: 'feishu',
|
||||
label: '飞书 (Feishu)',
|
||||
selectionLabel: 'Feishu / Lark',
|
||||
blurb: 'Connect to Feishu (飞书) for messaging via Feishu Bot API.',
|
||||
aliases: ['lark', 'feishu'],
|
||||
detailLabel: '飞书机器人',
|
||||
},
|
||||
capabilities: {
|
||||
chatTypes: ['direct', 'group'],
|
||||
},
|
||||
config: {
|
||||
listAccountIds: (cfg: any) =>
|
||||
Object.keys(cfg.channels?.feishu?.accounts ?? {}),
|
||||
resolveAccount: (cfg: any, accountId?: string) =>
|
||||
cfg.channels?.feishu?.accounts?.[accountId ?? 'default'] ?? { accountId },
|
||||
},
|
||||
outbound: {
|
||||
deliveryMode: 'direct',
|
||||
sendText: async ({ text, chatId, accountId }: SendTextOpts) => {
|
||||
const cfg = (globalThis as any).__openclaw_config;
|
||||
const account = feishuChannel.config.resolveAccount(cfg, accountId);
|
||||
if (!account?.appId || !account?.appSecret) {
|
||||
return { ok: false, error: 'Feishu account not configured (missing appId/appSecret)' };
|
||||
}
|
||||
const client = getClient(account, accountId ?? 'default');
|
||||
return client.sendText(chatId, text);
|
||||
},
|
||||
sendRichText: async ({ content, chatId, accountId }: SendRichTextOpts) => {
|
||||
const cfg = (globalThis as any).__openclaw_config;
|
||||
const account = feishuChannel.config.resolveAccount(cfg, accountId);
|
||||
if (!account?.appId || !account?.appSecret) {
|
||||
return { ok: false, error: 'Feishu account not configured' };
|
||||
}
|
||||
const client = getClient(account, accountId ?? 'default');
|
||||
return client.sendRichText(chatId, content);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin entry point
|
||||
*/
|
||||
export default function register(api: PluginAPI) {
|
||||
// Register Feishu as a channel
|
||||
api.registerChannel({ plugin: feishuChannel });
|
||||
|
||||
// Register custom RPC method for Feishu status
|
||||
api.registerGatewayMethod('feishu.status', ({ respond }: any) => {
|
||||
const accountIds = feishuChannel.config.listAccountIds(api.config);
|
||||
respond(true, {
|
||||
channel: 'feishu',
|
||||
accounts: accountIds.length,
|
||||
configured: accountIds.length > 0,
|
||||
});
|
||||
});
|
||||
|
||||
// Startup hook
|
||||
api.registerHook('gateway:startup', async () => {
|
||||
const accountIds = feishuChannel.config.listAccountIds(api.config);
|
||||
if (accountIds.length > 0) {
|
||||
console.log(`[ZCLAW] Feishu channel registered with ${accountIds.length} account(s)`);
|
||||
// Pre-warm token for each account
|
||||
for (const id of accountIds) {
|
||||
try {
|
||||
const account = feishuChannel.config.resolveAccount(api.config, id);
|
||||
if (account?.appId && account?.appSecret && account?.enabled !== false) {
|
||||
const client = getClient(account, id);
|
||||
await client.getTenantToken();
|
||||
console.log(`[ZCLAW] Feishu account "${id}" token acquired`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn(`[ZCLAW] Feishu account "${id}" token failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('[ZCLAW] Feishu channel registered (no accounts configured yet)');
|
||||
}
|
||||
}, { name: 'zclaw-feishu.startup', description: 'Initialize Feishu connections on startup' });
|
||||
}
|
||||
27
plugins/zclaw-feishu/plugin.json
Normal file
27
plugins/zclaw-feishu/plugin.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "zclaw-feishu",
|
||||
"name": "ZCLAW Feishu Channel",
|
||||
"version": "0.1.0",
|
||||
"description": "Feishu (飞书/Lark) messaging channel plugin for OpenClaw",
|
||||
"author": "ZCLAW",
|
||||
"entry": "index.ts",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"appId": { "type": "string" },
|
||||
"appSecret": { "type": "string" },
|
||||
"verificationToken": { "type": "string" },
|
||||
"encryptKey": { "type": "string" },
|
||||
"webhookUrl": { "type": "string" }
|
||||
},
|
||||
"required": ["appId", "appSecret"]
|
||||
},
|
||||
"uiHints": {
|
||||
"appId": { "label": "飞书 App ID", "placeholder": "cli_xxxxxxxxxxxx" },
|
||||
"appSecret": { "label": "飞书 App Secret", "sensitive": true },
|
||||
"verificationToken": { "label": "Verification Token", "sensitive": true },
|
||||
"encryptKey": { "label": "Encrypt Key", "sensitive": true },
|
||||
"webhookUrl": { "label": "Webhook URL (可选)", "placeholder": "https://your-server/feishu/webhook" }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user