Skip to content

插件工作台卡片 (Plugin Widget Cards) ​

插件可以通过 contributes.widgetCards 声明工作台卡片,将数据展示在用户工作台首页。

上一篇:UI 组件开发 升级方案:插件卡片功能升级


概述 ​

工作台卡片是插件向用户展示实时数据的入口,位于工作台(macOS 桌面小组件风格)中,与内置工具卡并列显示。

核心能力:

  • 通过插件 MCP 工具按需拉取数据
  • 自动刷新(core 15s / enhanced 60s)
  • 手动刷新按钮
  • 不可见时暂停刷新(IntersectionObserver)
  • 增量数据合并
  • 卡片库集成:用户可通过「添加卡片」弹层选择插件卡片

卡片添加流程 ​

用户点击 "+" 添加卡片
        ↓
    打开 CardLibrary(添加卡片弹层)
        ↓
    ┌─────────────────────────────────────┐
    │  通用操作卡                           │
    │  ├─ 新建任务                         │
    │  └─ ...                             │
    ├─────────────────────────────────────┤
    │  工作工具(当前模式)                 │
    │  ├─ 待办清单                         │
    │  └─ ...                             │
    ├─────────────────────────────────────┤
    │  插件卡片                            │
    │  ├─ 天气卡片 (weather-widget)        │
    │  ├─ 宏统计 (ai-macro-recorder)       │
    │  └─ ...                             │
    └─────────────────────────────────────┘
        ↓
    选择插件卡片 → 添加到工作台
        ↓
    PluginCardPreview 自动渲染并加载数据

配置方式 ​

清单文件 (manifest.json) ​

json
{
  "id": "ai-macro-recorder",
  "name": "AI Macro Recorder",
  "nameZh": "AI 宏录制器",
  "contributes": {
    "widgetCards": [
      {
        "id": "ai-macro-recorder:stats",
        "icon": "CirclePlay",
        "label": "Macro Stats",
        "labelZh": "录制统计",
        "description": "总录制次数: {{count}}",
        "descriptionZh": "总录制次数:{{count}}",
        "tier": "core",
        "previewComponent": "MacroCardPreview",
        "modes": ["work"]
      }
    ]
  }
}

widgetCards 字段说明 ​

字段类型必填说明
idstring是卡片唯一标识,格式 "<pluginKey>:<cardName>",如 "ai-macro-recorder:stats"
iconstring是lucide-react 图标名(通过 IconMap 解析)
labelstring是英文标题
labelZhstring是中文标题
descriptionstring否英文描述(卡片底部显示),支持 占位符
descriptionZhstring否中文描述
tier'core' | 'enhanced'否刷新优先级:'core' = 15s,'enhanced' = 60s(默认)
previewComponentstring是ui.js 中导出的预览组件键名
modesSceneMode[]否关联的场景模式:['work'] / ['life'] / ['study'];不填则所有模式可见

卡片库集成 ​

自动显示 ​

插件安装后,其声明的 widgetCards 会自动出现在「添加卡片」弹层的「插件卡片」分组中:

  • 模式过滤:声明了 modes 的卡片只在对应模式下显示
  • 来源标识:显示插件 key 便于用户识别
  • 已添加标识:已添加的卡片显示勾选图标并禁用

添加方式 ​

  1. 点击工作台右上角「+」按钮打开 CardLibrary
  2. 在「插件卡片」分组中选择要添加的卡片
  3. 卡片自动添加到工作台并开始加载数据

移除方式 ​

  • 右键卡片 → 选择「移除」
  • 或点击卡片右上角的「×」按钮

示例:CardLibrary 中的插件卡片 ​

typescript
// CardLibrary 组件中获取插件卡片
const pluginCards = useMemo(() => {
  const result = []
  
  // 遍历所有已发现的插件
  for (const [pluginKey, contribution] of pluginUiRegistry.discovered) {
    if (!contribution.contributes.widgetCards?.length) continue
    
    for (const wc of contribution.contributes.widgetCards) {
      // 模式过滤
      if (wc.modes?.length && !wc.modes.includes(mode)) continue
      
      result.push({
        pluginKey,
        cardId: wc.id,
        label: wc.label,
        labelZh: wc.labelZh,
        icon: wc.icon,
      })
    }
  }
  
  return result
}, [mode])

