返回博客
2026 年 3 月 6 日6 分钟阅读AIPluginsArchitecture

AI Agent 与脚本插件:Deck 的自我进化系统

Deck 如何将 AI 深度融入剪贴板管理——多 Provider 抽象、12 个工具、Agent 循环、JavaScriptCore 沙箱插件和 Smart Rule 引擎。


一个真实的例子

你在终端里复制了一段 JSON 响应,想把它格式化成可读的样子。打开 Deck,输入一句话:

"帮我把最近复制的那段 JSON 格式化一下。"

Deck 的 AI Agent 随即完成了这些事:

  1. 搜索剪贴板历史,找到 30 秒前复制的 JSON
  2. 发现没有现成的格式化插件,于是自己写了一个
  3. 将插件存入 ~/.deck/scripts/json-formatter/
  4. 执行插件,将 JSON 格式化为缩进整齐的格式
  5. 结果写入剪贴板历史,等你使用

这不是一个对话框套了层皮。Deck 的 AI 有自己的工具集、权限边界、执行循环,还能通过创建插件来扩展自身能力。


一、多 Provider 抽象:连接一切大模型

大多数 AI 应用被锁定在单一供应商上。Deck 从第一天起就采用了多 Provider 架构,你可以自由切换 OpenAI、Anthropic、Ollama 本地模型,甚至通过 ChatGPT macOS 客户端免费使用。

Provider 枚举

swift
enum AIProviderType {
    case chatgpt     // ChatGPT (Web/App)
    case openaiAPI   // OpenAI API (direct)
    case anthropic   // Anthropic Claude
    case ollama      // Ollama local models
}

统一协议

所有 Provider 遵循同一个协议 AIProviderProtocol,暴露统一接口:

  • sendMessage(messages:tools:stream:) — 发送消息,支持流式输出和工具调用定义
  • isConfigured — 是否已完成配置
  • currentModel — 当前模型标识
  • cancelCurrentRequest() — 取消进行中的请求

AIService 作为编排中心,维护一个 providerCache 避免重复初始化,并通过 switch 路由到正确的 Provider:

swift
func configuredProvider() -> AIProviderProtocol {
    switch config.providerType {
    case .chatgpt:   return ChatGPTProvider.shared
    case .openaiAPI: return OpenAIAPIProvider.shared
    case .anthropic: return AnthropicProvider.shared
    case .ollama:    return OllamaProvider.shared
    }
}

整个 Agent 系统(工具调用、插件执行、Smart Rule)对底层模型完全无感。换一个 Provider,一切照常工作。


二、12 个工具:AI 的能力边界

一个 Agent 的能力由它可以使用的工具决定。Deck 为 AI 定义了 12 个工具,覆盖了剪贴板管理的完整生命周期:

# 工具 / Tool 功能 / Function 需授权 / Auth
1 search_clipboard 搜索剪贴板历史(auto/exact/fuzzy/regex/mixed/semantic 六种模式)/ Search clipboard history (6 modes) -
2 write_clipboard 写入新记录到剪贴板历史 / Write new entry to clipboard history [Y]
3 delete_clipboard 按 ID 删除记录 / Delete entry by ID [Y]
4 list_script_plugins 列出所有插件元数据 / List all plugin manifests -
5 read_script_plugin 读取插件源码 / Read plugin source code -
6 read_skill_detail 读取技能描述 / Read skill description -
7 record_memory 持久化长期记忆(≤30 字符,可设 TTL)/ Persist long-term memory (≤30 chars, optional TTL) -
8 delete_memory 删除已保存的记忆 / Delete saved memory -
9 run_script_transform 执行插件 transform(input) / Run plugin's transform(input) [!]
10 generate_script_plugin AI 生成新插件 / AI generates a new plugin [Y]
11 modify_script_plugin 增量修改现有插件 / Incrementally patch an existing plugin [Y]
12 delete_script_plugin 删除插件 / Delete a plugin [Y]

授权机制

注意表中 [Y] 标记的工具:所有写入、删除、创建类操作都需要用户明确授权。授权逻辑由 UI 层注入,缺失时默认拒绝。AI 能做很多事,但关键操作永远需要你点头。

另一个细节:工具结果存在截断机制(maxToolSnippetCharacters = 2500),防止超长输出耗尽 context window。


三、Agent 循环:思考、执行、观察

Deck 的 AI 不是一问一答的聊天机器人,而是一个循环执行的 Agent。System Prompt 中明确定义了行为模式:

<agent_loop>
You are a capable agent. Act, don't just describe.

For each request, follow this cycle:
1. Understand the user's intent.
2. Plan internally...
3. Execute: call the necessary tool(s)...
4. Observe and verify each tool result before proceeding.
5. Respond to the user with the final result.

When issuing a tool call, output ONLY the tool call — no surrounding narration.
</agent_loop>

Act, don't just describe 是这段指令的核心。当你说"帮我搜索最近复制的链接"时,AI 不会回复"你可以使用搜索功能来查找链接"。它直接调用 search_clipboard,拿到结果,告诉你找到了什么。

流式执行

