1. 页面概览
1.1 是什么
Notebook 分析页(Notebook · 代码工作簿)是 LightFoundry 在 V5 Stage 3(B3-1)交付的分析协作闭环页面,对标 OpenFoundry notebook-service 的 Notebook / Cell / Session 三段式模型。它以「左目录、右单元格流」的布局,让分析师在一个工作簿里混合编排三类单元格:
- markdown 单元格:写说明文字(标题、结论、注释),当前版本以「纯文本 + 预览」方式展示(
.md-preview按white-space: pre-wrap原样排版,尚未做富文本渲染); - sql 单元格:写查询 SQL,点击「运行」后由后端安全翻译执行,结果以内嵌结果表格(
ResultTable组件)呈现; - chart 单元格:选择一个引用 SQL 单元格 + 图表类型(柱状图 / 折线图 / 饼图 / 散点图),运行后基于被引用 SQL 单元格的结果集在
chart-host上用 ECharts 渲染,并同步展示数据表。
单元格支持逐块运行(「运行」)与全量运行(「全量运行」),每次运行结果持久化到 fn_notebook_runs,可随时在「运行历史」中回看每次运行的单元格状态与行数、耗时。SQL 单元格复用 Ontology SQL 执行器(与 SQL 工作台同源),执行时自动注入 RLS/CLS 安全口径。
1.2 核心价值
| 维度 | 说明 |
|---|---|
| 三类块混合编排 | markdown(说明)+ SQL(取数)+ chart(可视化)在同一个工作簿内按序组织 |
| 逐块 / 全量运行 | 单格运行快速验证,全量运行一次性跑完整条分析链路 |
| 结果持久化 | 每次运行落 fn_notebook_runs(cell_results JSON),历史可回放 |
| 结果表格内嵌 | SQL 结果直接以内嵌表格展示,chart 单元格在结果上叠加 ECharts 渲染 |
| 安全取数 | SQL 单元格经 Ontology SQL 执行器(安全翻译 + RLS/CLS + 编辑态)执行 |
| chart 不重算 | chart 单元格引用 SQL 单元格结果集,只做渲染不重复执行查询 |
1.3 一句话总结
2. 访问入口
2.1 路由与菜单
- 路由:
/foundry/notebook - 路由名称:
FoundryNotebook - 路由标题:
Notebook 分析 - 菜单位置:Foundry 左侧边栏「Notebook 分析」(FoundryLayout 菜单项)
- 源码文件:
action/web/src/views/NotebookPage.vue(431 行)
2.2 认证与权限
- 需要登录:是(路由
meta.requiresAuth = true,挂 FoundryLayout) - 登录方式:AIP 统一登录,
notebookApi.js请求拦截器自动附加localStorage.aip_token为Authorization: Bearer <token> - 页面本身无角色限制;工作簿归属当前用户(后端
owner由currentUserID(c)注入) - 数据口径:SQL 单元格执行继承当前用户的 RLS/CLS 行级 / 列级安全约束,非授权行/列不会出现在结果中
- 404 排错:访问
/foundry/notebook出现 404 时,先确认 Foundry 后端(18081)已启动、前端路由已注册、Vite 代理/api指向 18081
2.3 端口与 API 前缀
- 后端端口:18081(Foundry)
- 前端 API 前缀:
/api/v1(独立notebookApi.jsaxios 实例,baseURL = '/api/v1') - 请求超时:默认 60000ms(60 秒);运行类请求(
runCell/runAll)单独放宽到 120000ms(120 秒),适合大查询
3. 界面布局
页面采用左右两栏布局(nb-layout,左侧 240px 固定,右侧自适应):
┌───────────────┬──────────────────────────────────────────────────┐ │ 左栏:工作簿 │ 右栏:当前工作簿 │ │ ① 新建输入+新建 │ ② 工具栏:名称输入 + 重命名 + 全量运行 │ │ ③ 工作簿列表 │ + Markdown / + SQL / + 图表 / 运行历史 │ │ (点击切换) │ ④ 运行历史卡片(可收起) │ │ │ ⑤ 单元格流:markdown / sql / chart 卡片 │ └───────────────┴──────────────────────────────────────────────────┘
各板块职责:
- ① 左栏新建区:
newName输入框(placeholder「新工作簿名称」,回车即新建)+「新建」按钮。 - ③ 左栏列表:每个工作簿显示名称与
updated_at(nb-sub),点击切换当前工作簿(openNotebook);「删除」按钮(link-btn)逐项删除;空列表提示「暂无工作簿,先新建一个。」;当前项高亮(li.active)。 - ② 工具栏:当前工作簿名称(
nbNameDraft可编辑)+「重命名」;「全量运行」(运行中变「运行中...」并禁用);三个添加单元格按钮(+ Markdown / + SQL / + 图表);「运行历史」开关(显示时变「收起历史」)。 - ④ 运行历史卡片:表格展示最近 N 次运行(时间 / 创建者 / 单元格数 / 状态徽标),每次运行的状态按单元格粒度以
status-badge显示 success/error。 - ⑤ 单元格流:按
ord升序纵向排列的单元格卡片,每张卡片头部是类型徽标 +#ord+ 最近运行结果摘要(状态 · N 行 · Xms)+ 操作按钮,正文按类型渲染。
未选择工作簿时右栏显示占位文案「从左侧选择或新建一个工作簿开始。」;空工作簿显示「空工作簿,点击上方按钮添加单元格。」
4. 交互元素详解
4.1 工作簿目录(左栏)
| 元素 | 位置 | 含义 | 操作效果与触发调用 |
|---|---|---|---|
| 新工作簿名称输入 | 左栏顶部 | 新工作簿名称 | @keyup.enter 触发「新建」;为空时不提交 |
| 新建 | 按钮 | 创建工作簿 | 调 createNotebook({name, description: ''});成功后提示「工作簿已创建」、清空输入、刷新列表并自动打开新工作簿 |
| 工作簿列表项 | 左栏列表 | 工作簿名称 + 更新时间 | 点击调 openNotebook(nb)(getNotebook 拉详情);当前项高亮 |
| 删除 | 每项右侧 | 删除工作簿 | confirm「确认删除工作簿「{name}」及其全部单元格/运行记录?」→ 调 deleteNotebook;若删除的是当前工作簿则置空当前,并提示「工作簿已删除」 |
4.2 工具栏
| 元素 | 含义 | 操作效果与触发调用 |
|---|---|---|
| 名称输入框 | 当前工作簿名称草稿(nbNameDraft) | 修改后点「重命名」生效 |
| 重命名 | 保存名称修改 | 调 updateNotebook(id, {name: nbNameDraft.trim()});提示「已重命名」并刷新列表 |
| 全量运行 | 顺序运行全部单元格 | 调 runAll(id)(120s 超时);运行中按钮文案变「运行中...」且禁用;完成后提示「全量运行完成」 |
| + Markdown | 新增 markdown 单元格 | 调 addCell(id, {cell_type:'markdown', content:'# 新说明'}),随后 refreshCells() |
| + SQL | 新增 sql 单元格 | 调 addCell(id, {cell_type:'sql', content:'SELECT * FROM '}),随后 refreshCells() |
| + 图表 | 新增 chart 单元格 | 调 addCell(id, {cell_type:'chart', chart_config:{cell_id: 第一个 SQL 单元格 id 或 '', chart_type:'bar'}}),随后 refreshCells() |
| 运行历史 | 运行历史面板开关 | showRuns 取反;显示时标题「运行历史(最近 N 次)」 |
4.3 单元格卡片
每张单元格卡片头部(cell-head)包含:类型徽标(ct-badge,markdown 紫 / sql 蓝 / chart 橙)、#ord 序号、最近运行结果摘要(status · row_count 行 · elapsed_ms ms,有结果才显示),右侧三个按钮:
| 按钮 | 含义 | 操作效果与调用 |
|---|---|---|
| 保存 | 保存单元格编辑内容 | 调 updateCell(id, cellId, {cell_type, content, chart_config?});提示「单元格已保存」 |
| 运行 | 运行该单元格 | 先自动 saveCell(cell) 保存再调 runCell(id, cellId)(120s);chart 单元格运行后 nextTick 渲染 ECharts;随后刷新运行历史 |
| 删除 | 删除该单元格 | confirm「确认删除该单元格?」→ 调 deleteCell(id, cellId),清空其本地结果并 refreshCells() |
4.4 三类单元格正文
| 类型 | 正文区 | 说明 |
|---|---|---|
| markdown | <textarea rows="4"> + .md-preview 预览 | 占位符「# 标题\n支持 markdown 文本」;预览区按 pre-wrap 原样展示文本 |
| sql | <textarea rows="5"> + 结果区 | 占位符「SELECT name, amount FROM order WHERE amount > 100 LIMIT 10」;有结果时显示 ResultTable(列排序 / 分页 / 行号),若有 error 以红字提示 |
| chart | 配置下拉 + chart-host + ResultTable | 见下 |
chart 单元格配置区(chart-cfg):
- 「选择引用的 SQL 单元格」下拉:
v-model="cell.chartConfigCellId",选项为#ord + SQL 前 30 字符(来源sqlCells计算属性,仅当前工作簿中cell_type === 'sql'的单元格)。 - 图表类型下拉:
bar(柱状图)/line(折线图)/pie(饼图)/scatter(散点图)。 - 运行后若有结果列,在
.chart-host[data-cell="id"](高 260px)用 ECharts 渲染,tooltip轴触发、x 轴取第 0 列、y 轴取第 1 列;饼图半径 60%;同时下方展示数据表。
4.5 运行历史表格
| 列 | 含义 |
|---|---|
| 时间 | run.created_at(fmtTime:T→空格、截 19 位) |
| 创建者 | run.created_by |
| 单元格 | Object.keys(run.cell_results || {}).length(本次运行涉及的单元格数) |
| 状态 | 每个单元格一个 status-badge:success 用 status-active 绿徽标,其余(error)用 status-error 红徽标 |
5. 后端关联
5.1 API 客户端
- 文件:
action/web/src/api/notebookApi.js baseURL: '/api/v1',默认timeout: 60000,请求头Content-Type: application/json- 请求拦截器:附加
aip_tokenBearer;响应拦截器:401 清 token 并跳登录 - 导出函数:
listNotebooks/createNotebook/getNotebook/updateNotebook/deleteNotebook、listCells/addCell/updateCell/deleteCell、runCell(120s)/runAll(120s)/listRuns
5.2 端点表(前缀 /api/v1,均挂 protected)
| 方法 | 路径 | 请求体 | 超时 | 用途 |
|---|---|---|---|---|
| GET | /notebooks | — | 60s | 工作簿列表 |
| POST | /notebooks | {name, description} | 60s | 创建工作簿 |
| GET | /notebooks/:id | — | 60s | 工作簿详情(含 cells) |
| PUT | /notebooks/:id | {name?, description?}(指针语义) | 60s | 更新工作簿 |
| DELETE | /notebooks/:id | — | 60s | 删除工作簿(级联 cells/runs) |
| GET | /notebooks/:id/cells | — | 60s | 单元格列表(ord 升序) |
| POST | /notebooks/:id/cells | {cell_type, content, chart_config?} | 60s | 添加单元格 |
| PUT | /notebooks/:id/cells/:cid | {cell_type?, content?, chart_config?} | 60s | 更新单元格 |
| DELETE | /notebooks/:id/cells/:cid | — | 60s | 删除单元格 |
| POST | /notebooks/:id/cells/:cid/run | {} | 120s | 运行单格 |
| POST | /notebooks/:id/run | {} | 120s | 全量运行(落 fn_notebook_runs) |
| GET | /notebooks/:id/runs?limit= | 查询参数 limit | 60s | 运行历史 |
5.3 响应结构(JSON 示例)
工作簿详情 GET /notebooks/:id:
{ "code": 0, "data": {
"notebook": { "id": "...", "rid": "notebook://foundry/...", "name": "销售分析",
"description": "", "owner": "550e8400-...",
"created_at": "...", "updated_at": "..." },
"cells": [
{ "id": "...", "notebook_id": "...", "ord": 1, "cell_type": "markdown",
"content": "# 标题", "created_at": "...", "updated_at": "..." },
{ "id": "...", "ord": 2, "cell_type": "sql",
"content": "SELECT name, amount FROM order WHERE amount > 100 LIMIT 10" },
{ "id": "...", "ord": 3, "cell_type": "chart",
"chart_config": "{ \"cell_id\": \"...\", \"chart_type\": \"bar\" }" }
]
} }
单格运行 POST /notebooks/:id/cells/:cid/run:
{ "code": 0, "data": {
"status": "success", "elapsed_ms": 42, "row_count": 10,
"columns": ["name", "amount"], "rows": [["张三", 1200], ["李四", 980]]
} }
全量运行 POST /notebooks/:id/run:
{ "code": 0, "data": {
"id": "run-uuid", "notebook_id": "...", "created_by": "550e8400-...",
"cell_results": {
"cell-id-2": { "status": "success", "elapsed_ms": 40, "row_count": 10,
"columns": [...], "rows": [...] },
"cell-id-3": { "status": "success", "elapsed_ms": 1, "row_count": 10,
"columns": [...], "rows": [...], "chart_type": "bar", "source_cell": "cell-id-2" }
}
} }
5.4 关联模块
| 后端包 / 表 | 职责 |
|---|---|
foundry/notebook/models.go | Notebook(fn_notebooks)、Cell(fn_cells)、NotebookRun(fn_notebook_runs)、ChartConfig、CellResult |
foundry/notebook/rest.go | REST handler:notebooks / cells CRUD + run / runAll / runs |
foundry/notebook/service.go | 业务服务:RunCell / RunAll / ListRuns 等 |
| Ontology SQL 执行器 | SQL 单元格经 query.OntologySQLService.Execute(注入的 SQLExecutor 接口)执行,复用安全翻译 + RLS/CLS + 编辑态 |
foundry/server/server.go | notebook.RegisterRoutes(protected, s.notebookSvc) 挂载(notebookSvc 经 NewOntologySQLAdapter 接 SQL 执行器) |
5.5 关键机制
- 单元格排序:
fn_cells.ord决定执行顺序,ListCells按 ord 升序返回。 - chart 单元格不重算:chart 单元格依赖其引用的 SQL 单元格结果集「渲染配置(不重算)」——RunAll 内以同批顺序执行的内存结果传递;独立 RunCell 回退到最近一次 run 的缓存结果,均不重新执行被引用 SQL 单元格。
- 运行记录:RunAll 顺序执行全部单元格并落
fn_notebook_runs,cell_results为{cell_id: CellResult}JSON;CellResult含status(success|error)、elapsed_ms、row_count,chart 单元格附chart_type/source_cell。 - 单格运行:
POST .../cells/:cid/run由RunCell执行,chart 单元格回退缓存,返回单格CellResult。 - 错误容错:
Results()/ParseSnapshot()对非法 JSON 返回空结构不报错(历史数据容错)。
6. 核心流程详解
6.1 新建工作簿主流程
- 左栏输入新名称(或直接回车)→ 点「新建」。
- 调
createNotebook({name, description: ''})→ 提示「工作簿已创建」→fetchNotebooks()刷新列表 →openNotebook(data.data)自动打开。 - 打开后
getNotebook拉详情,将每个单元格的chart_configJSON 解析为chartConfigCellId/chartType(前端编辑态字段),并loadRuns()加载运行历史。
6.2 添加与编辑单元格流程
- 点「+ Markdown / + SQL / + 图表」→
handleAddCell(type):- 构造默认 content(markdown
# 新说明、sqlSELECT * FROM、chart 空); - chart 单元格默认
chart_config = { cell_id: sqlCells[0]?.id || '', chart_type: 'bar' }; - 调
addCell后将新单元格 push 进本地current.cells,再调refreshCells()与后端对齐。
- 构造默认 content(markdown
- 编辑单元格内容(textarea 双向绑定
cell.content),点「保存」调updateCell。 - 点「运行」单格:先
saveCell(cell)保存当前编辑,再runCell,结果写入cellResults[cell.id],chart 单元格nextTick后renderChart。
6.3 全量运行流程
- 点「全量运行」(
handleRunAll)→running=true,按钮变「运行中...」。 - 调
runAll(id):后端按 ord 顺序执行全部单元格(SQL 取数、chart 渲染配置传递),落fn_notebook_runs。 - 前端将
run.cell_results整体写入cellResults,随后refreshCells()对齐单元格列表,nextTick后为每个 chart 单元格执行renderChart,最后loadRuns()刷新历史并提示「全量运行完成」。
6.4 运行历史回看
- 点工具栏「运行历史」展开面板(默认收起)。
loadRuns()调listRuns(id, limit=0)拉取全部运行记录;每行展示时间 / 创建者 / 单元格数 / 各单元格状态徽标。- 单元格数取
Object.keys(run.cell_results || {}).length;状态按每个 cell 的status分别打徽标。
6.5 删除分支流程
- 删除单元格:confirm →
deleteCell→ 清cellResults[cell.id]→refreshCells()。 - 删除工作簿:confirm →
deleteNotebook→ 若为当前工作簿则current=null→ 刷新列表。
7. 权限与安全
- 认证:全部端点要求
aip_token(protected 组);401 时notebookApi.js响应拦截器清理 token 并跳登录。 - 归属隔离:工作簿
owner由服务端currentUserID(c)注入,ListNotebooks/UpdateNotebook/DeleteNotebook均按当前用户过滤。 - 数据级安全:SQL 单元格执行走 Ontology SQL 执行器,自动注入 RLS(行级)/ CLS(列级),未授权行/列不会进入结果集;前端无法绕过。
- 写操作防护:删除工作簿 / 删除单元格均有
window.confirm二次确认;运行中running置位禁用「全量运行」与「运行」按钮防重复触发。
8. 常见问题与排错
8.1 添加 / 删除 / 全量运行时报「listCells is not defined」(已知缺陷)
- 现象:点「+ SQL」「+ 图表」「删除」或「全量运行」后,提示「添加单元格失败:listCells is not defined」「删除失败:listCells is not defined」或「全量运行失败:listCells is not defined」。
- 原因:
NotebookPage.vue的refreshCells()(约 278-289 行)调用了listCells,但 script 顶部 import(约 155-158 行)未导入listCells——handleAddCell/handleDeleteCell/handleRunAll触发时抛ReferenceError。listCells实际已在notebookApi.js中导出(GET /notebooks/:id/cells),仅组件 import 遗漏。 - 排查与规避:① 查看浏览器 Console 确认
listCells is not defined;② 规避方式:添加/删除单元格后「刷新页面」重新打开工作簿,单元格实际已写入后端(添加成功、删除成功),仅前端列表未刷新;全量运行时cell_results已写入内存,但图表渲染与成功提示被跳过;③ 彻底修复需在源码 import 语句补listCells(本手册不改源码,待前后端修复)。
8.2 新建工作簿后右侧仍是空
- 现象:左栏列表有工作簿,但右侧显示「从左侧选择或新建一个工作簿开始。」
- 原因与排查:
openNotebook(data.data)依赖创建接口返回的data.data对象;若返回结构不符(如data为空),可能未正确打开。可手动点击左栏列表项切换;仍失败则检查 Network 中GET /notebooks/:id响应结构是否含notebook与cells。
8.3 SQL 单元格运行后显示红字错误
- 现象:单元格结果区出现
error红字(如 SQL 语法错误、表不存在、无权限)。 - 原因:SQL 经 Ontology SQL 执行器翻译执行失败;或 RLS/CLS 过滤后无可见数据。
- 排查:① 检查 SQL 是否使用对象名而非物理表名;② 对照 SQL 工作台的翻译预览确认对象/属性名;③ 确认当前用户对该对象具备查询权限;④ 查看后端日志中执行器返回的具体错误。
8.4 图表单元格不渲染
- 现象:chart 单元格运行后没有出现 ECharts 图表。
- 排查:① 确认已选择「引用的 SQL 单元格」且该 SQL 运行出数(
cellResultOf(cell).columns非空才会渲染);② 确认图表类型为 bar/line/pie/scatter 之一;③ 若触发了 8.1 的缺陷(全量运行),先手动单格「运行」chart 单元格;④ 打开 Console 看是否有 ECharts 初始化报错(如 DOM 未挂载)。
8.5 运行历史不刷新
- 现象:运行后「运行历史」仍是旧数据。
- 排查:①
loadRuns()为静默失败(catch 无提示),检查 NetworkGET /notebooks/:id/runs是否 4xx/5xx;② 确认current.value已设置(未打开工作簿时不加载);③ 单格运行与全量运行都会触发loadRuns(),若中途抛错(如 8.1)可能未执行到刷新逻辑,手动点「运行历史」收起再展开可触发一次loadRuns。
9. 已知缺陷与边界
| 项目 | 说明 |
|---|---|
| chart 单元格不重算 SQL | chart 仅引用 SQL 单元格结果集渲染;被引用 SQL 改后须先「运行」该 SQL 单元格再运行 chart |
| 单格运行回退缓存 | 独立运行 chart 单元格时回退最近一次 run 的缓存结果,可能与当前 SQL 单元格最新结果不一致 |
| markdown 预览非富文本 | .md-preview 为 pre-wrap 纯文本展示,markdown 语法不会渲染成标题/列表 |
| 运行历史仅展示摘要 | 历史表只展示状态徽标与单元格数,不含每格的详细数据(详细结果在当期运行后内存中) |
| 无工作簿级共享/协作 | 工作簿归属个人 owner,无团队共享、评论、版本对比能力 |
| 运行超时边界 | runCell / runAll 前端 120s 超时,超大查询可能超时中断 |
注:listCells 未导入缺陷已于 2026-09-06 修复(源码 import 已补 listCells),添加/删除/全量运行不再抛 ReferenceError。