预览组件开发 ​

预览组件是插件负责渲染卡片内容的 React 组件,通过 ui.js 导出。

组件接口 ​

typescript
import type { WidgetCardPreviewProps } from '@aweeclaw/plugin-sdk'

interface MacroCardPreviewProps extends WidgetCardPreviewProps {
  // host.callTool('getCardData', { cardId }) → { success, content }
  // data 为 getCardData 返回的 JSON 解析结果
}

WidgetCardPreviewProps ​

字段类型说明
hostPluginHostApi插件宿主 API,可调用 callTool、getConfig 等
dataRecord<string, unknown> | null卡片数据,由框架自动拉取并增量合并
loadingboolean是否正在加载
onRefresh() => void触发手动刷新

完整示例 ​

typescript
// ui.js
import React from 'react'
import { Loader2, RefreshCw } from 'lucide-react'

/**
 * MacroCardPreview — 宏录制统计卡片
 *
 * 数据通过 getCardData MCP 工具拉取。
 * 框架自动处理:首次全量加载 → 后续增量合并 → 不可见暂停刷新。
 */
export function MacroCardPreview({ host, data, loading, onRefresh }) {
  const count = data?.totalCount ?? 0
  const isZh = host.language === 'zh'

  return (
    <div className="flex flex-col w-full h-full gap-1">
      {/* 主数据 */}
      {loading ? (
        <div className="flex items-center gap-2 text-[11px] text-text-muted">
          <Loader2 className="w-3 h-3 animate-spin" />
          <span>{isZh ? '加载中...' : 'Loading...'}</span>
        </div>
      ) : (
        <div className="flex items-baseline gap-1.5">
          <span className="text-[22px] font-semibold leading-none text-text-primary">
            {count}
          </span>
          <span className="text-[11px] text-text-muted">{isZh ? '次录制' : ' macros'}</span>
        </div>
      )}

      {/* 辅助信息 */}
      <div className="flex items-center justify-between mt-auto">
        <span className="text-[10px] text-text-muted/70">
          {isZh ? '累计录制' : 'Total recordings'}
        </span>
        <button
          onClick={onRefresh}
          title={isZh ? '刷新' : 'Refresh'}
          className="p-0.5 rounded text-text-muted/60 hover:text-accent transition-colors"
        >
          <RefreshCw className="w-3 h-3" strokeWidth={2} />
        </button>
      </div>
    </div>
  )
}

// 导出组件映射
export default {
  components: {
    MacroPanel: MacroPanel,        // 侧边栏面板
    MacroCardPreview,              // 工作台卡片预览
  },
  widgetCards: {
    MacroCardPreview,              // widgetCards 专用映射(与 components 同名 key 均可)
  },
}

注意:widgetCards 映射中的组件名称必须与 manifest.contributes.widgetCards[].previewComponent 一致。


数据接口约定 ​

getCardData 工具 ​

框架通过 host.callTool('getCardData', { cardId }) 拉取数据。

插件需在 manifest 中声明此工具,或在本插件的 MCP server 中实现:

typescript
// 插件 main.js 中注册工具
const tool = {
  name: 'getCardData',
  description: '获取工作台卡片数据',
  parameters: {
    cardId: { type: 'string', description: '卡片 ID' },
  },
  handler: async (params) => {
    const { cardId } = params
    const data = await fetchCardData(cardId)  // 插件自己的逻辑
    return JSON.stringify(data)
  },
}

返回格式 ​

工具应返回 JSON 字符串,框架会自动解析并增量合并:

json
{
  "totalCount": 42,
  "lastRecording": "2026-09-02T14:30:00Z",
  "status": "ready"
}

增量合并规则:

  • 首次:data = 返回结果
  • 后续:data = { ...prevData, ...newData }(新字段覆盖旧字段,不存在的字段保留)

刷新策略详解 ​

tier 分层 ​

