cc工作前备份
This commit is contained in:
211
plugins/zclaw-chinese-models/index.ts
Normal file
211
plugins/zclaw-chinese-models/index.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* ZCLAW Chinese Models Plugin
|
||||
*
|
||||
* Registers Chinese AI model providers for OpenClaw Gateway:
|
||||
* - Zhipu GLM (智谱)
|
||||
* - Qwen (通义千问)
|
||||
* - Kimi (月之暗面)
|
||||
* - MiniMax
|
||||
*
|
||||
* All providers use OpenAI-compatible API format.
|
||||
*/
|
||||
|
||||
interface PluginAPI {
|
||||
config: Record<string, any>;
|
||||
registerProvider(provider: ProviderDefinition): void;
|
||||
registerHook(event: string, handler: (...args: any[]) => any, meta?: Record<string, any>): void;
|
||||
}
|
||||
|
||||
interface ProviderDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
baseUrl: string;
|
||||
apiKeyEnvVar?: string;
|
||||
models: ProviderModel[];
|
||||
headers?: (apiKey: string) => Record<string, string>;
|
||||
}
|
||||
|
||||
interface ProviderModel {
|
||||
id: string;
|
||||
alias?: string;
|
||||
contextWindow?: number;
|
||||
maxOutputTokens?: number;
|
||||
supportsVision?: boolean;
|
||||
supportsStreaming?: boolean;
|
||||
}
|
||||
|
||||
// 智谱 GLM Provider
|
||||
const zhipuProvider: ProviderDefinition = {
|
||||
id: 'zhipu',
|
||||
name: 'Zhipu AI (智谱)',
|
||||
baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
|
||||
apiKeyEnvVar: 'ZHIPU_API_KEY',
|
||||
models: [
|
||||
{
|
||||
id: 'glm-5',
|
||||
alias: 'GLM-5',
|
||||
contextWindow: 128000,
|
||||
maxOutputTokens: 4096,
|
||||
supportsVision: true,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
{
|
||||
id: 'glm-4.7',
|
||||
alias: 'GLM-4.7',
|
||||
contextWindow: 128000,
|
||||
maxOutputTokens: 4096,
|
||||
supportsVision: true,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
{
|
||||
id: 'glm-4-plus',
|
||||
alias: 'GLM-4-Plus',
|
||||
contextWindow: 128000,
|
||||
maxOutputTokens: 4096,
|
||||
supportsVision: false,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
{
|
||||
id: 'glm-4-flash',
|
||||
alias: 'GLM-4-Flash',
|
||||
contextWindow: 128000,
|
||||
maxOutputTokens: 4096,
|
||||
supportsVision: false,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
],
|
||||
headers: (apiKey: string) => ({
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
};
|
||||
|
||||
// 通义千问 Provider (OpenAI-compatible via DashScope)
|
||||
const qwenProvider: ProviderDefinition = {
|
||||
id: 'qwen',
|
||||
name: 'Qwen (通义千问)',
|
||||
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
apiKeyEnvVar: 'QWEN_API_KEY',
|
||||
models: [
|
||||
{
|
||||
id: 'qwen3.5-plus',
|
||||
alias: 'Qwen3.5+',
|
||||
contextWindow: 131072,
|
||||
maxOutputTokens: 8192,
|
||||
supportsVision: false,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
{
|
||||
id: 'qwen-max',
|
||||
alias: 'Qwen-Max',
|
||||
contextWindow: 32768,
|
||||
maxOutputTokens: 8192,
|
||||
supportsVision: false,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
{
|
||||
id: 'qwen-vl-max',
|
||||
alias: 'Qwen-VL-Max',
|
||||
contextWindow: 32768,
|
||||
maxOutputTokens: 4096,
|
||||
supportsVision: true,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
],
|
||||
headers: (apiKey: string) => ({
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
};
|
||||
|
||||
// Kimi (月之暗面) Provider
|
||||
const kimiProvider: ProviderDefinition = {
|
||||
id: 'kimi',
|
||||
name: 'Kimi (月之暗面)',
|
||||
baseUrl: 'https://api.moonshot.cn/v1',
|
||||
apiKeyEnvVar: 'KIMI_API_KEY',
|
||||
models: [
|
||||
{
|
||||
id: 'kimi-k2.5',
|
||||
alias: 'Kimi-K2.5',
|
||||
contextWindow: 131072,
|
||||
maxOutputTokens: 8192,
|
||||
supportsVision: false,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
{
|
||||
id: 'moonshot-v1-128k',
|
||||
alias: 'Moonshot-128K',
|
||||
contextWindow: 128000,
|
||||
maxOutputTokens: 4096,
|
||||
supportsVision: false,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
],
|
||||
headers: (apiKey: string) => ({
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
};
|
||||
|
||||
// MiniMax Provider
|
||||
const minimaxProvider: ProviderDefinition = {
|
||||
id: 'minimax',
|
||||
name: 'MiniMax',
|
||||
baseUrl: 'https://api.minimax.chat/v1',
|
||||
apiKeyEnvVar: 'MINIMAX_API_KEY',
|
||||
models: [
|
||||
{
|
||||
id: 'minimax-m2.5',
|
||||
alias: 'MiniMax-M2.5',
|
||||
contextWindow: 245760,
|
||||
maxOutputTokens: 16384,
|
||||
supportsVision: false,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
{
|
||||
id: 'abab6.5s-chat',
|
||||
alias: 'ABAB-6.5s',
|
||||
contextWindow: 245760,
|
||||
maxOutputTokens: 8192,
|
||||
supportsVision: false,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
],
|
||||
headers: (apiKey: string) => ({
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin entry point - registers all Chinese model providers
|
||||
*/
|
||||
export default function register(api: PluginAPI) {
|
||||
const pluginConfig = api.config?.plugins?.entries?.['zclaw-chinese-models']?.config ?? {};
|
||||
|
||||
// Register Zhipu GLM
|
||||
const zhipu = { ...zhipuProvider };
|
||||
if (pluginConfig.zhipuBaseUrl) zhipu.baseUrl = pluginConfig.zhipuBaseUrl;
|
||||
api.registerProvider(zhipu);
|
||||
|
||||
// Register Qwen
|
||||
const qwen = { ...qwenProvider };
|
||||
if (pluginConfig.qwenBaseUrl) qwen.baseUrl = pluginConfig.qwenBaseUrl;
|
||||
api.registerProvider(qwen);
|
||||
|
||||
// Register Kimi
|
||||
const kimi = { ...kimiProvider };
|
||||
if (pluginConfig.kimiBaseUrl) kimi.baseUrl = pluginConfig.kimiBaseUrl;
|
||||
api.registerProvider(kimi);
|
||||
|
||||
// Register MiniMax
|
||||
const minimax = { ...minimaxProvider };
|
||||
if (pluginConfig.minimaxBaseUrl) minimax.baseUrl = pluginConfig.minimaxBaseUrl;
|
||||
api.registerProvider(minimax);
|
||||
|
||||
// Log registration
|
||||
api.registerHook('gateway:startup', async () => {
|
||||
console.log('[ZCLAW] Chinese model providers registered: zhipu, qwen, kimi, minimax');
|
||||
}, { name: 'zclaw-chinese-models.startup', description: 'Log provider registration on startup' });
|
||||
}
|
||||
32
plugins/zclaw-chinese-models/plugin.json
Normal file
32
plugins/zclaw-chinese-models/plugin.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"id": "zclaw-chinese-models",
|
||||
"name": "ZCLAW Chinese Models",
|
||||
"version": "0.1.0",
|
||||
"description": "Chinese AI model providers for ZCLAW: Zhipu GLM, Qwen, Kimi, MiniMax",
|
||||
"author": "ZCLAW",
|
||||
"entry": "index.ts",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"zhipuApiKey": { "type": "string" },
|
||||
"zhipuBaseUrl": { "type": "string" },
|
||||
"qwenApiKey": { "type": "string" },
|
||||
"qwenBaseUrl": { "type": "string" },
|
||||
"kimiApiKey": { "type": "string" },
|
||||
"kimiBaseUrl": { "type": "string" },
|
||||
"minimaxApiKey": { "type": "string" },
|
||||
"minimaxBaseUrl": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"uiHints": {
|
||||
"zhipuApiKey": { "label": "智谱 API Key", "sensitive": true },
|
||||
"zhipuBaseUrl": { "label": "智谱 Base URL", "placeholder": "https://open.bigmodel.cn/api/paas/v4" },
|
||||
"qwenApiKey": { "label": "通义千问 API Key", "sensitive": true },
|
||||
"qwenBaseUrl": { "label": "通义千问 Base URL", "placeholder": "https://dashscope.aliyuncs.com/compatible-mode/v1" },
|
||||
"kimiApiKey": { "label": "Kimi API Key", "sensitive": true },
|
||||
"kimiBaseUrl": { "label": "Kimi Base URL", "placeholder": "https://api.moonshot.cn/v1" },
|
||||
"minimaxApiKey": { "label": "MiniMax API Key", "sensitive": true },
|
||||
"minimaxBaseUrl": { "label": "MiniMax Base URL", "placeholder": "https://api.minimax.chat/v1" }
|
||||
}
|
||||
}
|
||||
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" }
|
||||
}
|
||||
}
|
||||
258
plugins/zclaw-ui/index.ts
Normal file
258
plugins/zclaw-ui/index.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* ZCLAW UI Extensions Plugin
|
||||
*
|
||||
* Registers custom Gateway RPC methods that the ZCLAW Tauri desktop UI
|
||||
* calls for features beyond standard OpenClaw protocol.
|
||||
*
|
||||
* Custom methods:
|
||||
* - zclaw.clones.list → list all agent clones (分身)
|
||||
* - zclaw.clones.create → create a new clone
|
||||
* - zclaw.clones.update → update clone config
|
||||
* - zclaw.clones.delete → delete a clone
|
||||
* - zclaw.stats.usage → token usage statistics
|
||||
* - zclaw.stats.sessions → session statistics
|
||||
* - zclaw.config.quick → quick configuration (name/role/scenarios)
|
||||
* - zclaw.workspace.info → workspace information
|
||||
* - zclaw.plugins.status → all ZCLAW plugin statuses
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
interface PluginAPI {
|
||||
config: Record<string, any>;
|
||||
registerGatewayMethod(method: string, handler: (ctx: RpcContext) => void): void;
|
||||
registerHook(event: string, handler: (...args: any[]) => any, meta?: Record<string, any>): void;
|
||||
}
|
||||
|
||||
interface RpcContext {
|
||||
params: Record<string, any>;
|
||||
respond(ok: boolean, payload: any): void;
|
||||
}
|
||||
|
||||
// Clone (分身) management - stored in ZCLAW config
|
||||
interface CloneConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
role?: string;
|
||||
nickname?: string;
|
||||
scenarios?: string[];
|
||||
model?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function register(api: PluginAPI) {
|
||||
const configDir = process.env.OPENCLAW_HOME || path.join(process.env.HOME || process.env.USERPROFILE || '', '.openclaw');
|
||||
const zclawDataPath = path.join(configDir, 'zclaw-data.json');
|
||||
|
||||
// Helper: read/write ZCLAW data
|
||||
function readZclawData(): { clones: CloneConfig[]; quickConfig?: Record<string, any> } {
|
||||
try {
|
||||
if (fs.existsSync(zclawDataPath)) {
|
||||
return JSON.parse(fs.readFileSync(zclawDataPath, 'utf-8'));
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return { clones: [] };
|
||||
}
|
||||
|
||||
function writeZclawData(data: any) {
|
||||
fs.mkdirSync(path.dirname(zclawDataPath), { recursive: true });
|
||||
fs.writeFileSync(zclawDataPath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
// === Clone Management ===
|
||||
|
||||
api.registerGatewayMethod('zclaw.clones.list', ({ respond }: RpcContext) => {
|
||||
const data = readZclawData();
|
||||
respond(true, { clones: data.clones });
|
||||
});
|
||||
|
||||
api.registerGatewayMethod('zclaw.clones.create', ({ params, respond }: RpcContext) => {
|
||||
const data = readZclawData();
|
||||
const clone: CloneConfig = {
|
||||
id: `clone_${Date.now().toString(36)}`,
|
||||
name: params.name || 'New Clone',
|
||||
role: params.role,
|
||||
nickname: params.nickname,
|
||||
scenarios: params.scenarios || [],
|
||||
model: params.model,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
data.clones.push(clone);
|
||||
writeZclawData(data);
|
||||
respond(true, { clone });
|
||||
});
|
||||
|
||||
api.registerGatewayMethod('zclaw.clones.update', ({ params, respond }: RpcContext) => {
|
||||
const data = readZclawData();
|
||||
const index = data.clones.findIndex((c: CloneConfig) => c.id === params.id);
|
||||
if (index === -1) {
|
||||
respond(false, { error: 'Clone not found' });
|
||||
return;
|
||||
}
|
||||
data.clones[index] = { ...data.clones[index], ...params.updates };
|
||||
writeZclawData(data);
|
||||
respond(true, { clone: data.clones[index] });
|
||||
});
|
||||
|
||||
api.registerGatewayMethod('zclaw.clones.delete', ({ params, respond }: RpcContext) => {
|
||||
const data = readZclawData();
|
||||
data.clones = data.clones.filter((c: CloneConfig) => c.id !== params.id);
|
||||
writeZclawData(data);
|
||||
respond(true, { ok: true });
|
||||
});
|
||||
|
||||
// === Statistics ===
|
||||
|
||||
api.registerGatewayMethod('zclaw.stats.usage', ({ respond }: RpcContext) => {
|
||||
// Read session files to compute token usage
|
||||
const sessionsDir = path.join(configDir, 'agents');
|
||||
const stats = {
|
||||
totalSessions: 0,
|
||||
totalMessages: 0,
|
||||
totalTokens: 0,
|
||||
byModel: {} as Record<string, { messages: number; inputTokens: number; outputTokens: number }>,
|
||||
};
|
||||
|
||||
try {
|
||||
if (fs.existsSync(sessionsDir)) {
|
||||
const agentDirs = fs.readdirSync(sessionsDir);
|
||||
for (const agentDir of agentDirs) {
|
||||
const sessDir = path.join(sessionsDir, agentDir, 'sessions');
|
||||
if (!fs.existsSync(sessDir)) continue;
|
||||
const sessionFiles = fs.readdirSync(sessDir).filter((f: string) => f.endsWith('.jsonl'));
|
||||
stats.totalSessions += sessionFiles.length;
|
||||
|
||||
for (const file of sessionFiles) {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(sessDir, file), 'utf-8');
|
||||
const lines = content.split('\n').filter(Boolean);
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
if (entry.role === 'assistant' || entry.role === 'user') {
|
||||
stats.totalMessages++;
|
||||
}
|
||||
if (entry.usage) {
|
||||
const model = entry.model || 'unknown';
|
||||
if (!stats.byModel[model]) {
|
||||
stats.byModel[model] = { messages: 0, inputTokens: 0, outputTokens: 0 };
|
||||
}
|
||||
stats.byModel[model].messages++;
|
||||
stats.byModel[model].inputTokens += entry.usage.input_tokens || 0;
|
||||
stats.byModel[model].outputTokens += entry.usage.output_tokens || 0;
|
||||
stats.totalTokens += (entry.usage.input_tokens || 0) + (entry.usage.output_tokens || 0);
|
||||
}
|
||||
} catch { /* skip malformed lines */ }
|
||||
}
|
||||
} catch { /* skip unreadable files */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* sessions dir may not exist yet */ }
|
||||
|
||||
respond(true, stats);
|
||||
});
|
||||
|
||||
api.registerGatewayMethod('zclaw.stats.sessions', ({ respond }: RpcContext) => {
|
||||
const sessionsDir = path.join(configDir, 'agents');
|
||||
const sessions: any[] = [];
|
||||
|
||||
try {
|
||||
if (fs.existsSync(sessionsDir)) {
|
||||
const agentDirs = fs.readdirSync(sessionsDir);
|
||||
for (const agentDir of agentDirs) {
|
||||
const sessDir = path.join(sessionsDir, agentDir, 'sessions');
|
||||
if (!fs.existsSync(sessDir)) continue;
|
||||
const sessionFiles = fs.readdirSync(sessDir).filter((f: string) => f.endsWith('.jsonl'));
|
||||
for (const file of sessionFiles) {
|
||||
const stat = fs.statSync(path.join(sessDir, file));
|
||||
sessions.push({
|
||||
id: file.replace('.jsonl', ''),
|
||||
agentId: agentDir,
|
||||
createdAt: stat.birthtime.toISOString(),
|
||||
updatedAt: stat.mtime.toISOString(),
|
||||
size: stat.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||||
respond(true, { sessions });
|
||||
});
|
||||
|
||||
// === Quick Configuration ===
|
||||
|
||||
api.registerGatewayMethod('zclaw.config.quick', ({ params, respond }: RpcContext) => {
|
||||
const data = readZclawData();
|
||||
if (params.get) {
|
||||
respond(true, { quickConfig: data.quickConfig || {} });
|
||||
return;
|
||||
}
|
||||
// Save quick config
|
||||
data.quickConfig = {
|
||||
userName: params.userName,
|
||||
userRole: params.userRole,
|
||||
agentNickname: params.agentNickname,
|
||||
scenarios: params.scenarios || [],
|
||||
workspaceDir: params.workspaceDir,
|
||||
};
|
||||
writeZclawData(data);
|
||||
respond(true, { ok: true, quickConfig: data.quickConfig });
|
||||
});
|
||||
|
||||
// === Workspace Info ===
|
||||
|
||||
api.registerGatewayMethod('zclaw.workspace.info', ({ respond }: RpcContext) => {
|
||||
const workspace = api.config?.agents?.defaults?.workspace || '~/.openclaw/zclaw-workspace';
|
||||
const resolvedPath = workspace.replace('~', process.env.HOME || process.env.USERPROFILE || '');
|
||||
|
||||
let exists = false;
|
||||
let fileCount = 0;
|
||||
let totalSize = 0;
|
||||
|
||||
try {
|
||||
exists = fs.existsSync(resolvedPath);
|
||||
if (exists) {
|
||||
const files = fs.readdirSync(resolvedPath, { recursive: true }) as string[];
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(resolvedPath, file);
|
||||
try {
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isFile()) {
|
||||
fileCount++;
|
||||
totalSize += stat.size;
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
respond(true, {
|
||||
path: workspace,
|
||||
resolvedPath,
|
||||
exists,
|
||||
fileCount,
|
||||
totalSize,
|
||||
});
|
||||
});
|
||||
|
||||
// === Plugin Status ===
|
||||
|
||||
api.registerGatewayMethod('zclaw.plugins.status', ({ respond }: RpcContext) => {
|
||||
respond(true, {
|
||||
plugins: [
|
||||
{ id: 'zclaw-chinese-models', status: 'active', version: '0.1.0' },
|
||||
{ id: 'zclaw-feishu', status: api.config?.channels?.feishu?.enabled ? 'active' : 'inactive', version: '0.1.0' },
|
||||
{ id: 'zclaw-ui', status: 'active', version: '0.1.0' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// Startup log
|
||||
api.registerHook('gateway:startup', async () => {
|
||||
console.log('[ZCLAW] UI extension RPC methods registered');
|
||||
}, { name: 'zclaw-ui.startup', description: 'Log ZCLAW UI extensions registration' });
|
||||
}
|
||||
8
plugins/zclaw-ui/plugin.json
Normal file
8
plugins/zclaw-ui/plugin.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "zclaw-ui",
|
||||
"name": "ZCLAW UI Extensions",
|
||||
"version": "0.1.0",
|
||||
"description": "Custom Gateway RPC methods for ZCLAW Tauri desktop UI",
|
||||
"author": "ZCLAW",
|
||||
"entry": "index.ts"
|
||||
}
|
||||
Reference in New Issue
Block a user