Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

oh-my-opencode-slim 架构深度解析:从理解到自建 Plugin(插件)

适合读者: 中级 Agent(智能体) 开发者 · Plugin(插件) 作者 · 技术负责人

本文是“从使用 slim 到理解 slim“的桥接文章。如果你已经了解 slim 的基本用法(见 oh-my-opencode-slim:轻量级 Agent 编排方案)和 Plugin API 基础(见 自定义 Agent(智能体)Plugin(插件)),本文将带你深入 slim 的代码架构,读懂它的设计模式,最终能够自建类似的 OpenCode Plugin。

文章概述

oh-my-opencode-slim(以下简称 slim)表面上是一个“开箱即用的轻量编排插件“,但它的代码组织方式本身就是一份优秀的 Plugin 开发参考。本文从三个递进层次展开:

  1. 解剖 slim — 分析 slim 如何利用 OpenCode Plugin API 实现 Hub-and-Spoke 架构,它的 Plugin 入口、Agent 注册、任务分发机制
  2. 提取模式 — 从 slim 的关键特性(Preset、LazySkills、Council、Background Agents)中抽象出可复用的 Plugin 设计模式
  3. 自建 Plugin — 基于 slim 的模式,构建一个你自己的轻量编排 Plugin

⏱ 时间有限?先读这些: Part 2(Agent 编排的 Plugin 实现)→ Part 3(关键特性的代码模式)→ Part 5(实战示例)

前置知识

slim 的 Plugin 架构全景

从 Plugin 入口到 Agent 就绪

slim 本质上是一个 OpenCode Plugin。它的入口文件(简化示意)遵循标准的 definePlugin 结构:

// 简化版本的 slim Plugin 入口结构
import { definePlugin } from "opencode";
import { HubOrchestrator } from "./orchestrator";
import { ExplorerAgent, CoderAgent, ReviewerAgent, DebuggerAgent } from "./agents";
import { CompanionAgent, ReflectAgent } from "./background-agents";
import { PresetManager } from "./presets";
import { LazySkillLoader } from "./lazy-skills";

export default definePlugin({
  name: "oh-my-opencode-slim",
  description: "轻量级 Agent 编排插件",
  version: "2.0.0",

  /**
   * 启动时初始化 Hub 编排器和所有 Agent
   */
  async onActivate(context) {
    const preset = PresetManager.load(context.config.preset || "opencode-go");
    const hub = new HubOrchestrator({
      preset,
      config: context.config,
      logger: context.logger,
    });

    // 注册 7 个 Agent 到 Hub
    hub.register("sisyphus", new SisyphusCoordinator(hub, preset));
    hub.register("explorer", new ExplorerAgent(preset.getModelConfig("explorer")));
    hub.register("coder", new CoderAgent(preset.getModelConfig("coder")));
    hub.register("reviewer", new ReviewerAgent(preset.getModelConfig("reviewer")));
    hub.register("debugger", new DebuggerAgent(preset.getModelConfig("debugger")));

    // Companion 和 Reflect 作为后台 Agent 注册
    hub.registerBackground(new CompanionAgent(preset));
    hub.registerBackground(new ReflectAgent(preset));

    // 暴露 Hub 实例供其他 Plugin 工具调用
    context.services.register("slim:hub", hub);
  },

  hooks: {
    /**
     * 拦截用户消息,通过 Sisyphus 分发到合适的 Agent
     */
    "session:beforeProcessMessage": async ({ message }, context) => {
      const hub = context.services.get("slim:hub");
      return hub.dispatch(message);
    },

    /**
     * Companion 的后台心跳
     */
    "session:tick": async (_, context) => {
      const hub = context.services.get("slim:hub");
      await hub.tickBackgroundAgents();
    },
  },

  tools: [
    {
      name: "slim:get_council_consensus",
      description: "多模型共识评估:对高风险决策进行交叉验证",
      parameters: {
        type: "object",
        properties: {
          question: { type: "string", description: "需要共识的决策问题" },
          options: {
            type: "array",
            items: { type: "string" },
            description: "待评估的选项列表",
          },
          min_confidence: {
            type: "number",
            description: "最低置信度阈值(0-1)",
            default: 0.7,
          },
        },
        required: ["question", "options"],
      },
      handler: async (params, context) => {
        const hub = context.services.get("slim:hub");
        return hub.council.consensus(params.question, params.options, params.min_confidence);
      },
    },
  ],
});

