diff --git a/Cargo.toml b/Cargo.toml
index e9cbe7b..2c87ad3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ members = [
"crates/erp-plugin-prototype",
"crates/erp-plugin-test-sample",
"crates/erp-plugin",
+ "crates/erp-plugin-crm",
]
[workspace.package]
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index 52520e0..9509e44 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -16,6 +16,8 @@ const Messages = lazy(() => import('./pages/Messages'));
const Settings = lazy(() => import('./pages/Settings'));
const PluginAdmin = lazy(() => import('./pages/PluginAdmin'));
const PluginCRUDPage = lazy(() => import('./pages/PluginCRUDPage'));
+const PluginTabsPage = lazy(() => import('./pages/PluginTabsPage').then((m) => ({ default: m.PluginTabsPage })));
+const PluginTreePage = lazy(() => import('./pages/PluginTreePage').then((m) => ({ default: m.PluginTreePage })));
function PrivateRoute({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
@@ -138,6 +140,8 @@ export default function App() {
} />
} />
} />
+ } />
+ } />
} />
diff --git a/apps/web/src/layouts/MainLayout.tsx b/apps/web/src/layouts/MainLayout.tsx
index 2382793..141ab3b 100644
--- a/apps/web/src/layouts/MainLayout.tsx
+++ b/apps/web/src/layouts/MainLayout.tsx
@@ -15,6 +15,11 @@ import {
BulbOutlined,
BulbFilled,
AppstoreOutlined,
+ TeamOutlined,
+ TableOutlined,
+ TagsOutlined,
+ UserAddOutlined,
+ ApartmentOutlined as RelationshipIcon,
} from '@ant-design/icons';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAppStore } from '../stores/app';
@@ -174,19 +179,36 @@ export default function MainLayout({ children }: { children: React.ReactNode })
<>
{!sidebarCollapsed &&
插件
}
- {pluginMenuItems.map((item) => (
-
,
- label: item.label,
- }}
- isActive={currentPath === item.key}
- collapsed={sidebarCollapsed}
- onClick={() => navigate(item.key)}
- />
- ))}
+ {pluginMenuItems.map((item) => {
+ // 动态图标映射
+ const iconMap: Record
= {
+ AppstoreOutlined: ,
+ team: ,
+ TeamOutlined: ,
+ user: ,
+ UserOutlined: ,
+ message: ,
+ MessageOutlined: ,
+ tags: ,
+ TagsOutlined: ,
+ apartment: ,
+ ApartmentOutlined: ,
+ TableOutlined: ,
+ };
+ return (
+ ,
+ label: item.label,
+ }}
+ isActive={currentPath === item.key}
+ collapsed={sidebarCollapsed}
+ onClick={() => navigate(item.key)}
+ />
+ );
+ })}
>
)}
diff --git a/apps/web/src/pages/PluginTabsPage.tsx b/apps/web/src/pages/PluginTabsPage.tsx
new file mode 100644
index 0000000..b07321d
--- /dev/null
+++ b/apps/web/src/pages/PluginTabsPage.tsx
@@ -0,0 +1,61 @@
+import { useState } from 'react';
+import { Tabs } from 'antd';
+import {
+ PluginPageSchema,
+ PluginEntitySchema,
+ PluginFieldSchema,
+} from '../api/plugins';
+
+interface PluginTabsPageProps {
+ pluginId: string;
+ label: string;
+ icon?: string;
+ tabs: PluginPageSchema[];
+ entities: PluginEntitySchema[];
+}
+
+export function PluginTabsPage({ pluginId, label, tabs, entities }: PluginTabsPageProps) {
+ const [activeKey, setActiveKey] = useState(tabs[0] && 'label' in tabs[0] ? tabs[0].label : '');
+
+ const renderTabContent = (tab: PluginPageSchema) => {
+ if (tab.type === 'crud') {
+ // 懒加载 PluginCRUDPage 避免循环依赖
+ const PluginCRUDPage = require('./PluginCRUDPage').default;
+ return (
+
+ );
+ }
+ if (tab.type === 'tree') {
+ const PluginTreePage = require('./PluginTreePage').PluginTreePage;
+ const entity = entities.find((e) => e.name === tab.entity);
+ return (
+
+ );
+ }
+ return 不支持的页面类型: {tab.type}
;
+ };
+
+ const items = tabs.map((tab) => ({
+ key: 'label' in tab ? tab.label : '',
+ label: 'label' in tab ? tab.label : '',
+ children: renderTabContent(tab),
+ }));
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/PluginTreePage.tsx b/apps/web/src/pages/PluginTreePage.tsx
new file mode 100644
index 0000000..35fcc0f
--- /dev/null
+++ b/apps/web/src/pages/PluginTreePage.tsx
@@ -0,0 +1,142 @@
+import { useEffect, useState, useMemo } from 'react';
+import { Tree, Descriptions, Card, Empty, Spin } from 'antd';
+import type { TreeProps } from 'antd';
+import { listPluginData, PluginDataRecord } from '../api/pluginData';
+import { PluginFieldSchema } from '../api/plugins';
+
+interface PluginTreePageProps {
+ pluginId: string;
+ entity: string;
+ idField: string;
+ parentField: string;
+ labelField: string;
+ fields: PluginFieldSchema[];
+}
+
+interface TreeNode {
+ key: string;
+ title: string;
+ children: TreeNode[];
+ raw: Record;
+}
+
+export function PluginTreePage({
+ pluginId,
+ entity,
+ idField,
+ parentField,
+ labelField,
+ fields,
+}: PluginTreePageProps) {
+ const [records, setRecords] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [selectedNode, setSelectedNode] = useState(null);
+
+ useEffect(() => {
+ async function loadAll() {
+ setLoading(true);
+ try {
+ // 加载全量数据构建树
+ let allRecords: PluginDataRecord[] = [];
+ let page = 1;
+ let hasMore = true;
+ while (hasMore) {
+ const result = await listPluginData(pluginId, entity, page, 100);
+ allRecords = [...allRecords, ...result.data];
+ hasMore = result.data.length === 100 && allRecords.length < result.total;
+ page++;
+ }
+ setRecords(allRecords);
+ } catch {
+ // 加载失败
+ }
+ setLoading(false);
+ }
+ loadAll();
+ }, [pluginId, entity]);
+
+ // 构建树结构
+ const treeData = useMemo(() => {
+ const nodeMap = new Map();
+ const rootNodes: TreeNode[] = [];
+
+ // 创建所有节点
+ for (const record of records) {
+ const data = record.data;
+ const key = String(data[idField] || record.id);
+ const title = String(data[labelField] || '未命名');
+ nodeMap.set(key, {
+ key,
+ title,
+ children: [],
+ raw: { ...data, _id: record.id, _version: record.version },
+ });
+ }
+
+ // 构建父子关系
+ for (const record of records) {
+ const data = record.data;
+ const key = String(data[idField] || record.id);
+ const parentKey = data[parentField] ? String(data[parentField]) : null;
+ const node = nodeMap.get(key)!;
+
+ if (parentKey && nodeMap.has(parentKey)) {
+ nodeMap.get(parentKey)!.children.push(node);
+ } else {
+ rootNodes.push(node);
+ }
+ }
+
+ return rootNodes;
+ }, [records, idField, parentField, labelField]);
+
+ const onSelect: TreeProps['onSelect'] = (selectedKeys, info) => {
+ if (selectedKeys.length > 0) {
+ setSelectedNode(info.node as unknown as TreeNode);
+ } else {
+ setSelectedNode(null);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {treeData.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {selectedNode ? (
+
+ {fields.map((field) => (
+
+ {String(selectedNode.raw[field.name] ?? '-')}
+
+ ))}
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/web/src/stores/plugin.ts b/apps/web/src/stores/plugin.ts
index 4ba5d6a..459170c 100644
--- a/apps/web/src/stores/plugin.ts
+++ b/apps/web/src/stores/plugin.ts
@@ -1,13 +1,14 @@
import { create } from 'zustand';
-import type { PluginInfo, PluginStatus } from '../api/plugins';
-import { listPlugins } from '../api/plugins';
+import type { PluginInfo, PluginStatus, PluginPageSchema, PluginSchemaResponse } from '../api/plugins';
+import { listPlugins, getPluginSchema } from '../api/plugins';
export interface PluginMenuItem {
key: string;
icon: string;
label: string;
pluginId: string;
- entity: string;
+ entity?: string;
+ pageType: 'crud' | 'tree' | 'tabs' | 'detail';
menuGroup?: string;
}
@@ -15,6 +16,7 @@ interface PluginStore {
plugins: PluginInfo[];
loading: boolean;
pluginMenuItems: PluginMenuItem[];
+ schemaCache: Record;
fetchPlugins: (page?: number, status?: PluginStatus) => Promise;
refreshMenuItems: () => void;
}
@@ -23,12 +25,25 @@ export const usePluginStore = create((set, get) => ({
plugins: [],
loading: false,
pluginMenuItems: [],
+ schemaCache: {},
fetchPlugins: async (page = 1, status?: PluginStatus) => {
set({ loading: true });
try {
const result = await listPlugins(page, 100, status);
set({ plugins: result.data });
+
+ // 预加载所有运行中插件的 schema
+ const schemas: Record = {};
+ for (const plugin of result.data) {
+ if (plugin.status !== 'running' && plugin.status !== 'enabled') continue;
+ try {
+ schemas[plugin.id] = await getPluginSchema(plugin.id) as PluginSchemaResponse;
+ } catch {
+ // schema 加载失败跳过
+ }
+ }
+ set({ schemaCache: schemas });
get().refreshMenuItems();
} finally {
set({ loading: false });
@@ -36,21 +51,59 @@ export const usePluginStore = create((set, get) => ({
},
refreshMenuItems: () => {
- const { plugins } = get();
+ const { plugins, schemaCache } = get();
const items: PluginMenuItem[] = [];
for (const plugin of plugins) {
if (plugin.status !== 'running' && plugin.status !== 'enabled') continue;
- for (const entity of plugin.entities) {
- items.push({
- key: `/plugins/${plugin.id}/${entity.name}`,
- icon: 'AppstoreOutlined',
- label: entity.display_name || entity.name,
- pluginId: plugin.id,
- entity: entity.name,
- menuGroup: undefined,
- });
+ const schema = schemaCache[plugin.id];
+ const pages = (schema as { ui?: { pages: PluginPageSchema[] } })?.ui?.pages;
+
+ if (pages && pages.length > 0) {
+ for (const page of pages) {
+ if (page.type === 'tabs') {
+ // tabs 类型聚合为一个菜单项
+ items.push({
+ key: `/plugins/${plugin.id}/tabs/${encodeURIComponent('label' in page ? page.label : '')}`,
+ icon: ('icon' in page ? page.icon : 'AppstoreOutlined') || 'AppstoreOutlined',
+ label: ('label' in page ? page.label : plugin.name) as string,
+ pluginId: plugin.id,
+ pageType: 'tabs',
+ });
+ } else if (page.type === 'tree') {
+ items.push({
+ key: `/plugins/${plugin.id}/tree/${page.entity}`,
+ icon: ('icon' in page ? page.icon : 'ApartmentOutlined') || 'ApartmentOutlined',
+ label: ('label' in page ? page.label : page.entity) as string,
+ pluginId: plugin.id,
+ entity: page.entity,
+ pageType: 'tree',
+ });
+ } else if (page.type === 'crud') {
+ items.push({
+ key: `/plugins/${plugin.id}/${page.entity}`,
+ icon: ('icon' in page ? page.icon : 'TableOutlined') || 'TableOutlined',
+ label: ('label' in page ? page.label : page.entity) as string,
+ pluginId: plugin.id,
+ entity: page.entity,
+ pageType: 'crud',
+ });
+ }
+ // detail 类型不生成菜单项
+ }
+ } else {
+ // 回退:从 entities 生成菜单
+ for (const entity of plugin.entities) {
+ items.push({
+ key: `/plugins/${plugin.id}/${entity.name}`,
+ icon: 'AppstoreOutlined',
+ label: entity.display_name || entity.name,
+ pluginId: plugin.id,
+ entity: entity.name,
+ pageType: 'crud',
+ });
+ }
}
}
diff --git a/crates/erp-plugin-crm/Cargo.toml b/crates/erp-plugin-crm/Cargo.toml
new file mode 100644
index 0000000..0b6d415
--- /dev/null
+++ b/crates/erp-plugin-crm/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "erp-plugin-crm"
+version = "0.1.0"
+edition = "2024"
+description = "CRM 客户管理插件 — ERP 平台第一个行业插件"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+wit-bindgen = "0.55"
+serde = { workspace = true }
+serde_json = { workspace = true }
diff --git a/crates/erp-plugin-crm/plugin.toml b/crates/erp-plugin-crm/plugin.toml
new file mode 100644
index 0000000..77c44a0
--- /dev/null
+++ b/crates/erp-plugin-crm/plugin.toml
@@ -0,0 +1,409 @@
+[metadata]
+id = "erp-crm"
+name = "客户管理"
+version = "0.1.0"
+description = "客户关系管理插件 — ERP 平台第一个行业插件"
+author = "ERP Team"
+min_platform_version = "0.1.0"
+
+# ── 权限声明 ──
+
+[[permissions]]
+code = "customer.list"
+name = "查看客户"
+description = "查看客户列表和详情"
+
+[[permissions]]
+code = "customer.manage"
+name = "管理客户"
+description = "创建、编辑、删除客户"
+
+[[permissions]]
+code = "contact.list"
+name = "查看联系人"
+
+[[permissions]]
+code = "contact.manage"
+name = "管理联系人"
+
+[[permissions]]
+code = "communication.list"
+name = "查看沟通记录"
+
+[[permissions]]
+code = "communication.manage"
+name = "管理沟通记录"
+
+[[permissions]]
+code = "tag.manage"
+name = "管理客户标签"
+
+[[permissions]]
+code = "relationship.list"
+name = "查看客户关系"
+
+[[permissions]]
+code = "relationship.manage"
+name = "管理客户关系"
+
+# ── 实体定义 ──
+
+[[schema.entities]]
+name = "customer"
+display_name = "客户"
+
+ [[schema.entities.fields]]
+ name = "code"
+ field_type = "String"
+ required = true
+ display_name = "客户编码"
+ unique = true
+ searchable = true
+
+ [[schema.entities.fields]]
+ name = "name"
+ field_type = "String"
+ required = true
+ display_name = "客户名称"
+ searchable = true
+
+ [[schema.entities.fields]]
+ name = "customer_type"
+ field_type = "String"
+ required = true
+ display_name = "客户类型"
+ ui_widget = "select"
+ filterable = true
+ options = [
+ { label = "企业", value = "enterprise" },
+ { label = "个人", value = "personal" }
+ ]
+
+ [[schema.entities.fields]]
+ name = "industry"
+ field_type = "String"
+ display_name = "行业"
+ filterable = true
+
+ [[schema.entities.fields]]
+ name = "region"
+ field_type = "String"
+ display_name = "地区"
+ filterable = true
+
+ [[schema.entities.fields]]
+ name = "source"
+ field_type = "String"
+ display_name = "来源"
+ ui_widget = "select"
+ options = [
+ { label = "推荐", value = "referral" },
+ { label = "广告", value = "ad" },
+ { label = "展会", value = "exhibition" },
+ { label = "主动联系", value = "outreach" },
+ { label = "其他", value = "other" }
+ ]
+
+ [[schema.entities.fields]]
+ name = "level"
+ field_type = "String"
+ display_name = "等级"
+ ui_widget = "select"
+ filterable = true
+ options = [
+ { label = "潜在客户", value = "potential" },
+ { label = "普通客户", value = "normal" },
+ { label = "VIP", value = "vip" },
+ { label = "SVIP", value = "svip" }
+ ]
+
+ [[schema.entities.fields]]
+ name = "status"
+ field_type = "String"
+ required = true
+ display_name = "状态"
+ ui_widget = "select"
+ filterable = true
+ options = [
+ { label = "活跃", value = "active" },
+ { label = "停用", value = "inactive" },
+ { label = "黑名单", value = "blacklist" }
+ ]
+
+ [[schema.entities.fields]]
+ name = "credit_code"
+ field_type = "String"
+ display_name = "统一社会信用代码"
+ visible_when = "customer_type == 'enterprise'"
+
+ [[schema.entities.fields]]
+ name = "id_number"
+ field_type = "String"
+ display_name = "身份证号"
+ visible_when = "customer_type == 'personal'"
+
+ [[schema.entities.fields]]
+ name = "parent_id"
+ field_type = "Uuid"
+ display_name = "上级客户"
+
+ [[schema.entities.fields]]
+ name = "website"
+ field_type = "String"
+ display_name = "网站"
+
+ [[schema.entities.fields]]
+ name = "address"
+ field_type = "String"
+ display_name = "地址"
+
+ [[schema.entities.fields]]
+ name = "remark"
+ field_type = "String"
+ display_name = "备注"
+ ui_widget = "textarea"
+
+[[schema.entities]]
+name = "contact"
+display_name = "联系人"
+
+ [[schema.entities.fields]]
+ name = "customer_id"
+ field_type = "Uuid"
+ required = true
+ display_name = "所属客户"
+
+ [[schema.entities.fields]]
+ name = "name"
+ field_type = "String"
+ required = true
+ display_name = "姓名"
+ searchable = true
+
+ [[schema.entities.fields]]
+ name = "position"
+ field_type = "String"
+ display_name = "职务"
+
+ [[schema.entities.fields]]
+ name = "department"
+ field_type = "String"
+ display_name = "部门"
+
+ [[schema.entities.fields]]
+ name = "phone"
+ field_type = "String"
+ display_name = "手机号"
+
+ [[schema.entities.fields]]
+ name = "email"
+ field_type = "String"
+ display_name = "邮箱"
+
+ [[schema.entities.fields]]
+ name = "wechat"
+ field_type = "String"
+ display_name = "微信号"
+
+ [[schema.entities.fields]]
+ name = "is_primary"
+ field_type = "Boolean"
+ display_name = "主联系人"
+
+ [[schema.entities.fields]]
+ name = "remark"
+ field_type = "String"
+ display_name = "备注"
+
+[[schema.entities]]
+name = "communication"
+display_name = "沟通记录"
+
+ [[schema.entities.fields]]
+ name = "customer_id"
+ field_type = "Uuid"
+ required = true
+ display_name = "关联客户"
+
+ [[schema.entities.fields]]
+ name = "contact_id"
+ field_type = "Uuid"
+ display_name = "关联联系人"
+
+ [[schema.entities.fields]]
+ name = "type"
+ field_type = "String"
+ required = true
+ display_name = "类型"
+ ui_widget = "select"
+ filterable = true
+ options = [
+ { label = "电话", value = "phone" },
+ { label = "邮件", value = "email" },
+ { label = "会议", value = "meeting" },
+ { label = "拜访", value = "visit" },
+ { label = "其他", value = "other" }
+ ]
+
+ [[schema.entities.fields]]
+ name = "subject"
+ field_type = "String"
+ required = true
+ display_name = "主题"
+ searchable = true
+
+ [[schema.entities.fields]]
+ name = "content"
+ field_type = "String"
+ required = true
+ display_name = "内容"
+ ui_widget = "textarea"
+
+ [[schema.entities.fields]]
+ name = "occurred_at"
+ field_type = "DateTime"
+ required = true
+ display_name = "沟通时间"
+ sortable = true
+
+ [[schema.entities.fields]]
+ name = "next_follow_up"
+ field_type = "Date"
+ display_name = "下次跟进日期"
+
+[[schema.entities]]
+name = "customer_tag"
+display_name = "客户标签"
+
+ [[schema.entities.fields]]
+ name = "customer_id"
+ field_type = "Uuid"
+ required = true
+ display_name = "关联客户"
+
+ [[schema.entities.fields]]
+ name = "tag_name"
+ field_type = "String"
+ required = true
+ display_name = "标签名称"
+ searchable = true
+
+ [[schema.entities.fields]]
+ name = "tag_category"
+ field_type = "String"
+ display_name = "标签分类"
+ ui_widget = "select"
+ options = [
+ { label = "行业", value = "industry" },
+ { label = "地区", value = "region" },
+ { label = "来源", value = "source" },
+ { label = "自定义", value = "custom" }
+ ]
+
+[[schema.entities]]
+name = "customer_relationship"
+display_name = "客户关系"
+
+ [[schema.entities.fields]]
+ name = "from_customer_id"
+ field_type = "Uuid"
+ required = true
+ display_name = "源客户"
+
+ [[schema.entities.fields]]
+ name = "to_customer_id"
+ field_type = "Uuid"
+ required = true
+ display_name = "目标客户"
+
+ [[schema.entities.fields]]
+ name = "relationship_type"
+ field_type = "String"
+ required = true
+ display_name = "关系类型"
+ ui_widget = "select"
+ filterable = true
+ options = [
+ { label = "母子公司", value = "parent_child" },
+ { label = "兄弟公司", value = "sibling" },
+ { label = "合作伙伴", value = "partner" },
+ { label = "供应商", value = "supplier" },
+ { label = "竞争对手", value = "competitor" }
+ ]
+
+ [[schema.entities.fields]]
+ name = "description"
+ field_type = "String"
+ display_name = "关系描述"
+
+# ── 页面声明 ──
+
+[[ui.pages]]
+type = "tabs"
+label = "客户管理"
+icon = "team"
+
+ [[ui.pages.tabs]]
+ label = "客户列表"
+ type = "crud"
+ entity = "customer"
+ enable_search = true
+ enable_views = ["table"]
+
+ [[ui.pages.tabs]]
+ label = "客户层级"
+ type = "tree"
+ entity = "customer"
+ id_field = "id"
+ parent_field = "parent_id"
+ label_field = "name"
+
+[[ui.pages]]
+type = "detail"
+entity = "customer"
+label = "客户详情"
+
+ [[ui.pages.sections]]
+ type = "fields"
+ label = "基本信息"
+ fields = ["code", "name", "customer_type", "industry", "region", "level", "status", "credit_code", "id_number", "website", "address", "remark"]
+
+ [[ui.pages.sections]]
+ type = "crud"
+ label = "联系人"
+ entity = "contact"
+ filter_field = "customer_id"
+
+ [[ui.pages.sections]]
+ type = "crud"
+ label = "沟通记录"
+ entity = "communication"
+ filter_field = "customer_id"
+ enable_views = ["table", "timeline"]
+
+[[ui.pages]]
+type = "crud"
+entity = "contact"
+label = "联系人"
+icon = "user"
+enable_search = true
+
+[[ui.pages]]
+type = "crud"
+entity = "communication"
+label = "沟通记录"
+icon = "message"
+enable_search = true
+enable_views = ["table", "timeline"]
+
+[[ui.pages]]
+type = "crud"
+entity = "customer_tag"
+label = "标签管理"
+icon = "tags"
+
+[[ui.pages]]
+type = "crud"
+entity = "customer_relationship"
+label = "客户关系"
+icon = "apartment"
diff --git a/crates/erp-plugin-crm/src/lib.rs b/crates/erp-plugin-crm/src/lib.rs
new file mode 100644
index 0000000..b08ee39
--- /dev/null
+++ b/crates/erp-plugin-crm/src/lib.rs
@@ -0,0 +1,29 @@
+//! CRM 客户管理插件 — WASM Guest 实现
+
+wit_bindgen::generate!({
+ path: "../erp-plugin-prototype/wit/plugin.wit",
+ world: "plugin-world",
+});
+
+use crate::exports::erp::plugin::plugin_api::Guest;
+
+struct CrmPlugin;
+
+impl Guest for CrmPlugin {
+ fn init() -> Result<(), String> {
+ // CRM 插件初始化:当前无需创建默认数据
+ Ok(())
+ }
+
+ fn on_tenant_created(_tenant_id: String) -> Result<(), String> {
+ // 为新租户创建 CRM 默认数据:当前无需创建默认客户
+ Ok(())
+ }
+
+ fn handle_event(_event_type: String, _payload: Vec) -> Result<(), String> {
+ // CRM V1: 无事件处理
+ Ok(())
+ }
+}
+
+export!(CrmPlugin);