tier刷新间隔适用场景
core15s核心数据卡片,需要较高实时性(如待办数、当前状态)
enhanced(默认)60s一般数据卡片(如统计汇总、历史记录)

可见性感知 ​

  • 卡片进入视口 → 开始刷新
  • 卡片移出视口 → 暂停刷新(IntersectionObserver,threshold: 0.1)
  • 卡片再次进入 → 恢复刷新

手动刷新 ​

  • 卡片右上角显示 RotateCcw 图标(仅插件卡片)
  • 点击后立即触发 getCardData 并重新渲染
  • 同时会 dispatch plugin-card-refresh 事件(cardId 为 detail)

完整插件示例 ​

manifest.json ​

json
{
  "id": "weather-widget",
  "name": "Weather Widget",
  "nameZh": "天气卡片",
  "icon": "Cloud",
  "type": "tool",
  "version": "1.0.0",
  "permissions": ["network"],
  "contributes": {
    "widgetCards": [
      {
        "id": "weather-widget:current",
        "icon": "CloudSun",
        "label": "Weather",
        "labelZh": "天气",
        "tier": "core",
        "previewComponent": "WeatherCardPreview",
        "modes": ["life"]
      }
    ]
  }
}

ui.js ​

javascript
import React from 'react'
import { CloudSun, CloudRain, Wind, Droplets } from 'lucide-react'

function WeatherCardPreview({ host, data, loading, onRefresh }) {
  const isZh = host.language === 'zh'
  const temp = data?.temperature ?? '--'
  const condition = data?.condition ?? 'unknown'
  const iconMap = { sunny: CloudSun, rainy: CloudRain, windy: Wind, humid: Droplets }
  const Icon = iconMap[condition] ?? CloudSun

  return (
    <div className="flex flex-col w-full h-full">
      <div className="flex items-center gap-2">
        <Icon className="w-5 h-5 text-yellow-400" strokeWidth={1.8} />
        <span className="text-[20px] font-semibold leading-none">{temp}°C</span>
        <span className="text-[11px] text-text-muted ml-auto">{condition}</span>
      </div>
      {data?.humidity !== undefined && (
        <div className="flex items-center gap-1 mt-1 text-[10px] text-text-muted">
          <Droplets className="w-3 h-3" />
          <span>{data.humidity}%</span>
          <span className="mx-1">·</span>
          <Wind className="w-3 h-3" />
          <span>{data?.windSpeed ?? '--'} km/h</span>
        </div>
      )}
      <div className="mt-auto flex justify-end">
        <button
          onClick={onRefresh}
          className="text-[10px] text-text-muted/60 hover:text-accent"
        >
          {isZh ? '刷新' : 'Refresh'}
        </button>
      </div>
    </div>
  )
}

export default {
  components: { WeatherCardPreview },
  widgetCards: { WeatherCardPreview },
}

main.js(注册 getCardData 工具) ​

javascript
export function getPlugin() {
  return {
    name: 'weather-widget',
    tools: [
      {
        name: 'getCardData',
        description: '获取天气卡片数据',
        parameters: {
          cardId: { type: 'string', description: '卡片 ID' },
        },
        handler: async (params, ctx) => {
          const { cardId } = params
          if (cardId === 'weather-widget:current') {
            const res = await fetch('https://api.weather.com/current')
            const data = await res.json()
            return JSON.stringify({
              temperature: data.temp,
              condition: data.condition,
              humidity: data.humidity,
              windSpeed: data.windSpeed,
            })
          }
          return JSON.stringify({})
        },
      },
    ],
  }
}

注意事项 ​

  1. 卡片 id 规范:必须为 <pluginKey>:<cardName> 格式,作为工作台卡片 id 使用
  2. 预览组件必须导出:ui.js 需在 widgetCards 映射中声明对应组件,否则卡片不显示
  3. 数据序列化:getCardData 返回 JSON 字符串,框架自动解析;解析失败时按字符串处理
  4. 不可见时省电:卡片滚出视口后自动停止轮询,避免无效请求
  5. modes 约束:声明了 modes 的卡片只在对应场景模式下显示

AweeClaw AI 应用构建平台