这段代码揭示了 slim 作为 Plugin 的三个设计决策:

  1. onActivate 初始化 — 在 Plugin 加载时完成全部初始化(Hub 创建、Agent 注册、Preset 加载),而非在运行时懒加载。这保证了 Agent 就绪后零延迟响应。
  2. Hook 点选择 — 只用了 session:beforeProcessMessagesession:tick 两个 Hook 点,避免过度 Hook。前者拦截用户消息进行分发,后者为后台 Agent 提供心跳驱动。
  3. services.register 模式 — 通过 Plugin API 的 Service Registry 暴露 Hub 实例,让自定义 Tool 可以访问编排器状态。这是一种轻量的依赖注入模式。

以下时序图展示了 slim Plugin 从加载到处理用户消息的完整生命周期:

sequenceDiagram
    participant OpenCode as OpenCode 主进程
    participant Plugin as slim Plugin
    participant Hub as Hub Orchestrator
    participant Agent as 子 Agent
    participant LazySkills as LazySkills 加载器

    OpenCode->>Plugin: 加载 Plugin(onActivate)
    Plugin->>Hub: 创建 HubOrchestrator
    Hub->>Hub: 注册 5 个子 Agent(Explorer/Coder/Reviewer/Debugger/Companion/Reflect)
    Plugin->>OpenCode: 注册 Hook(session:beforeProcessMessage)
    Plugin->>OpenCode: 注册 Hook(session:tick)
    Plugin->>OpenCode: 注册 Tool(slim:get_council_consensus)
    OpenCode-->>Plugin: Plugin 就绪

    Note over OpenCode,Agent: 用户发送消息

    OpenCode->>Plugin: 触发 session:beforeProcessMessage
    Plugin->>Hub: hub.dispatch(message)
    Hub->>Hub: classifyIntent(message) → intent
    Hub->>Hub: selectAgent(intent) → agent
    Hub->>LazySkills: loadFor(agent, intent)
    LazySkills-->>Hub: Skills 加载完成
    Hub->>Agent: agent.execute(message)
    Agent-->>Hub: result
    Hub->>Hub: reflectOnTask(task, result)
    Hub-->>Plugin: AgentResponse
    Plugin-->>OpenCode: 返回处理结果

Hub-and-Spoke 的代码实现

Hub 核心是一个事件驱动的任务分发器。它的骨架如下:

// 简化版本的 Hub Orchestrator 核心实现
class HubOrchestrator {
  private agents: Map<string, BaseAgent> = new Map();
  private backgroundAgents: BaseAgent[] = [];
  private taskQueue: Task[] = [];
  private activeTask: Task | null = null;

  register(name: string, agent: BaseAgent): void {
    this.agents.set(name, agent);
  }

  registerBackground(agent: BaseAgent): void {
    this.backgroundAgents.push(agent);
  }

  /**
   * 核心分发逻辑:根据消息意图选择 Agent
   */
  async dispatch(message: UserMessage): Promise<AgentResponse> {
    // 1. 意图识别 — 分析用户消息决定派给哪个 Agent
    const intent = await this.classifyIntent(message);

    // 2. Agent 选择 — 根据意图选择最合适的 Agent
    const agent = this.selectAgent(intent);
    if (!agent) {
      return { fallback: true, message: "请明确任务类型(编码/审查/调试/探索)" };
    }

    // 3. 任务入队 — 如果当前有活跃任务,排队等待
    const task: Task = { intent, agent, message, status: "queued" };
    if (this.activeTask) {
      this.taskQueue.push(task);
      return { queued: true, position: this.taskQueue.length };
    }

    // 4. 执行任务
    return this.executeTask(task);
  }

  private async executeTask(task: Task): Promise<AgentResponse> {
    this.activeTask = task;
    try {
      // 使用 LazySkills 按需加载 Agent 需要的 Skill
      await LazySkillLoader.loadFor(task.agent, task.intent);
      const result = await task.agent.execute(task.message);

      // Reflect Agent 在任务完成后自动回顾
      await this.reflectOnTask(task, result);

      return result;
    } finally {
      this.activeTask = null;
      // 执行下一个排队任务
      this.processQueue();
    }
  }

