AI Agent 与脚本插件:Deck 的自我进化系统
Deck 如何将 AI 深度融入剪贴板管理——多 Provider 抽象、12 个工具、Agent 循环、JavaScriptCore 沙箱插件和 Smart Rule 引擎。
一个真实的例子
你在终端里复制了一段 JSON 响应,想把它格式化成可读的样子。打开 Deck,输入一句话:
"帮我把最近复制的那段 JSON 格式化一下。"
Deck 的 AI Agent 随即完成了这些事:
- 搜索剪贴板历史,找到 30 秒前复制的 JSON
- 发现没有现成的格式化插件,于是自己写了一个
- 将插件存入
~/.deck/scripts/json-formatter/ - 执行插件,将 JSON 格式化为缩进整齐的格式
- 结果写入剪贴板历史,等你使用
这不是一个对话框套了层皮。Deck 的 AI 有自己的工具集、权限边界、执行循环,还能通过创建插件来扩展自身能力。
一、多 Provider 抽象:连接一切大模型
大多数 AI 应用被锁定在单一供应商上。Deck 从第一天起就采用了多 Provider 架构,你可以自由切换 OpenAI、Anthropic、Ollama 本地模型,甚至通过 ChatGPT macOS 客户端免费使用。
Provider 枚举
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:
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 格式化插件长这样:
// 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 实现了这一功能:
static let scriptExecutionTimeout: TimeInterval = 5.0具体做法是通过 dlopen 加载 JavaScriptCore 框架,用 dlsym 获取 JSGlobalContextGetGroup 和 JSContextGroupSetExecutionTimeLimit 两个符号,封装为 JSExecutionTimeLimiter 类。在创建 JS 执行上下文时调用 setLimit(group, timeLimit, callback, nil),确保任何脚本最多运行 5 秒。
需要网络权限的插件必须在 manifest 中声明 requiresNetwork: true,运行时使用独立的 ephemeral URLSession,不会泄露主应用的 cookies 或缓存。
五、AI 创建插件:自我扩展的系统
这是 Deck Agent 系统最有趣的部分。AI 不仅可以使用插件,还可以创建和修改插件。
生成新插件
generate_script_plugin(工具 10)让 AI 从零创建插件:
prepareDraft(parameters:)— 校验 manifest 和源码- 安全校验
- 必须包含
transform(input)函数 plugin_id符合命名规范
- 生成
AIPluginInstallDraft/ GenerateAIPluginInstallDraft install(_ draft)— 写入磁盘
增量修改
modify_script_plugin(工具 11)使用 apply_patch 风格的增量修改,AI 不需要重写整个文件,只需描述变更部分:
- 支持多处修改
插件的积累效应
回到开头的例子:
- 用户说:"帮我格式化 JSON"
- AI 搜索历史,找到 JSON
- AI 检查现有插件,没有 JSON 格式化器
- AI 编写一个,通过安全校验后安装
- AI 立即用新插件格式化 JSON
- 下次再需要格式化 JSON?插件已经在那里了
AI 每解决一个新问题,就会留下一个可复用的插件。随着使用时间增长,积累的插件越来越多,系统能做的事也越来越多。
六、Smart Rule 引擎:当条件遇上 AI
到目前为止都是"用户主动触发"的场景。Smart Rule 引擎让 AI 变成了一个后台自动化系统,在你复制的那一刻,规则就开始运转。
条件类型
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)组合。
动作类型
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 自动介入处理:
构造
SmartRuleSourceSnapshot,包含触发 item 的完整上下文(ID、类型、来源应用、内容、时间戳)调用
AIService.shared.runSmartRuleAutomation(prompt:source:)在
smartRule(source:)上下文中执行,自动授权(autoApprove: true)工具作用域限定为当前触发的 item
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 真正参与产品工作流的架构,而不只是在旁边聊天。