整个过程通过 AsyncThrowingStream<AIStreamEvent, Error> 实现流式处理,事件类型包括:

  • textDelta — 文本片段
  • toolCall — 工具调用请求
  • done — 完成
  • error — 错误

工具调用的完整流程:

sequenceDiagram
    participant U as 用户 / User
    participant AI as AIService
    participant P as Provider
    participant T as AIToolService

    U->>AI: "格式化最近的 JSON"
    AI->>P: sendMessage(messages, tools)
    P-->>AI: toolCall(search_clipboard)
    AI->>T: execute(search_clipboard)
    T-->>AI: [找到 JSON 记录]
    AI->>P: 继续生成(含工具结果)
    P-->>AI: toolCall(run_script_transform)
    AI->>T: execute(run_script_transform)
    T-->>AI: [格式化结果]
    AI->>P: 继续生成
    P-->>AI: textDelta("已格式化完成...")
    AI-->>U: 最终响应 + 格式化结果

每次工具调用的结果以 AIMessage.tool(toolCallId:text:) 追加到消息历史,Provider 基于这些结果生成下一步响应。Agent 循环可以连续调用多个工具,直到完成任务。


四、JavaScriptCore 沙箱:安全的插件执行环境

插件系统是 Deck 的核心特性之一。每个插件本质上是一个 JavaScript 文件,运行在 JavaScriptCore 沙箱中。

插件结构

~/.deck/scripts/
└── my-plugin/
    ├── manifest.json    # 元数据 / Metadata
    └── main.js          # 必须导出 transform(input) / Must export transform(input)

plugin_id 命名遵循 ^[a-z0-9._-]{1,64}$,manifest 最大 64KB,脚本最大 1MB。

一个 AI 生成的 JSON 格式化插件长这样:

javascript
// main.js
function transform(input) {
    try {
        var obj = JSON.parse(input);
        return JSON.stringify(obj, null, 2);
    } catch (e) {
        return "Invalid JSON: " + e.message;
    }
}

沙箱安全约束

Deck 对插件的运行环境施加了严格限制:

约束 / Constraint 状态 / Status
文件系统访问 / File system access [N] 禁止 / Blocked
Shell 执行 / Shell execution [N] 禁止 / Blocked
eval / Function 构造 / eval / Function constructor [N] 禁止 / Blocked
网络访问 / Network access [N] 默认禁止(需 manifest 声明 + 用户确认)/ Blocked by default (requires manifest declaration + user confirmation)
输入/输出大小 / I/O size limit [Y] 各 100KB / 100KB each
执行时限 / Execution timeout [Y] 5 秒 / 5 seconds
中断检查 / Interrupt checking [Y] __deckCheckInterrupt()

5 秒时限的实现

JavaScriptCore 本身不提供公共的执行时限 API,Deck 通过私有 API 实现了这一功能:

swift
static let scriptExecutionTimeout: TimeInterval = 5.0

具体做法是通过 dlopen 加载 JavaScriptCore 框架,用 dlsym 获取 JSGlobalContextGetGroupJSContextGroupSetExecutionTimeLimit 两个符号,封装为 JSExecutionTimeLimiter 类。在创建 JS 执行上下文时调用 setLimit(group, timeLimit, callback, nil),确保任何脚本最多运行 5 秒。

需要网络权限的插件必须在 manifest 中声明 requiresNetwork: true,运行时使用独立的 ephemeral URLSession,不会泄露主应用的 cookies 或缓存。


五、AI 创建插件:自我扩展的系统

这是 Deck Agent 系统最有趣的部分。AI 不仅可以使用插件,还可以创建修改插件。

生成新插件

generate_script_plugin(工具 10)让 AI 从零创建插件:

  1. prepareDraft(parameters:) — 校验 manifest 和源码
  2. 安全校验
  • 必须包含 transform(input) 函数
  • plugin_id 符合命名规范
  1. 生成 AIPluginInstallDraft / Generate AIPluginInstallDraft
  2. install(_ draft) — 写入磁盘

增量修改

modify_script_plugin(工具 11)使用 apply_patch 风格的增量修改,AI 不需要重写整个文件,只需描述变更部分:

  • 支持多处修改

插件的积累效应

回到开头的例子:

  1. 用户说:"帮我格式化 JSON"
  2. AI 搜索历史,找到 JSON
  3. AI 检查现有插件,没有 JSON 格式化器
  4. AI 编写一个,通过安全校验后安装
  5. AI 立即用新插件格式化 JSON
  6. 下次再需要格式化 JSON?插件已经在那里了

AI 每解决一个新问题,就会留下一个可复用的插件。随着使用时间增长,积累的插件越来越多,系统能做的事也越来越多。


六、Smart Rule 引擎:当条件遇上 AI

到目前为止都是"用户主动触发"的场景。Smart Rule 引擎让 AI 变成了一个后台自动化系统,在你复制的那一刻,规则就开始运转

条件类型