  /**
   * 后台 Agent 心跳:每个 tick 驱动一次
   */
  async tickBackgroundAgents(): Promise<void> {
    for (const agent of this.backgroundAgents) {
      if (agent.shouldActivate()) {
        // 后台 Agent 运行在低优先级,不影响主任务
        agent.tick().catch(err => console.warn(`[Background] ${agent.name} tick failed:`, err));
      }
    }
  }

  private async processQueue(): Promise<void> {
    if (this.taskQueue.length > 0) {
      const next = this.taskQueue.shift()!;
      await this.executeTask(next);
    }
  }
}

这段代码展示了三个关键模式:

模式说明可复用到何处
任务队列 + 单活跃任务防止多个 Agent 同时操作同一文件,确保确定性任何需要串行化 Agent 操作的 Plugin
意图分类 → Agent 选择将自然语言意图映射到预定义 Agent路由型 Plugin(代码审查、文档生成等)
后台 Agent 心跳通过 session:tick Hook 驱动被动 Agent监控类、缓存预热类 Plugin

Hub 的任务分发流程可以用以下流程图直观呈现:

flowchart TB
    subgraph Input["输入处理"]
        A[用户消息] --> B[意图分类<br/>classifyIntent]
    end

    subgraph Dispatch["分发决策"]
        B --> C{匹配到 Agent?}
        C -->|否| D[返回 fallback<br/>请明确任务类型]
        C -->|是| E{当前有活跃任务?}
        E -->|是| F[入队等待<br/>返回排队位置]
        E -->|否| G[执行任务]
    end

    subgraph Execute["任务执行"]
        G --> H[LazySkills 按需加载]
        H --> I[Agent 执行主逻辑]
        I --> J[Reflect 任务回顾]
    end

    subgraph Queue["队列处理"]
        F --> K[当前任务完成]
        K --> L[取出下一个排队任务]
        L --> G
    end

    J --> M[返回结果]

    style A fill:#4A90D9,color:#fff
    style B fill:#50C878,color:#fff
    style C fill:#FF9F43,color:#fff
    style D fill:#A66CFF,color:#fff
    style E fill:#FF9F43,color:#fff
    style F fill:#A66CFF,color:#fff
    style G fill:#4A90D9,color:#fff
    style H fill:#50C878,color:#fff
    style I fill:#4A90D9,color:#fff
    style J fill:#50C878,color:#fff
    style M fill:#4A90D9,color:#fff

关键特性的 Plugin 实现模式

Preset 驱动:配置即策略

slim 的 Preset 系统本质上是 配置驱动的策略注入。它将模型选择、Token 预算、重试策略封装为可交换的配置对象:

// Preset 的核心数据结构
interface Preset {
  name: string;
  modelMap: Record<string, string>;     // Agent 类型 → 模型名称
  budget: {
    perAgent: Record<string, number>;   // 各 Agent 的 Token 上限
    total: number;                       // 总预算上限($30)
  };
  retry: {
    maxAttempts: number;
    fallbackModel: string;               // 超限后降级到哪个模型
  };
  features: {
    lazySkills: boolean;
    council: boolean;
    backgroundAgents: boolean;
  };
}

const OPENAI_PRESET: Preset = {
  name: "openai",
  modelMap: {
    sisyphus: "gpt-4o",
    explorer: "gpt-4o-mini",    // 探索用轻量模型节省 Token
    coder: "gpt-4o",
    reviewer: "gpt-4o-mini",
    debugger: "gpt-4o",
    companion: "gpt-4o-mini",
    reflect: "gpt-4o-mini",
  },
  budget: {
    perAgent: {
      sisyphus: 8000,
      explorer: 4000,
      coder: 16000,
      reviewer: 4000,
      debugger: 8000,
      companion: 2000,
      reflect: 2000,
    },
    total: 30,                    // 单位:美元
  },
  retry: { maxAttempts: 3, fallbackModel: "gpt-4o-mini" },
  features: { lazySkills: true, council: false, backgroundAgents: true },
};

可复用的设计:将策略与实现分离。你的 Plugin 可以定义多个 Preset(如 "strict""fast""cheap"),用户通过一行配置切换整套行为。

LazySkills:上下文窗口优化

LazySkills 解决的核心问题是:传统 Plugin 在启动时加载所有 Skill,浪费上下文窗口。slim 的做法是延迟加载 + 频率缓存:

