Block
Block 扩展框架是一个通用的 UI 扩展解决方案,提供轻量级的单文件 UI 扩展方法,支持 React 组件渲染,同时在不同环境中保持可重用性。
1. 设计原则
Section titled “1. 设计原则”1.1 轻量级设计
Section titled “1.1 轻量级设计”- 采用单文件组件方法,便于项目间重用代码
- 复杂组件建议使用传统方法开发并发布到 npm
- 保持关注点分离,减少维护开销
1.2 无构建开发体验
Section titled “1.2 无构建开发体验”- 零配置构建,保存后立即预览更改
- 自动将导入转换为最新的外部包版本
- 利用 npm 生态系统,减少版本管理复杂性
1.3 导入解析策略
Section titled “1.3 导入解析策略”使用标准 Node.js 导入语法,自动解析到外部包:
// 标准导入语法import { Excalidraw } from "@excalidraw/excalidraw"
// 自动解析为: https://esm.sh/@excalidraw/excalidraw2. 内置组件库集成
Section titled “2. 内置组件库集成”2.1 Shadcn/ui 集成
Section titled “2.1 Shadcn/ui 集成”提供与 Shadcn/ui 组件的无缝集成,实现与 Eidos 界面一致的 UI 样式:
import { Button } from "@/components/ui/button"组件与主应用程序共享主题配置,同时支持独立的主题自定义。
2.2 AI 辅助开发
Section titled “2.2 AI 辅助开发”Shadcn/ui 的 LLM 友好架构促进了 AI 辅助开发,在简单场景下无需手动编码即可生成代码。
3. 运行时环境
Section titled “3. 运行时环境”Block 组件在标准 React 组件的浏览器环境中执行。在 Eidos Desktop 中,每个 Block 都在独立域中运行:
<extid>.block.<spaceId>.eidos.localhost:131274. 扩展类型和规范
Section titled “4. 扩展类型和规范”4.1 表格视图扩展
Section titled “4.1 表格视图扩展”提供超出默认网格、画廊和看板视图的自定义可视化选项。
interface TableViewMeta { type: "tableView" componentName: string tableView: { title: string type: string // 内置类型: grid, gallery, kanban description: string }}自定义扩展视图类型信息存储在 eidos__views 表中,采用 ext__<type> 格式:
- 内置视图:
grid、gallery、kanban - 自定义扩展:
ext__list、ext__timeline、ext__chart等
URL 访问模式
Section titled “URL 访问模式”当作为表格视图扩展渲染时,块将访问以下 URL 结构:
<extid>.block.<spaceId>.eidos.localhost:13127/<tableid>/<viewid>export const meta = { type: "tableView", componentName: "MyListView", tableView: { title: "列表视图", type: "list", description: "这是一个列表视图", },}
export function MyListView() { const [rows, setRows] = useState<any[]>([])
// 从 URL 中获取 tableId 和 viewId const pathParts = window.location.pathname.split("/") const tableId = pathParts[pathParts.length - 2] const viewId = pathParts[pathParts.length - 1]
useEffect(() => { eidos.currentSpace.table(tableId).rows.query({}, { viewId }).then(setRows) }, [tableId, viewId])
return ( <div> {rows.map((row) => ( <div key={row.id}>{row.title}</div> ))} </div> )}4.2 扩展节点类型
Section titled “4.2 扩展节点类型”提供超出默认文档和表格节点的自定义节点类型,具有一致的目录树行为但自定义渲染逻辑。
URL 访问模式
Section titled “URL 访问模式”<extid>.block.<spaceId>.eidos.localhost:13127/<nodeid>interface ExtNodeMeta { type: "extNode" componentName: string extNode: { title: string description: string type: string }}客户端数据检索(仅本地):
export const meta = { type: "extNode", componentName: "MyExcalidraw", extNode: { title: "Excalidraw", description: "这是一个 excalidraw 节点", type: "excalidraw", },}
export function MyExcalidraw() { const [initialData, setInitialData] = useState("") const nodeId = window.location.pathname.split("/").pop()
useEffect(() => { eidos.currentSpace.extNode.getText(nodeId).then((text) => { setInitialData(JSON.parse(text)) }) }, [nodeId])
return <Excalidraw initialData={initialData} />}服务端数据检索(可发布):
export const loader = async () => { const nodeid = request.url.split("/").pop() const text = await eidos.currentSpace.extNode.getText(nodeid) return { props: { text } }}
export function MyExtNode({ text }: { text: string }) { return <div>{text}</div>}4.3 文件处理器扩展
Section titled “4.3 文件处理器扩展”提供自定义文件类型处理器,类似操作系统的”打开方式”功能,让 Block 扩展可以注册为特定文件类型的处理器。
URL 访问模式
Section titled “URL 访问模式”文件路径通过 hash 传递:
<extid>.block.<spaceId>.eidos.localhost:13127#<filePath>重要提示: 文件路径在 URL hash 中是经过 URL 编码的(例如空格会被编码为 %20),因此在读取时需要先移除开头的 #,然后使用 decodeURIComponent() 进行解码。
支持的文件路径格式:
| 路径格式 | 说明 | 示例 |
|---|---|---|
~/path/to/file | 项目文件夹(.eidos 所在目录) | ~/readme.md |
@/mount/path/file | 挂载文件夹(需授权) | @/music/song.mp3 |
URL 示例:
# 打开项目中的 README 文件markdown-editor.block.my-space.eidos.localhost:13127#~/readme.md
# 播放挂载的音乐文件(包含空格的文件名会被编码)audio-player.block.my-space.eidos.localhost:13127#@/music/my%20song.mp3interface FileHandlerMeta { type: "fileHandler" componentName: string fileHandler: { title: string // 处理器名称 description: string // 描述 extensions: string[] // 支持的扩展名,如 [".md", ".markdown"] icon?: string // 可选图标 }}处理器选择逻辑
Section titled “处理器选择逻辑”系统会自动查询 eidos__extensions 表找出所有支持该文件扩展名的处理器:
- 如果只有一个处理器,直接使用
- 如果有多个处理器,查询用户设置的默认处理器(存储在 KV 表中)
- 如果没有设置默认,提示用户选择
默认处理器存储:
// 仅在存在多个处理器时才需要在 KV 表中存储用户偏好// 键格式: eidos:space:file:handler:default:<扩展名>// 值: 处理器扩展 ID
await eidos.currentSpace.kv.put( `eidos:space:file:handler:default:.md`, "markdown-editor-ext-id")Markdown 编辑器:
export const meta = { type: "fileHandler", componentName: "MarkdownEditor", fileHandler: { title: "Markdown 编辑器", description: "支持实时预览的 Markdown 编辑器", extensions: [".md", ".markdown"], icon: "📝", },}
export function MarkdownEditor() { const [content, setContent] = useState("") // 从 hash 中获取文件路径,需要先移除 # 然后解码 URL 编码 const filePath = decodeURIComponent(window.location.hash.slice(1))
useEffect(() => { // 读取文件内容 eidos.currentSpace.fs .readFile(filePath, "utf8") .then(setContent) .catch((err) => { eidos.currentSpace.notify({ title: "错误", description: `无法读取文件: ${err.message}` }) }) }, [filePath])
const handleSave = async () => { try { await eidos.currentSpace.fs.writeFile(filePath, content, "utf8") eidos.currentSpace.notify("文件已保存") } catch (err) { eidos.currentSpace.notify({ title: "错误", description: `保存失败: ${err.message}` }) } }
return ( <div className="flex h-screen"> <textarea value={content} onChange={(e) => setContent(e.target.value)} className="w-1/2 p-4" /> <div className="w-1/2 p-4 markdown-preview"> <ReactMarkdown>{content}</ReactMarkdown> </div> <button onClick={handleSave}>保存</button> </div> )}音频播放器:
export const meta = { type: "fileHandler", componentName: "AudioPlayer", fileHandler: { title: "音频播放器", description: "支持多种音频格式的播放器", extensions: [".mp3", ".flac", ".wav", ".ogg", ".m4a"], icon: "🎵", },}
export function AudioPlayer() { const [audioUrl, setAudioUrl] = useState("") // 从 hash 中获取文件路径,需要先移除 # 然后解码 URL 编码 const filePath = decodeURIComponent(window.location.hash.slice(1))
useEffect(() => { // 构造音频 URL(通过 Eidos 的文件服务) const url = `http://${window.location.host}${filePath}` setAudioUrl(url) }, [filePath])
return ( <div className="flex flex-col items-center justify-center h-screen"> <h2 className="text-2xl font-bold mb-4">{filePath.split("/").pop()}</h2> <audio controls src={audioUrl} className="w-full max-w-md"> 您的浏览器不支持音频播放 </audio> </div> )}文件访问 API
Section titled “文件访问 API”使用 eidos.currentSpace.fs API 访问文件:
// 读取文本文件const text = await eidos.currentSpace.fs.readFile(filePath, "utf8")
// 读取二进制文件const data = await eidos.currentSpace.fs.readFile(filePath)
// 写入文件await eidos.currentSpace.fs.writeFile(filePath, content, "utf8")
// 获取文件信息const stats = await eidos.currentSpace.fs.stat(filePath)更多文件系统 API 详情,请参阅 Space API 参考 - 文件系统 API。
5. 指令系统
Section titled “5. 指令系统”Block 支持特殊的指令来控制组件的行为和渲染方式。这些指令通过字符串字面量在代码顶部声明。
5.1 use sidebar 指令
Section titled “5.1 use sidebar 指令”use sidebar 指令用于指示 Block 组件应该在左侧边栏中渲染,而不是在主区域打开。
当 Block 被添加到左侧边栏标签页时,默认行为是点击后在主区域打开独立的 Block 页面:
// 默认行为 - 在主区域打开export function MyDashboard() { return ( <div> <h1>仪表盘</h1> <p>这个组件会在主区域的独立页面中显示</p> </div> )}这种默认行为适用于:
- 仪表盘组件
- 数据可视化
- 详细内容页面
- 需要较大显示区域的功能
使用指令改变行为
Section titled “使用指令改变行为”如果希望 Block 在侧边栏中直接展示(如导航栏、工具栏等),需要添加 use sidebar 指令:
"use sidebar"
// 在侧边栏中渲染export function MyNavigation() { const navigateToTable = () => { eidos.currentSpace.navigate("/table_123") }
const navigateToToday = () => { const today = new Date().toISOString().split("T")[0] eidos.currentSpace.navigate(`/${today}`) }
return ( <div className="space-y-2"> <h3 className="font-medium">快速导航</h3>
<button onClick={navigateToTable} className="w-full text-left px-3 py-2 rounded hover:bg-gray-100" > 📊 我的表格 </button>
<button onClick={navigateToToday} className="w-full text-left px-3 py-2 rounded hover:bg-gray-100" > 📅 今日页面 </button>
<button onClick={() => eidos.currentSpace.navigate("/extensions")} className="w-full text-left px-3 py-2 rounded hover:bg-gray-100" > 🔧 扩展管理 </button> </div> )}指令行为说明:
- 包含
use sidebar指令的 Block 点击后会在侧边栏中直接渲染组件内容 - 不包含此指令的 Block 点击后导航到主区域的独立 Block 页面
- 指令必须作为字符串字面量出现在文件顶部
适用场景:
- 导航栏组件
- 工具栏组件
- 快速操作面板
- 状态显示组件
- 侧边栏工具
6. 安全注意事项
Section titled “6. 安全注意事项”扩展执行应该被适当沙箱化,防止未经授权的系统访问。实现必须验证组件属性,并在扩展和主机应用程序之间强制执行适当的隔离。
7. 实现要求
Section titled “7. 实现要求”- 需要特定扩展功能时,应导出符合指定接口的
meta对象 - 不导出
meta对象时,组件作为普通 React 组件运行 - 导出
meta对象时,meta.componentName必须与实际导出的组件匹配 - 应实现适当的错误边界和加载状态
- 与应用程序数据交互时,必须通过 Eidos SDK 执行
8. 未来扩展
Section titled “8. 未来扩展”本规范可能会扩展以支持其他扩展类型,包括但不限于:
- 自定义字段渲染器: 为表格字段提供自定义渲染和编辑组件
- MIME 类型支持: 文件处理器支持基于 MIME 类型的匹配
- 协议处理器: 支持自定义 URL 协议(如
eidos://,notion://) - 上下文菜单扩展: 为文件和节点提供自定义右键菜单项
- 命令面板扩展: 注册自定义命令到全局命令面板