swift
enum RuleCondition {
    case contentContains(String)           // 内容包含 / Content contains
    case contentMatches(String)            // 正则匹配 / Regex match
    case contentType(String)              // 类型匹配 / Type match (text/image/file/url/code/color)
    case sourceApp(String)                // 来源应用 / Source application
    case contentLength(CompareOp, Int)    // 长度比较 / Length comparison
    case hasCustomTitle                   // 有自定义标题 / Has custom title
}

条件可以用 .all(AND)或 .any(OR)组合。

动作类型

swift
enum RuleAction {
    case addTag(String)          // 添加标签 / Add tag
    case markSensitive           // 标记敏感 / Mark as sensitive
    case autoDelete(Int)         // N 分钟后自动删除 / Auto-delete after N minutes
    case transform(String)       // 文本转换 / Text transform (built-in or plugin)
    case aiPrompt(String)        // 触发 AI 处理 / Trigger AI processing
    case ignore                  // 不保存 / Don't save to history
}

执行流程

flowchart TD
    A[ClipboardService 检测到复制<br/>Copy detected] --> B[processParsedItemWithRules]
    B --> C[SmartRuleService.processItem]
    C --> D{条件求值<br/>Evaluate conditions}
    D -->|命中 ignore| E[丢弃,不入库<br/>Discard]
    D -->|命中 transform| F[先转换再入库<br/>Transform then save]
    D -->|其他动作| G[入库<br/>Save to store]
    F --> G
    G --> H[applyActions]
    H --> I[addTag / markSensitive / autoDelete]
    H --> J[aiPrompt → AI 自动化处理<br/>AI automation]

AI 动作链

当规则触发 aiPrompt(prompt) 时,AI 自动介入处理:

  1. 构造 SmartRuleSourceSnapshot,包含触发 item 的完整上下文(ID、类型、来源应用、内容、时间戳)

  2. 调用 AIService.shared.runSmartRuleAutomation(prompt:source:)

  3. smartRule(source:) 上下文中执行,自动授权(autoApprove: true

  4. 工具作用域限定为当前触发的 item

  5. AI 使用受限工具集(工具 1-7),不能在 Smart Rule 上下文中创建或修改插件

实际场景

  • Slack 链接提取 / Slack link extraction:来源是 Slack + 内容包含 URL → AI 自动提取链接摘要并打标签 / Source is Slack + content contains URL → AI extracts link summary and adds tags
  • 代码格式化 / Code formatting:内容类型是 code + 长度 > 100 字符 → 自动用插件格式化 / Content type is code + length > 100 chars → auto-format via plugin
  • 密码保护 / Password protection:来源是密码管理器 → 标记敏感 + 5 分钟后自动删除 / Source is a password manager → mark sensitive + auto-delete after 5 minutes

系统架构总览

graph TB
    subgraph Orchestration["编排层 / Orchestration"]
        AIS[AIService<br/>编排中心 / Orchestrator]
    end

    subgraph Providers["模型层 / Model Layer"]
        PP[AIProviderProtocol]
        PP --> CGP[ChatGPTProvider]
        PP --> OAP[OpenAIAPIProvider]
        PP --> ANP[AnthropicProvider]
        PP --> OLP[OllamaProvider]
    end

    subgraph Tools["工具层 / Tool Layer"]
        ATS[AIToolService<br/>12 个工具 / 12 Tools]
    end

    subgraph Plugins["插件层 / Plugin Layer"]
        SPS[ScriptPluginService<br/>JS 沙箱 / JS Sandbox]
        APGS[AIPluginGeneratorService<br/>生成·修改·删除 / Generate·Patch·Delete]
    end

    subgraph Automation["自动化层 / Automation"]
        SRS[SmartRuleService<br/>条件匹配 + 动作 / Conditions + Actions]
    end

    subgraph Storage["数据层 / Data Layer"]
        DDS[DeckDataStore<br/>存储与规则入口 / Storage & Rule Entry]
    end

    AIS <--> PP
    AIS <--> ATS
    ATS --> SPS
    ATS --> APGS
    APGS --> SPS
    SRS --> AIS
    DDS --> SRS
    ATS --> DDS

能力与约束:怎么划线

Deck 的 Agent 设计始终在平衡一件事:给 AI 足够的能力去解决问题,同时确保它不会做出用户不想要的事。

AI 能做什么:

  • 12 个工具覆盖完整的剪贴板操作生命周期
  • 自己编写和安装插件来扩展能力
  • 流式 Agent 循环支持多步推理和工具链式调用
  • 通过 Smart Rule 在后台自动运行

什么被限制了:

  • 写入和删除操作需要用户授权
  • 插件在严格的沙箱中执行,无文件系统、无 Shell、5 秒超时
  • Smart Rule 上下文中 AI 不能创建新插件,只能用现有工具
  • 工具结果截断防止 context 爆炸
  • 网络权限需要显式声明和用户确认

这些限制不是因为不信任 AI。实际情况是:AI 可以自由创建插件,但需要你点确认;它可以在后台自动运行,但只有读取和搜索权限,没法自己装新插件。该放权的地方放权,该收紧的地方收紧。

这就是 Deck 的 AI Agent 系统:一套让 AI 真正参与产品工作流的架构,而不只是在旁边聊天。