class LazySkillLoader {
  private static loadedSkills: Map<string, SkillDefinition> = new Map();
  private static accessCount: Map<string, number> = new Map();
  private static readonly MAX_SKILLS = 5;         // 最多同时缓存 5 个 Skill
  private static readonly UNLOAD_THRESHOLD = 3;   // 3 次 tick 未使用则卸载

  /**
   * 按需加载 Agent 需要的 Skill
   */
  static async loadFor(agent: BaseAgent, intent: Intent): Promise<void> {
    const requiredSkills = agent.getSkillsFor(intent);

    for (const skillName of requiredSkills) {
      if (this.loadedSkills.has(skillName)) {
        // 已加载,更新访问计数
        this.accessCount.set(skillName, (this.accessCount.get(skillName) || 0) + 1);
        continue;
      }

      // 未加载且达到上限 → 卸载最不常用的
      if (this.loadedSkills.size >= this.MAX_SKILLS) {
        this.evictLeastUsed();
      }

      // 动态加载 Skill 定义
      const skill = await this.fetchSkill(skillName);
      this.loadedSkills.set(skillName, skill);
      this.accessCount.set(skillName, 1);
    }
  }

  private static evictLeastUsed(): void {
    let minAccess = Infinity;
    let evictKey = "";

    for (const [name, count] of this.accessCount) {
      if (count < minAccess) {
        minAccess = count;
        evictKey = name;
      }
    }

    if (evictKey) {
      this.loadedSkills.delete(evictKey);
      this.accessCount.delete(evictKey);
    }
  }
}

可复用的设计:任何需要管理上下文压力的 Plugin 都可以实现类似的加载器——不仅是 Skill,也可以是 Tool 定义、配置片段、Prompt 模板。

LazySkills 的加载和卸载决策流程如下:

flowchart TB
    subgraph Request["请求处理"]
        A[Agent 请求 Skill] --> B{Skill 在缓存中?}
    end

    subgraph CacheHit["命中缓存"]
        B -->|是| C[更新访问计数]
        C --> D[从缓存加载 Skill]
    end

    subgraph CacheMiss["未命中缓存"]
        B -->|否| E{缓存已达上限?}
        E -->|否| F[直接加载 Skill]
        E -->|是| G[淘汰最不常用 Skill]
        G --> F
    end

    subgraph Execution["执行"]
        D --> H[Agent 执行任务]
        F --> H
        H --> I[任务完成]
    end

    subgraph Maintenance["后台维护"]
        I --> J[session:tick 检查]
        J --> K{Skill 访问间隔<br/>超过阈值?}
        K -->|是| L[卸载低频 Skill<br/>释放上下文]
        K -->|否| M[保持加载]
    end

    style A fill:#4A90D9,color:#fff
    style B fill:#FF9F43,color:#fff
    style C fill:#50C878,color:#fff
    style D fill:#50C878,color:#fff
    style E fill:#FF9F43,color:#fff
    style F fill:#50C878,color:#fff
    style G fill:#A66CFF,color:#fff
    style H fill:#4A90D9,color:#fff
    style I fill:#50C878,color:#fff
    style L fill:#A66CFF,color:#fff
    style M fill:#50C878,color:#fff

Council:多模型共识的 Tool 封装

Council 的实现展示了如何将“多模型调用“封装为标准的 OpenCode Tool:

class Council {
  private members: CouncilMember[];

  async consensus(question: string, options: string[], minConfidence: number): Promise<CouncilResult> {
    // 并行调用多个模型
    const votes = await Promise.all(
      this.members.map(member => this.queryMember(member, question, options))
    );

    // 加权投票
    const scores: Record<string, number> = {};
    for (const vote of votes) {
      for (const option of vote.preferences) {
        scores[option.name] = (scores[option.name] || 0) + option.weight * vote.member.weight;
      }
    }

    // 判断是否达到共识
    const totalWeight = this.members.reduce((s, m) => s + m.weight, 0);
    const result = Object.entries(scores)
      .map(([name, score]) => ({ name, confidence: score / totalWeight }))
      .sort((a, b) => b.confidence - a.confidence);

    return {
      consensus: result[0].confidence >= minConfidence ? result[0].name : null,
      ranking: result,
      votes: votes.map(v => ({ member: v.member.name, choice: v.preferences[0].name })),
    };
  }

