ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Agent Zero Pin to Top 插件:侧边栏置顶的持久化状态、API 端点与行级扩展契约

Agent Zero Pin to Top 插件:侧边栏置顶的持久化状态、API 端点与行级扩展契约 Agent Zero Pin to Top 插件侧边栏置顶的持久化状态、API 端点与行级扩展契约【免费下载链接】agent-zeroAgent Zero AI framework项目地址: https://gitcode.com/GitHub_Trending/ag/agent-zeroPin to Top 是 Agent Zero 内置的_pin_to_top插件负责让会话chat与定时任务task两列侧边栏列表中的行支持置顶操作。本文以该插件的 DOX 文档为核心结合 plugin.yaml、持久化状态实现、API 端点、前端 Alpine Store 与配套测试完整拆解置顶功能的持久化数据结构、排序与分隔线回调契约以及插件与侧边栏之间的解耦边界。读完本文你可以理解该插件零配置、始终启用的设计如何落地并能以它为标准范式向侧边栏行菜单贡献自己的扩展。一、插件定位内置、常开、无配置按照 AGENTS.md 的 Purpose 一节该插件拥有侧边栏中 chat 与 scheduled-task 两列行的内置置顶能力。README 将其行为归纳为四点在侧边栏行菜单中提供上下文感知的Pin to Top / Unpin from Top动作置顶项按置顶先后排序先置顶的排前面未置顶组内部保持原有顺序用标准侧边栏分隔线把置顶组与未置顶组分开状态持久化在 Agent Zero 的用户键值存储persistent KVP中。插件元数据 plugin.yaml 决定了它的部署形态name: _pin_to_top title: Pin to Top description: Pin chats and scheduled tasks to the top of their sidebar lists. version: 1.0.0 settings_sections: [] per_project_config: false per_agent_config: false always_enabled: true几个字段直接对应 DOX 中Keep the plugin always enabled and configuration-free的工作约束字段取值含义always_enabledtrue插件常驻启用不出现在启用/停用开关中settings_sections[]没有设置界面DOX 明确要求保持零配置per_project_config/per_agent_configfalse不做项目级、Agent 级配置分叉这也解释了为什么仓库中该插件目录下没有任何设置 UI 文件——置顶状态是纯粹的用户级运行数据而非插件配置。二、持久化状态plugin_pin_to_topKVP 键与数据结构DOX 的 Local Contracts 第一条规定Pin 状态按chat与task两个分组隔离存储在plugin_pin_to_top持久化 KVP 键下。核心实现在 helpers/pins.pyPinKind Literal[chat, task] STORE_KEY plugin_pin_to_top KINDS: tuple[PinKind, ...] (chat, task) _lock threading.RLock()数据形态是一个两层字典外层键为分组chat/task内层键为行id值为置顶时刻的时间戳time.time()的浮点值例如{ chat: { chat-1: 1700000000.0 }, task: { task-1: 1700000100.0 } }时间戳是排序的锚点——前端据此让更早置顶的排在前面。2.1 读写入口get_pins()读取并归一化后返回两组 pinstoggle_pin(kind, item_id)是一次原子切换已存在则删除并返回(False, 0.0)不存在则写入当前时间戳并返回(True, timestamp)def toggle_pin(kind: str, item_id: str) - tuple[bool, float]: normalized_kind _require_kind(kind) normalized_id _require_item_id(item_id) with _lock: pins _normalize(kvp.get_persistent(STORE_KEY, {})) kind_pins pins[normalized_kind] if normalized_id in kind_pins: del kind_pins[normalized_id] timestamp 0.0 pinned False else: timestamp time.time() kind_pins[normalized_id] timestamp pinned True kvp.set_persistent(STORE_KEY, pins) return pinned, timestamp整个模块用threading.RLock包裹读写路径保证并发请求下读-改-写序列的一致性。2.2 输入校验与防御性归一化两处校验函数把非法输入挡在状态写入之前_require_kindkind必须是chat或task否则抛ValueError_require_item_iditem_id去除首尾空白后不能为空且长度不得超过 512。_normalize则负责把磁盘上可能损坏的历史数据读成干净结构非 dict 直接返回空分组逐条检查时间戳可转float且大于 0、id 去除空白后非空任何一条不合法就静默丢弃该条目而不是让异常扩散到 UI。2.3 底层存储位置DOX 的 Work Guidance 要求Keep runtime state underusr/through the shared persistent KVP helper。这正是 helpers/kvp.py 的行为持久化键会被写成usr/kvp/目录下的 JSON 文件_key_to_path返回os.path.join(usr, kvp, f{key}.json)并且键名本身经过校验——不能为空、不能含 NUL、不能含路径分隔符防止通过键名越权写文件。因此该插件的运行时状态实际落盘在usr/kvp/plugin_pin_to_top.json。三、API 端点get_pins与toggle_pin插件的api/目录提供两个认证端点由 api/get_pins.py 与 api/toggle_pin.py 实现前端调用路径分别为/plugins/_pin_to_top/get_pins与/plugins/_pin_to_top/toggle_pin。GET 语义的GetPins直接透传持久化状态class GetPins(ApiHandler): async def process(self, input: Input, request: Request) - Output: return {ok: True, pins: get_pins()}TogglePin从请求体取kind与item_id把状态层的ValueError映射为 HTTP 400try: pinned, timestamp toggle_pin( str(input.get(kind, )), str(input.get(item_id, )), ) except ValueError as error: return Response(str(error), 400) return {ok: True, pinned: pinned, timestamp: timestamp}成功响应返回切换后的状态pinned: true时携带新时间戳供前端直接更新本地缓存pinned: false时时间戳归零。安全属性上两个 handler 都继承ApiHandler的默认值登录认证与 CSRF 防护同时开启。tests/test_pins.py 中有专门断言for handler in (GetPins, TogglePin): assert handler.requires_auth() is True assert handler.requires_csrf() is True四、前端 Store排序、分隔线与缓存同步webui/pin-to-top-store.js 用 Alpine.js 的createStore注册了pinToTopstore它是 DOX 中owns row ordering and divider callbacks一行的落点。4.1 初始化向侧边栏注册回调init()只做一次_initialized幂等守卫对chat与task两种kind分别调用侧边栏 store 的registerRowListExtension挂入sort与dividerBefore两个回调for (const kind of [chat, task]) { sidebarStore.registerRowListExtension(kind, PLUGIN_ID, { sort: (items) this.sortItems(kind, items), dividerBefore: (item, index, items) this.dividerBefore(kind, item, index, items), }); } await this.loadPins();这就是 DOX 本地契约中必须注册 sidebar row-list 回调不得 patch chat/task store 或直接向行内注入控件的具体含义插件只通过侧边栏预留的扩展点参与列表渲染不侵入聊天与任务 store 自身的数据流。4.2 排序算法置顶优先、时间戳升序、稳定保持sortItems实现了 DOX 中Pinned items sort before unpinned items, older pins remain first, and existing order is preserved within the unpinned group的三条规则return items .map((item, index) ({ item, index })) .sort((left, right) { const leftPinned kindPins[left.item.id] ! undefined; const rightPinned kindPins[right.item.id] ! undefined; if (leftPinned ! rightPinned) return leftPinned ? -1 : 1; // 规则一 if (leftPinned leftPin ! rightPin) return leftPin - rightPin; // 规则二 return left.index - right.index; // 规则三 }) .map(({ item }) item);置顶项整体排在未置顶项之前比较置顶与否的布尔值同为置顶项时按时间戳升序——先置顶的时间戳更小排在前面的正是older pins remain first其余情况回退到原始下标保证未置顶组内顺序稳定不受重排影响。dividerBefore判断是否需要在某行前插入分隔线当前行未置顶、且前一行已置顶恰好落在两个组的交界处于是置顶组与未置顶组之间出现一条标准侧边线分隔。4.3 状态同步从菜单 id 反解 item id行菜单打开时侧边栏 store 会把rowMenuOpenId设为形如chat:id或task:id的字符串前缀规则见 tests/test_sidebar_row_actions.py 中对$store.sidebar.rowMenuToggle(task:${task.id})的断言。store 中的itemIdFromMenu据此反解itemIdFromMenu(menuId, kind) { const prefix ${kind}:; return typeof menuId string menuId.startsWith(prefix) ? menuId.slice(prefix.length) : ; }切换请求成功后前端就地更新本地pins缓存pinned: true写入时间戳否则删除该 key避免再发一次get_pins请求加载或切换失败时通过toastFrontendError弹出带 Pin to Top 标题的提示。五、行菜单动作图标与文案随状态翻转DOX 要求菜单标签和图标必须反映当前行是否已置顶。扩展文件 extensions/webui/sidebar-row-actions-menu/pin-to-top.html 是一个 Alpine 驱动的 dropdown 项挂到侧边栏统一固定的行菜单扩展点sidebar-row-actions-menu上x-icon :name$store.pinToTop.isMenuItemPinned(...) ? keep_off : push_pin/x-icon span x-text$store.pinToTop.isMenuItemPinned(...) ? Unpin from Top : Pin to Top/span未置顶行显示图钉push_pin图标与 Pin to Top 文案已置顶行则换成keep_off与 Unpin from Top。点击时调用toggleFromMenu并用rowMenuClose()关闭菜单。六、侧边栏扩展机制插件如何非侵入式参与列表上面所有回调最终汇入 webui/components/sidebar/sidebar-store.js 的扩展机制这也是理解本插件架构价值的关键registerRowListExtension(kind, name, extension) { if (!this.rowListExtensions[kind] || !name) return; this.rowListExtensions { ...this.rowListExtensions, [kind]: { ...this.rowListExtensions[kind], [name]: extension }, }; } sortRows(kind, rows) { return Object.values(this.rowListExtensions[kind] || {}).reduce( (result, extension) extension.sort?.(result) || result, [...rows], ); } hasRowDividerBefore(kind, item, index, rows) { return Object.values(this.rowListExtensions[kind] || {}).some((extension) extension.dividerBefore?.(item, index, rows), ); }从源码结构看侧边栏按kind维护一组以扩展名为 key 的扩展集合sortRows把原始行依次喂给各扩展的sortreduce 链hasRowDividerBefore则取任一扩展要求分隔线的逻辑或。多个插件如本插件与负责重命名的_chat_naming可以各自贡献菜单动作与排序规则而互不干扰——每个扩展以插件 id 命名注册行菜单本体是left-sidebar.html中唯一一个固定定位position: fixed; z-index: 9999;的 dropdown由x-extension idsidebar-row-actions-menu聚合各插件贡献的 HTML 片段。七、验证契约测试如何锁死边界DOX 的 Verification 一节给出的命令是pytest plugins/_pin_to_top/tests tests/test_sidebar_row_actions.py两个测试文件分别从实现和契约两侧锁定行为tests/test_pins.py 用monkeypatch把pins.kvp替换为内存字典模拟持久层覆盖同一次会话内 chat 与 task 分组互不干扰切换后再切换即取消toggle_pin(chat, chat-1)第二次返回(False, 0.0)非法输入未知 kind、空 item_id、长度 513 的 item_id全部抛ValueError未知 kind 经TogglePin端点返回status_code 400两个端点默认开启认证与 CSRF。tests/test_sidebar_row_actions.py 则以静态断言方式验证插件没有越界assert registerRowListExtension in plugin_store assert MutationObserver not in plugin_store assert applyContexts not in plugin_store assert applyTasks not in plugin_store assert registerRowListExtension(kind, name, extension) in sidebar_store assert hasRowDividerBefore(kind, item, index, rows) in sidebar_store它同时校验了菜单文案/图标翻转Unpin from Top : Pin to Top、keep_off : push_pin、chat 与 task 行各自有 hover 溢出动作用、以及侧边栏使用唯一固定行菜单与标准关闭行为点击外部、Esc 关闭。这套断言把不得直接 patch chat/task store从文档约定变成了可执行的回归测试。八、所有权与契约速查汇总 DOX 的 Ownership 与 Local Contracts配合源码落点可以形成如下速查表职责归属文件关键实现插件元数据常开、零配置plugin.yamlalways_enabled: true、settings_sections: []持久化状态与切换逻辑helpers/pins.pyKVP 键plugin_pin_to_top、chat/task分组、RLock保护认证 APIapi/get_pins.py、api/toggle_pin.py默认认证 CSRF非法输入返回 400排序与分隔线回调webui/pin-to-top-store.jssortItems三规则排序、dividerBefore交界检测行菜单动作pin-to-top.html图标/文案随isMenuItemPinned翻转侧边栏扩展机制sidebar-store.jsregisterRowListExtension/sortRows/hasRowDividerBefore行为与契约测试tests/test_pins.py、tests/test_sidebar_row_actions.py持久化语义、输入校验、认证默认值、非侵入断言九、小结Pin to Top 插件展示了 Agent Zero 中一个典型的小型内置插件范式状态层用带锁和防御性归一化的 KVP 读写保持简单可靠API 层复用框架的认证与 CSRF 默认值前端层不修改宿主 store而是通过registerRowListExtension注册的sort/dividerBefore回调参与列表呈现再用一个 Alpine 扩展片段向统一行菜单贡献按钮。零配置、常开、状态落盘于usr/kvp/、契约由 pytest 静态断言兜底——这些约束在 AGENTS.md 中写成开发守则在源码与测试中得到了一一对应。对想要为侧边栏添加自定义动作或排序规则的开发者来说该插件的目录结构api/webui/extensions/webui/tests/与扩展点用法可直接作为参照模板。【免费下载链接】agent-zeroAgent Zero AI framework项目地址: https://gitcode.com/GitHub_Trending/ag/agent-zero创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表