## 📑 1. 系统总体架构 采用 **前端 UI 注入 + 本地 Server Extension 透传** 的轻量双层架构: ```text ┌────────────────────────────────────────────────────────────────────────┐ │ JupyterLab / Notebook v7 (前端 TS) │ │ │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ Cell Component │ │ │ │ [CodeEditor] ─── (挂载) ───► [Cell Action Toolbar] │ │ │ │ │ (点击 🪄 智能编辑) │ │ │ ▼ │ │ │ [Inline Prompt Box] │ │ │ └───────────────────────────────────┬────────────────────────────┘ │ │ │ │ │ Context Provider 收集 │ │ (Target Cell, Error, Neighbors) │ │ │ │ └───────────────────────────────────────┼────────────────────────────────┘ │ REST / SSE Stream ▼ ┌────────────────────────────────────────────────────────────────────────┐ │ Jupyter Server Extension (后端 Python) │ │ │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ OpenCode Bridge Handler │ │ │ │ - 接收 Context & Prompt │ │ │ │ - 拼装标准 OpenCode API Payload │ │ │ │ - SSE 流式透传代理 (Stream Proxy) │ │ │ └───────────────────────────────────┬────────────────────────────┘ │ └───────────────────────────────────────┼────────────────────────────────┘ │ Local Socket / HTTP ▼ ┌────────────────────────────────────────────────────────────────────────┐ │ OpenCode Serve 进程 │ └────────────────────────────────────────────────────────────────────────┘ ``` --- ## 🎨 2. 交互与UI流程设计 1. **原生 Cell Toolbar 单按钮(2026-07-23 v2 修正)**:不再自绘悬浮 toolbar,也合并了 v1 的三个 mode(优化/排错/编辑)为单个 `🪄 AI 智能编辑` 图标按钮。通过 `IToolbarWidgetRegistry.addFactory('Cell', 'opencode-cell-actions', ...)` 注入 JupyterLab 原生 cell toolbar,显示在 **active cell 右上角**(move up/down 按钮旁,`rank: 100` 排在 delete 右侧);仅 `CodeCell` 显示,随 active cell 切换移动。**前端不做任何语义处理** —— 不再发 `mode` 字段,后端用统一的系统提示词 + 用户的自然语言指令 + 上下文(含 traceback)由 LLM 自己判断是优化/排错/编辑。 2. **Inline Prompt 输入框(v3-final)**:点击 `🪄 AI 智能编辑` 按钮后,`OpenCodeInlinePrompt` widget 追加到当前 `CodeCell` 的 DOM 末尾,呈现内嵌输入面板: * 顶部一行 `模型:
`; promptBox.querySelector('.submit-btn')!.addEventListener('click', () => { const input = promptBox.querySelector('textarea')!.value; if (input.trim()) onSendPrompt(input); }); cell.node.appendChild(promptBox); }); cell.node.insertBefore(toolbar, cell.node.firstChild); } ``` --- ## 🔗 5. Session 管理策略(v0.2.1 修正) > **2026-07-22 修正**:原 v0.2 设计 `EditHandler` 采用"每请求一个 session + finally 清理"——这是错的。OpenCode 的 session 是有状态的对话上下文,每个 notebook 必须复用同一个 session,否则用户先优化 cell1、再编辑 cell2 时,AI 看不到前一次的结果。 ### 5.1 核心规则 | 维度 | 决策 | |---|---| | 映射关系 | `notebookPath`(1 个)→ OpenCode `sessionID`(1 个) | | 生命周期 | 首次 `EditHandler` 调用时 lazy create;显式 `DELETE /session` 才释放 | | 并发 | 同 notebook 的并发请求用 `asyncio.Lock` 串行化 session 创建;消息发送并发无锁(OpenCode 端自己排队) | | 失效恢复 | 当 `send_message_sync` 抛 `OpenCodeError` 且消息含 404/410 → 自动 invalidate 缓存,下次请求重新 create | | 清理 | **不做自动释放**。前端 notebook 关闭时**不**调 release(避免两个 tab 共享 session 时误杀)。用户可手动点"重置对话"触发 release | | 跨重启 | Jupyter server 重启 → in-memory map 丢失 → OpenCode 端孤立 session 接受泄漏(v0.4 加启动清理) | ### 5.2 SessionManager 接口 ```python class SessionManager: def __init__(self, client_factory: Callable[[], OpenCodeClient]): ... async def get_or_create(self, notebook_path: str) -> str: """Return existing sessionID, or create one. Async-safe via per-path lock.""" async def release(self, notebook_path: str) -> bool: """Delete session and remove from map. Returns True if session existed.""" def has_session(self, notebook_path: str) -> bool: ... def list_sessions(self) -> list[dict]: """For debug: [{notebookPath, sessionId}, ...]""" ``` **关键不变量**: - `get_or_create` 永远不返回 None 或抛 "not found" - 重复调用 `get_or_create(same_path)` → 同一 sessionID - `release(path)` 后再 `get_or_create(path)` → 新 sessionID ### 5.3 HTTP 端点 | Method | Path | 作用 | 备注 | |---|---|---|---| | `GET` | `/opencode-bridge/sessions` | 列出所有 active session | debug 用 | | `DELETE` | `/opencode-bridge/session?notebook=` | 显式释放指定 notebook 的 session | URL-encode path | **为什么用 query param 而不是 path param**: - notebook path 可能含 `/`、`空格`、`中文` - `url_path_join` + regex 捕获在含 `/` 的路径上行为不可控 - query string 是 RFC 3986 标准做法,tornado 自动 decode ### 5.4 EditHandler 改造 **改造前(错的)**: ``` create_session → send_message_sync → delete_session # 每个请求都这样 ``` **改造后(对的)**: ``` session_id = session_manager.get_or_create(notebook_path) # 首次创建,复用后续 send_message_sync(session_id, ...) # 不管 session # 没有 delete — session 留着给下次 ``` ### 5.5 错误恢复 `send_message_sync` 失败时: 1. 检查 `OpenCodeError` 消息是否含 "404" 或 "session not found" 2. 是 → `session_manager.invalidate(notebook_path)`,下次 `get_or_create` 会重新创建 3. 否 → 透传错误给前端 v0.2.1 不实现自动重试,只 invalidate。前端看到 502 后可重发。 ### 5.6 未解决问题(v0.4+) 1. **TTL reaper**:notebook 关闭后 session 永远不释放 → 改用 mtime 追踪 + 30 分钟 idle 自动 release 2. **多 server 横向扩展**:in-memory state 不能 scale → 改成 Redis 或 sticky session 3. **启动清理**:Jupyter 重启后调 `GET /session` 列出 OpenCode 端的孤儿,逐个 delete 4. **并发 LLM 调用**:当前两次 `send_message_sync` 到同一 session 是串行(OpenCode 端排队)— 可加 `asyncio.Semaphore(per_session=1)` 显式控制 --- ## ⚙️ 6. 启动配置 + 动态模型选择(v3-final,2026-07-23) > **v3-final 修正**:`opencode_bridge` 插件**完全不再使用 JupyterLab Settings Editor**(`schema/plugin.json` 的 `properties: {}`)。所有 OpenCode Serve 连接参数在 `jupyter lab` 启动时通过环境变量指定;模型在 inline 输入框中动态选择(见 §2 item 2),无任何持久默认。 ### 6.1 启动环境变量 | 变量 | 默认 | 说明 | |---|---|---| | `OPENCODE_BRIDGE_URL` | `http://127.0.0.1:4096` | OpenCode Serve 地址 | | `OPENCODE_BRIDGE_USER` | `opencode` | HTTP Basic Auth 用户名 | | `OPENCODE_BRIDGE_PASSWORD` | `""`(无 auth) | HTTP Basic Auth 密码 | | `OPENCODE_BRIDGE_TIMEOUT` | `120` | 请求 OpenCode Serve 超时(秒,整数) | 由 `opencode_bridge/config.py` `resolve_config` 统一解析,**不再**从 JupyterLab settings 字典兜底(`jupyter_settings` 参数保留仅为 API 兼容,实际未读)。 ### 6.2 模型选择 `opencodeProvider` / `opencodeModel` v0.2.2 settings 字段**已移除**。inline 输入框顶部 ` 列出所有 (provider, model) └─ 用户选一项 → 提交时 └─ OpenCodeRequest.providerId / modelId └─ POST /opencode-bridge/edit ``` - 默认选中:下拉框**第一项**(无 settings 可作默认)。 - 拉取失败(OpenCode Serve 未起)→ 缓存为 `null` → inline 不渲染 select → 提交时省略 `providerId`/`modelId`,OpenCode Serve 用其默认。**优雅降级**。 ### 6.3 行为变更(v0.2.2 → v3-final) - 之前 6 个 JupyterLab settings 字段(`opencodeServerUrl` / `User` / `Password` / `requestTimeoutSeconds` / `opencodeProvider` / `opencodeModel`)**全部失效**,需改用环境变量(连接类)或在 inline 输入框里挑(模型)。 - 连接配置从"可在 Settings Editor 改"变为"启动时定",更符合服务端连接参数的语义。 - 模型从"settings 持久默认"变为"每次手选",无持久化偏好。