  private async queryMember(member: CouncilMember, question: string, options: string[]) {
    // 通过 OpenCode API 调用指定模型
    const response = await opencode.invokeModel(member.model, {
      messages: [
        { role: "system", content: `你是一个决策评估专家。请从以下选项中选择最合适的:${options.join(", ")}` },
        { role: "user", content: question },
      ],
      temperature: 0.3,
    });
    return {
      member,
      preferences: this.parseResponse(response, options),
    };
  }
}

可复用的设计:Council 模式适用于任何需要“多模型交叉验证“的场景——安全审计(多个模型审查代码)、架构决策(多个模型评估方案)、内容审核(多个模型判断合规)。

Background Agents:Hook 驱动的心跳模式

Background Agents 的实现核心是 session:tick Hook——OpenCode 在每个会话心跳周期(约 1-2 秒)触发一次:

// Companion Agent 的实现骨架
class CompanionAgent extends BackgroundAgent {
  private observations: Observation[] = [];
  private fileWatcher: FileWatcher;

  async tick(): Promise<void> {
    // 1. 检查文件变化
    const changes = this.fileWatcher.getChanges();

    // 2. 记录观察
    for (const change of changes) {
      this.observations.push({
        type: "file_change",
        path: change.path,
        timestamp: Date.now(),
        summary: change.summary,
      });
    }

    // 3. 如果检测到关键变化,触发低干扰提醒
    if (this.hasCriticalChanges(changes)) {
      await this.suggestAction(changes);
    }
  }

  shouldActivate(): boolean {
    // 只在有观察记录且上次提醒已过冷却期时激活
    return this.observations.length > 0 && Date.now() - this.lastSuggestion > 30000;
  }
}

可复用的设计session:tick Hook 可以驱动多种后台行为——文件监控、缓存预热、进度上报、自动保存。

从读取到自建:Plugin 设计决策框架

当你理解了 slim 的模式后,自建 Plugin 的核心挑战不再是“怎么写代码“,而是“做什么决策“。以下是基于 slim 模式提炼的决策框架。

决策 1:选择架构模式

你的需求推荐模式参考自
多个 Agent 围绕一个协调器工作Hub-and-Spokeslim 的核心架构
任务有明确的阶段顺序(A→B→C)PipelineOMO 的 Hook 链
Agent 之间需要自由通信Event BusOMO 的 Event 系统
单个 Agent 处理所有请求直接 Hook简单场景无需编排

决策 2:选择 Hook 点

你想做的事情合适的 Hook 点
拦截用户消息进行路由session:beforeProcessMessage
在工具调用前后注入逻辑tool:before / tool:after
驱动后台周期性任务session:tick
阻止危险的文件操作file:beforeWrite

决策 3:选择扩展方式

