fiftyone-develop-plugin
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDevelop FiftyOne Plugins
开发FiftyOne插件
Key Directives
核心准则
ALWAYS follow these rules:
请始终遵循以下规则:
1. Understand before coding
1. 先理解再编码
Ask clarifying questions. Never assume what the plugin should do.
提出澄清问题,绝不假设插件的功能需求。
2. Plan before implementing
2. 先规划再实现
Present file structure and design. Get user approval before generating code.
展示文件结构和设计方案,在生成代码前获得用户认可。
3. Search existing plugins for patterns
3. 搜索现有插件的模式
bash
undefinedbash
undefinedClone official plugins for reference
克隆官方插件作为参考
git clone https://github.com/voxel51/fiftyone-plugins.git /tmp/fiftyone-plugins 2>/dev/null || true
git clone https://github.com/voxel51/fiftyone-plugins.git /tmp/fiftyone-plugins 2>/dev/null || true
Search for similar patterns
搜索相似模式
grep -r "keyword" /tmp/fiftyone-plugins/plugins/ --include="*.py" -l
```python
list_plugins(enabled=True)
list_operators(builtin_only=False)
get_operator_schema(operator_uri="@voxel51/brain/compute_similarity")grep -r "keyword" /tmp/fiftyone-plugins/plugins/ --include="*.py" -l
```python
list_plugins(enabled=True)
list_operators(builtin_only=False)
get_operator_schema(operator_uri="@voxel51/brain/compute_similarity")4. Test locally before done
4. 完成前先本地测试
bash
undefinedbash
undefinedGet plugins directory
获取插件目录
PLUGINS_DIR=$(python -c "import fiftyone as fo; print(fo.config.plugins_dir)")
PLUGINS_DIR=$(python -c "import fiftyone as fo; print(fo.config.plugins_dir)")
Develop plugin in plugins directory
在插件目录中开发插件
cd $PLUGINS_DIR/my-plugin
Write tests:
- **Python**: `pytest` for operators/panels
- **JavaScript**: `vitest` for React components
Verify in FiftyOne App before done.cd $PLUGINS_DIR/my-plugin
编写测试:
- **Python**:使用`pytest`测试operators/panels
- **JavaScript**:使用`vitest`测试React组件
在FiftyOne App中验证后再完成开发。5. Iterate on feedback
5. 根据反馈迭代
Run server separately to see logs:
bash
undefined单独运行服务器查看日志:
bash
undefinedTerminal 1: Python logs
终端1:Python日志
python -m fiftyone.server.main
python -m fiftyone.server.main
Terminal 2: Browser at localhost:5151 (JS logs in DevTools console)
终端2:在浏览器访问localhost:5151(JS日志在开发者工具控制台中)
For automated iteration, use Playwright e2e tests:
```bash
npx playwright testRefine until the plugin works as expected.
如需自动化迭代,使用Playwright端到端测试:
```bash
npx playwright test优化直到插件按预期工作。
Critical Patterns
关键模式
Operator Execution
Operator执行
python
undefinedpython
undefinedChain operators (non-delegated operators only, in execute() only, fire-and-forget)
链式调用operators(仅非委托operators,仅在execute()中使用,即发即弃)
ctx.trigger("@plugin/other_operator", params={...})
ctx.trigger("@plugin/other_operator", params={...})
UI operations
UI操作
ctx.ops.notify("Done!")
ctx.ops.set_progress(0.5)
undefinedctx.ops.notify("Done!")
ctx.ops.set_progress(0.5)
undefinedView Selection
视图选择
python
undefinedpython
undefinedUse ctx.target_view() to respect user's current selection and filters
使用ctx.target_view()以尊重用户当前的选择和过滤器
view = ctx.target_view()
view = ctx.target_view()
ctx.dataset - Full dataset (use when explicitly exporting all)
ctx.dataset - 完整数据集(明确导出全部数据时使用)
ctx.view - Filtered view (use for read-only operations)
ctx.view - 过滤后的视图(用于只读操作)
ctx.target_view() - Filtered + selected samples (use for exports/processing)
ctx.target_view() - 过滤后+选中的样本(用于导出/处理)
undefinedundefinedStore Keys (Avoid Collisions)
存储键(避免冲突)
python
undefinedpython
undefinedUse namespaced keys to avoid cross-dataset conflicts
使用命名空间键避免跨数据集冲突
def _get_store_key(self, ctx):
plugin_name = self.config.name.split("/")[-1]
return f"{plugin_name}store{ctx.dataset.doc.id}{self.version}"
store = ctx.store(self._get_store_key(ctx))
undefineddef _get_store_key(self, ctx):
plugin_name = self.config.name.split("/")[-1]
return f"{plugin_name}store{ctx.dataset.doc.id}{self.version}"
store = ctx.store(self._get_store_key(ctx))
undefinedPanel State vs Execution Store
Panel状态与执行存储
python
undefinedpython
undefinedctx.panel.state - Transient (resets when panel reloads)
ctx.panel.state - 临时存储(面板重新加载时重置)
ctx.store() - Persistent (survives across sessions)
ctx.store() - 持久存储(跨会话保留)
def on_load(self, ctx):
ctx.panel.state.selected_tab = "overview" # Transient
store = ctx.store(self._get_store_key(ctx))
ctx.panel.state.config = store.get("user_config") or {} # Persistent
undefineddef on_load(self, ctx):
ctx.panel.state.selected_tab = "overview" # 临时
store = ctx.store(self._get_store_key(ctx))
ctx.panel.state.config = store.get("user_config") or {} # 持久
undefinedDelegated Execution
委托执行
Use for operations that: process >100 samples or take >1 second.
python
@property
def config(self):
return foo.OperatorConfig(
name="heavy_operator",
allow_delegated_execution=True,
default_choice_to_delegated=True,
)适用于处理超过100个样本或耗时超过1秒的操作。
python
@property
def config(self):
return foo.OperatorConfig(
name="heavy_operator",
allow_delegated_execution=True,
default_choice_to_delegated=True,
)Progress Reporting
进度报告
python
@property
def config(self):
return foo.OperatorConfig(
name="progress_operator",
execute_as_generator=True,
)
def execute(self, ctx):
total = len(ctx.target_view())
for i, sample in enumerate(ctx.target_view()):
# Process sample...
yield ctx.trigger("set_progress", {"progress": (i+1)/total})
yield {"status": "complete"}python
@property
def config(self):
return foo.OperatorConfig(
name="progress_operator",
execute_as_generator=True,
)
def execute(self, ctx):
total = len(ctx.target_view())
for i, sample in enumerate(ctx.target_view()):
# 处理样本...
yield ctx.trigger("set_progress", {"progress": (i+1)/total})
yield {"status": "complete"}Custom Runs (Auditability)
自定义运行(可审计性)
Use Custom Runs for operations needing reproducibility and history tracking:
python
undefined对于需要可复现性和历史跟踪的操作,使用自定义运行:
python
undefinedCreate run key (must be valid Python identifier - use underscores, not slashes)
创建运行键(必须是有效的Python标识符 - 使用下划线,不要用斜杠)
run_key = f"my_plugin_{self.config.name}v1{timestamp}"
run_key = f"my_plugin_{self.config.name}v1{timestamp}"
Initialize and register
初始化并注册
run_config = ctx.dataset.init_run(operator=self.config.name, params=ctx.params)
ctx.dataset.register_run(run_key, run_config)
See [PYTHON-OPERATOR.md](PYTHON-OPERATOR.md#custom-runs-auditable-operations) for full Custom Runs pattern.
See [EXECUTION-STORE.md](EXECUTION-STORE.md) for advanced caching patterns.
See [HYBRID-PLUGINS.md](HYBRID-PLUGINS.md) for Python + JavaScript communication.run_config = ctx.dataset.init_run(operator=self.config.name, params=ctx.params)
ctx.dataset.register_run(run_key, run_config)
查看[PYTHON-OPERATOR.md](PYTHON-OPERATOR.md#custom-runs-auditable-operations)获取完整的自定义运行模式。
查看[EXECUTION-STORE.md](EXECUTION-STORE.md)获取高级缓存模式。
查看[HYBRID-PLUGINS.md](HYBRID-PLUGINS.md)了解Python + JavaScript通信方式。Workflow
工作流程
Phase 1: Requirements
阶段1:需求分析
Understand what the user needs to accomplish:
- "What problem are you trying to solve?"
- "What should the user be able to do?" (user's perspective)
- "What information does the user provide?"
- "What result does the user expect to see?"
- "Any external data sources or services involved?"
- "How will this fit into the user's workflow?"
理解用户需要实现的目标:
- "你要解决什么问题?"
- "用户应该能够执行哪些操作?"(用户视角)
- "用户需要提供哪些信息?"
- "用户期望看到什么结果?"
- "是否涉及外部数据源或服务?"
- "这将如何融入用户的工作流?"
Phase 2: Design
阶段2:设计
- Search existing plugins for similar patterns
- For panels, default to hybrid (Python + JavaScript). See HYBRID-PLUGINS.md.
- Create plan with:
- Plugin name ()
@org/plugin-name - File structure
- Operator/panel specs
- Input/output definitions
- Plugin name (
- Get user approval before coding
See PLUGIN-STRUCTURE.md for file formats.
- 搜索现有插件的相似模式
- 对于面板,默认使用混合模式(Python + JavaScript)。查看HYBRID-PLUGINS.md。
- 创建包含以下内容的方案:
- 插件名称()
@org/plugin-name - 文件结构
- Operator/面板规格
- 输入/输出定义
- 插件名称(
- 编码前获得用户认可
查看PLUGIN-STRUCTURE.md了解文件格式。
Phase 3: Generate Code
阶段3:生成代码
Create these files:
| File | Required | Purpose |
|---|---|---|
| Yes | Plugin manifest |
| Yes | Python operators/panels |
| If deps | Python dependencies |
| JS only | Node.js metadata |
| JS only | React components |
Reference docs:
- PYTHON-OPERATOR.md - Python operators
- PYTHON-PANEL.md - Python panels
- JAVASCRIPT-PANEL.md - React/TypeScript panels
- HYBRID-PLUGINS.md - Python + JavaScript communication
- EXECUTION-STORE.md - Persistent storage and caching
For JavaScript panels with rich UI: Invoke the skill for VOODO components (buttons, inputs, toasts, design tokens). VOODO is FiftyOne's official React component library.
fiftyone-voodo-design创建以下文件:
| 文件 | 是否必需 | 用途 |
|---|---|---|
| 是 | 插件清单 |
| 是 | Python operators/面板 |
| 有依赖时 | Python依赖 |
| 仅JS | Node.js元数据 |
| 仅JS | React组件 |
参考文档:
- PYTHON-OPERATOR.md - Python operators
- PYTHON-PANEL.md - Python面板
- JAVASCRIPT-PANEL.md - React/TypeScript面板
- HYBRID-PLUGINS.md - Python + JavaScript通信
- EXECUTION-STORE.md - 持久存储和缓存
对于具有丰富UI的JavaScript面板:调用技能获取VOODO组件(按钮、输入框、提示框、设计令牌)。VOODO是FiftyOne官方的React组件库。
fiftyone-voodo-designPhase 4: Validate & Test
阶段4:验证与测试
4.1 Validate Detection
4.1 验证插件检测
python
list_plugins(enabled=True) # Should show your plugin
list_operators() # Should show your operatorsIf not found: Check fiftyone.yml syntax, Python syntax errors, restart App.
python
list_plugins(enabled=True) # 应显示你的插件
list_operators() # 应显示你的operators如果未找到:检查fiftyone.yml语法、Python语法错误,重启App。
4.2 Validate Schema
4.2 验证Schema
python
get_operator_schema(operator_uri="@myorg/my-operator")Verify inputs/outputs match your expectations.
python
get_operator_schema(operator_uri="@myorg/my-operator")验证输入/输出是否符合预期。
4.3 Test Execution
4.3 测试执行
python
set_context(dataset_name="test-dataset")
launch_app()
execute_operator(operator_uri="@myorg/my-operator", params={...})Common failures:
- "Operator not found" → Check fiftyone.yml operators list
- "Missing parameter" → Check resolve_input() required fields
- "Execution error" → Check execute() implementation
python
set_context(dataset_name="test-dataset")
launch_app()
execute_operator(operator_uri="@myorg/my-operator", params={...})常见失败原因:
- "Operator not found" → 检查fiftyone.yml中的operators列表
- "Missing parameter" → 检查resolve_input()的必填字段
- "Execution error" → 检查execute()实现
Phase 5: Iterate
阶段5:迭代
- Get user feedback
- Fix issues (sync source and plugins directory if separate)
- Restart App if needed
- Repeat until working
- 获取用户反馈
- 修复问题(如果源码和插件目录分离,需同步)
- 必要时重启App
- 重复直到插件正常工作
Quick Reference
快速参考
Plugin Types
插件类型
| Type | Language | Use Case |
|---|---|---|
| Operator | Python | Data processing, computations |
| Panel | Hybrid (default) | Python backend + React frontend (recommended) |
| Panel | Python-only | Simple UI without rich interactivity |
| 类型 | 语言 | 适用场景 |
|---|---|---|
| Operator | Python | 数据处理、计算 |
| Panel | 混合模式(默认) | Python后端 + React前端(推荐) |
| Panel | 纯Python | 无丰富交互的简单UI |
Operator Config Options
Operator配置选项
| Option | Default | Effect |
|---|---|---|
| False | Recalculate inputs on change |
| False | Stream progress with yield |
| True | Execute in foreground |
| False | Background execution |
| False | Default to background |
| False | Hide from operator browser |
| False | Execute when app starts |
| False | Execute when dataset opens |
| 选项 | 默认值 | 作用 |
|---|---|---|
| False | 变更时重新计算输入 |
| False | 通过yield流式返回进度 |
| True | 前台执行 |
| False | 后台执行 |
| False | 默认后台执行 |
| False | 在operator浏览器中隐藏 |
| False | App启动时执行 |
| False | 数据集打开时执行 |
Panel Config Options
Panel配置选项
| Option | Default | Effect |
|---|---|---|
| False | Allow multiple panel instances |
| "grid" | Where panel can display ("grid", "modal", "grid modal") |
| None | Panel category in browser |
| None | Sort order in UI |
| 选项 | 默认值 | 作用 |
|---|---|---|
| False | 允许多个面板实例 |
| "grid" | 面板可显示的位置("grid", "modal", "grid modal") |
| None | 面板在浏览器中的分类 |
| None | UI中的排序顺序 |
Input Types
输入类型
| Type | Method |
|---|---|
| Text | |
| Number | |
| Boolean | |
| Dropdown | |
| File | |
| View | |
| 类型 | 方法 |
|---|---|
| 文本 | |
| 数字 | |
| 布尔值 | |
| 下拉框 | |
| 文件 | |
| 视图 | |
Minimal Example
最简示例
fiftyone.yml:
yaml
name: "@myorg/hello-world"
type: plugin
operators:
- hello_worldinit.py:
python
import fiftyone.operators as foo
import fiftyone.operators.types as types
class HelloWorld(foo.Operator):
@property
def config(self):
return foo.OperatorConfig(
name="hello_world",
label="Hello World"
)
def resolve_input(self, ctx):
inputs = types.Object()
inputs.str("message", label="Message", default="Hello!")
return types.Property(inputs)
def execute(self, ctx):
print(ctx.params["message"])
return {"status": "done"}
def register(p):
p.register(HelloWorld)fiftyone.yml:
yaml
name: "@myorg/hello-world"
type: plugin
operators:
- hello_worldinit.py:
python
import fiftyone.operators as foo
import fiftyone.operators.types as types
class HelloWorld(foo.Operator):
@property
def config(self):
return foo.OperatorConfig(
name="hello_world",
label="Hello World"
)
def resolve_input(self, ctx):
inputs = types.Object()
inputs.str("message", label="Message", default="Hello!")
return types.Property(inputs)
def execute(self, ctx):
print(ctx.params["message"])
return {"status": "done"}
def register(p):
p.register(HelloWorld)Debugging
调试
Where Logs Go
日志位置
| Log Type | Location |
|---|---|
| Python backend | Terminal running the server |
| JavaScript frontend | Browser console (F12 → Console) |
| Network requests | Browser DevTools (F12 → Network) |
| Operator errors | Operator browser in FiftyOne App |
| 日志类型 | 位置 |
|---|---|
| Python后端 | 运行服务器的终端 |
| JavaScript前端 | 浏览器控制台(F12 → Console) |
| 网络请求 | 浏览器开发者工具(F12 → Network) |
| Operator错误 | FiftyOne App中的operator浏览器 |
Running Server Separately (Recommended for Development)
单独运行服务器(开发推荐)
To see Python plugin logs, run the server and app separately:
bash
undefined要查看Python插件日志,单独运行服务器和App:
bash
undefinedTerminal 1: Run FiftyOne server (shows Python logs)
终端1:运行FiftyOne服务器(显示Python日志)
python -m fiftyone.server.main
python -m fiftyone.server.main
Terminal 2: Access the app in browser
终端2:在浏览器中访问App
Logs from print() and logging will appear in Terminal 1
print()和logging的日志将显示在终端1中
undefinedundefinedPython Debugging
Python调试
python
def execute(self, ctx):
# Use print() for quick debugging (shows in server terminal)
print(f"Params received: {ctx.params}")
print(f"Dataset: {ctx.dataset.name}, View size: {len(ctx.view)}")
# For structured logging
import logging
logging.info(f"Processing {len(ctx.target_view())} samples")
# ... rest of executionpython
def execute(self, ctx):
# 使用print()快速调试(显示在服务器终端)
print(f"Params received: {ctx.params}")
print(f"Dataset: {ctx.dataset.name}, View size: {len(ctx.view)}")
# 结构化日志
import logging
logging.info(f"Processing {len(ctx.target_view())} samples")
# ... 剩余执行代码JavaScript/TypeScript Debugging
JavaScript/TypeScript调试
typescript
// Use console.log in React components
console.log("Component state:", state);
console.log("Panel data:", panelData);
// Check browser DevTools:
// - Console: JS errors, syntax errors, plugin load failures
// - Network: API calls, variable values before/after executiontypescript
// 在React组件中使用console.log
console.log("Component state:", state);
console.log("Panel data:", panelData);
// 检查浏览器开发者工具:
// - Console: JS错误、语法错误、插件加载失败
// - Network: API调用、执行前后的变量值Common Debug Locations
常见调试位置
- Operator not executing: Check Network tab for request/response
- Plugin not loading: Check Console for syntax errors
- Variables not updating: Check Network tab for payload data
- Silent failures: Check Operator browser for error messages
- Operator未执行:检查Network标签页的请求/响应
- 插件未加载:检查Console中的语法错误
- 变量未更新:检查Network标签页的负载数据
- 静默失败:检查Operator浏览器中的错误消息
Troubleshooting
故障排除
Plugin not appearing:
- Check exists in plugin root
fiftyone.yml - Verify location:
~/.fiftyone/plugins/ - Check for Python syntax errors
- Restart FiftyOne App
Operator not found:
- Verify operator listed in
fiftyone.yml - Check function
register() - Run to debug
list_operators()
Secrets not available:
- Add to under
fiftyone.ymlsecrets: - Set environment variables before starting FiftyOne
插件未显示:
- 检查插件根目录是否存在
fiftyone.yml - 验证位置:
~/.fiftyone/plugins/ - 检查Python语法错误
- 重启FiftyOne App
Operator未找到:
- 验证中是否列出该operator
fiftyone.yml - 检查函数
register() - 运行调试
list_operators()
密钥不可用:
- 在的
fiftyone.yml下添加secrets: - 启动FiftyOne前设置环境变量
Advanced
高级功能
Programmatic Operator Execution
程序化执行Operator
python
undefinedpython
undefinedFor executing operators outside of FiftyOne App context
在FiftyOne App上下文外执行operator
import fiftyone.operators as foo
result = foo.execute_operator(operator_uri, ctx, **params)
undefinedimport fiftyone.operators as foo
result = foo.execute_operator(operator_uri, ctx, **params)
undefined