## 📑 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 输入框(v2 实现)**:点击 `🪄 AI 智能编辑` 按钮后,`OpenCodeInlinePrompt` widget 追加到当前 `CodeCell` 的 DOM 末尾,呈现内嵌输入面板:多行 `textarea`(placeholder "向 AI 描述你想要的修改…",无默认值)+ `🚀 发送` + `✕ 取消` 两个按钮。提交时前端仅收集 cell `source` / `outputs` / `error` 与用户在 textarea 里写的指令,POST `/opencode-bridge/edit`,body 为 `{prompt, context, providerId?, modelId?}`(**无 `mode`**)。**成功** → 前端调用 `cell.model.sharedModel.setSource(resp.finalSource)` 就地替换 cell 源码 + `Notification.info`;**失败** → `Notification.error`,输入框保留(可改文案重发)。Session 由服务端 `SessionManager` 保证 1 notebook 1 sid(前端不感知)。 3. **流式替换/Diff 预览**: * 点击发送后,支持**打字机实时替换**或在 Cell 下方出现 `[ Accept ]` / `[ Reject ]` 临时 Diff 比对面板。 --- ## 🔌 3. 接口契约设计 (API Contract) ### 接口:`POST /opencode-bridge/edit`(v2 — 无 `mode`) **Request Payload**(前端 `OpenCodeCellActions` 在用户提交 inline 输入框时发出): ```json { "prompt": "把这段代码改为用 plotly 绘制交互式折线图", "context": { "notebookPath": "analysis/demo.ipynb", "cellId": "cell-3", "language": "python", "cellIndex": 3, "totalCells": 5, "source": "import matplotlib.pyplot as plt\nplt.plot([1, 2, 3])", "previousCode": "import pandas as pd", "error": null }, "providerId": "anthropic", "modelId": "claude-sonnet-4-20250514" } ``` > **v2 变化**:body 不再含 `mode` / `action` 字段。前端只把 cell 的 input/output/error 收集到 `context`,加用户的自然语言 `prompt` 一起发出;后端用统一的 `UNIFIED_SYSTEM_PROMPT` + LLM 自己判断优化/排错/编辑。`providerId` / `modelId` 来自 JupyterLab settings 透传。 **Response**(当前 v2 实现:单次 JSON;SSE 流式属于 Slice 4 后续工作): ```json { "ok": true, "finalSource": "import plotly.express as px\nfig = px.line(x=[1, 2, 3], y=[1, 2, 3])\nfig.show()", "sessionId": "ses-abc123", "notebookPath": "analysis/demo.ipynb" } ``` > 成功时前端用 `finalSource` 就地替换 cell 源码(`cell.model.sharedModel.setSource`)。Session 由服务端 `SessionManager` 按 `notebookPath` 复用(1 notebook 1 sid),404 / `session not found` 时服务端自动 invalidate 并在下次请求重建。 --- ## 🛠️ 4. 核心前端实现逻辑 (TypeScript) 以下是前端实现“Toolbar 注入”和“上下文抓取”的伪代码骨架,可直接作为开发参考: ### `src/context_provider.ts` (上下文感知模块) ```typescript import { Notebook, NotebookTracker } from '@jupyterlab/notebook'; import { Cell } from '@jupyterlab/cells'; export interface CellContext { notebookPath: string; targetCode: string; errorOutput?: string; surroundingCells: string[]; } export function extractCellContext(tracker: NotebookTracker, targetCell: Cell): CellContext | null { const currentWidget = tracker.currentWidget; if (!currentWidget) return null; const notebook: Notebook = currentWidget.content; const cells = notebook.widgets; const targetIndex = cells.findIndex(c => c === targetCell); // 1. 抓取当前 Cell 的错误输出 let errorMsg = ''; if (targetCell.model.type === 'code') { const outputs = (targetCell.model as any).outputs; for (let i = 0; i < outputs.length; i++) { const out = outputs.get(i); if (out.type === 'error') { errorMsg += `${out.ename}: ${out.evalue}\n${(out.traceback || []).join('\n')}`; } } } // 2. 抓取前一个 Cell 的代码(上下文) const prevCode = targetIndex > 0 ? cells[targetIndex - 1].model.sharedModel.getSource() : ''; return { notebookPath: currentWidget.context.path, targetCode: targetCell.model.sharedModel.getSource(), errorOutput: errorMsg, surroundingCells: [prevCode] }; } ``` ### `src/components/cell_toolbar.ts` (工具栏与内嵌框注入) ```typescript import { Cell } from '@jupyterlab/cells'; export function injectOpenCodeActionBox(cell: Cell, onSendPrompt: (prompt: string) => void) { // 避免重复注入 if (cell.node.querySelector('.opencode-toolbar')) return; // 1. 创建 Toolbar 容器 const toolbar = document.createElement('div'); toolbar.className = 'opencode-toolbar'; toolbar.innerHTML = ` `; // 2. 绑定展开事件 toolbar.querySelector('.edit-btn')!.addEventListener('click', () => { let promptBox = cell.node.querySelector('.opencode-inline-box') as HTMLDivElement; if (promptBox) { promptBox.style.display = promptBox.style.display === 'none' ? 'block' : 'none'; return; } // 动态创建 Inline Prompt 输入框 promptBox = document.createElement('div'); promptBox.className = 'opencode-inline-box'; promptBox.innerHTML = `