场景推荐方式
需要 Agent 直接调用注册为 Tool(如 slim 的 slim:get_council_consensus
需要在关键节点拦截注册为 Hook
需要在后台持续运行注册为 Service + session:tick

决策 4:配置策略

参考 slim 的 Preset 模式,将可变参数提取为配置:

{
  "plugin": {
    "my-plugin": {
      "path": "./plugins/my-plugin/index.ts",
      "enabled": true,
      "config": {
        "mode": "standard",       // "standard" | "strict" | "lightweight"
        "maxAgents": 3,
        "budget": 15,
        "features": {
          "autoRetry": true,
          "parallelTasks": false
        }
      }
    }
  }
}

实战:构建轻量代码审查 Plugin

下面我们基于 slim 的模式,从零构建一个专用的 代码审查 Plugin。这个 Plugin 只有 3 个 Agent(Reviewer、Explorer、Reflect),专注于“提交代码后自动审查“场景。

import { definePlugin } from "opencode";

// 1. 定义审查 Agent
class ReviewAgent {
  async review(filePath: string, diff: string): Promise<ReviewResult> {
    const response = await opencode.invokeModel("gpt-4o-mini", {
      messages: [
        {
          role: "system",
          content: `你是一个代码审查专家。审查以下代码变更,关注:
1. 逻辑错误和边界情况
2. 安全漏洞(注入、泄露等)
3. 代码风格和可维护性
4. 性能问题

对每个问题标注严重级别:critical / major / minor`,
        },
        { role: "user", content: `文件: ${filePath}\n\n变更内容:\n${diff}` },
      ],
      temperature: 0.2,
    });
    return this.parseReview(response);
  }
}

// 2. 定义探索 Agent
class ExploreAgent {
  async findRelatedFiles(filePath: string): Promise<string[]> {
    // 查找与被修改文件相关的其他文件
    const imports = await opencode.grep(`import.*from.*${filePath.replace(/\..*$/, "")}`, {
      filePattern: "*.ts",
    });
    return imports.map(i => i.file);
  }
}

// 3. 定义反思 Agent
class ReflectAgent {
  async summarize(findings: ReviewResult[]): Promise<string> {
    const critical = findings.filter(f => f.severity === "critical").length;
    const major = findings.filter(f => f.severity === "major").length;
    const minor = findings.filter(f => f.severity === "minor").length;

    if (critical > 0) return `⛔ 发现 ${critical} 个严重问题,建议修复后再合并`;
    if (major > 2) return `⚠️ 发现 ${major} 个主要问题,建议修复`;
    return `✅ 仅 ${minor} 个次要问题,可直接合并`;
  }
}

// 4. 注册为 Plugin
export default definePlugin({
  name: "code-review-lite",
  description: "轻量代码审查 Plugin:自动审查 Git 变更",

  onActivate(context) {
    this.reviewAgent = new ReviewAgent();
    this.exploreAgent = new ExploreAgent();
    this.reflectAgent = new ReflectAgent();
  },

  hooks: {
    "git:afterCommit": async ({ commitHash, files }) => {
      // 获取文件变更
      const diff = await opencode.exec(`git diff ${commitHash}^..${commitHash}`);

      // 并行审查所有变更文件
      const reviewPromises = files.map(async (file) => {
        const review = await this.reviewAgent.review(file, diff);
        const relatedFiles = await this.exploreAgent.findRelatedFiles(file);
        return { file, review, relatedFiles };
      });

      const results = await Promise.all(reviewPromises);

      // 总结
      const summary = await this.reflectAgent.summarize(results.map(r => r.review));

      // 通过 OpenCode 通知用户
      await opencode.notify(`[Code Review Lite] ${summary}`);
    },
  },

  tools: [
    {
      name: "review:check_current_changes",
      description: "审查当前分支的未提交变更",
      parameters: { type: "object", properties: {} },
      handler: async () => {
        const diff = await opencode.exec("git diff HEAD");
        if (!diff) return "没有未提交的变更";
        const review = await this.reviewAgent.review("working-tree", diff);
        return formatReview(review);
      },
    },
  ],
});

这个示例展示了如何复用 slim 的三种模式:

slim 模式本示例中的映射
Hub + 多 AgentReviewAgent + ExploreAgent + ReflectAgent
并行执行Promise.all 并行审查多个文件
Reflect 总结ReflectAgent.summarize 聚合审查结果
Tool 暴露review:check_current_changes 提供按需触发

小结

oh-my-opencode-slim 不仅仅是一个“开箱即用的编排插件“——它的代码本身就是一份 Plugin 架构的教科书。通过本文的分析,你应该掌握了三层能力:

  1. 读懂 slim — 理解 Hub-and-Spoke 的代码实现、Preset 驱动模式、LazySkills 的上下文优化策略
  2. 提取模式 — 从 slim 中抽象出可复用的设计模式(任务队列、后台心跳、配置注入、多模型共识)
  3. 自建 Plugin — 应用 slim 的决策框架和实现模式,构建你自己的 OpenCode Plugin

如果你已经完成了 自定义 Agent(智能体)Plugin(插件) 的 Plugin 基础学习,并且理解了 oh-my-opencode-slim:轻量级 Agent 编排方案,那么现在你已经具备了自建 Plugin 的全套能力。

延伸思考

  • 如果想让 Plugin 支持并行 Agent 执行:参考 Promise.all + git worktree 隔离模式(见 slim 的 Worktrees 集成)
  • 如果想让 Plugin 支持更长周期的后台任务:参考 session:tick + 任务持久化(将任务状态写入文件或数据库)
  • 如果想让 Plugin 支持可视化 Dashboard:参考 OpenCode Plugin(插件) 系统参考 中的 UI 扩展点

关联章节