# 普通用户建应用:方案 > 设计文档 · 草案,待拍板 · 2026-10-15 > > - 本文档只定方案,不含代码改动。第五部分「实施组织」在本文签字后另行审批,再开工。 > - 代码位置按 **main@69c5c74**(应用平台合入后)标注,三路只读调研逐条核过。 > - 承接《应用平台与助手按需查看:总方案》(`docs/design/app-platform-and-look.md`,下称「总方案」)。本文不改它,引用写作「总方案 §x.y」。 > - 文中出现的数据源名、表名、应用名、人名都是虚构的。本文不含本机开发环境的计数。 ## 目录 - 摘要 - 第一部分 现状与问题 - 第二部分 关键决定(N1–N11) - 第三部分 规格 - 第四部分 界面 - 第五部分 实施组织 - 第六部分 验证与验收 - 第七部分 风险与对策 - 第八部分 待拍板 - 附录 A 代码位置索引 · B 术语 --- ## 摘要 **问题。** 应用平台已经能做相当复杂的应用:多级审批、会签、时限与升级、写回外部系统、看板、按新口径补算。但能做出来的只有懂编排的人。普通人卡在这几处: - 必须先在画布上搭好并发布工作流,应用只能从已发布版本生成; - 从问数据出发要跳四次:做成应用 → 提取工作流 → 去画布发布 → 回来做成应用; - 满屏专家词:固定版本、处置、岗位绑定、视图来源、出具契约、口径卡、`cell(nodes.fetch, 0, 'gmv')`、`PT48H`; - 发布前看不到真数,判断不了「这是不是我要的」; - 两类最常见的需求做不了:上传文件,以及「填报登记」(每次提交存一行,再汇总着看)。 **做法。** 把「搭应用」改成「说需求 → 看蓝图 → 用真数试 → 确认」,用的就是项目的路线「模板定骨架、助手填关节」: 1. **一条链路,两种终点**(N1)。说需求、蓝图、模板填关节、试运行,两类人走同一条链路,只在最后一步分开: - 搭建者:一次确认,发布成正式应用; - 使用者:存成**个人应用**,只自己看得见、只读、跑探索运行,想给别人用就提交给搭建者审核发布。 2. **应用蓝图**(N2):用户的话和工作流、应用之间的中间件,用白话卡片呈现。卡片写的是看哪些数、谁发起谁审批、多久更新一次、要不要写回系统,用户逐张确认或修改。卡片的改动是确定性的操作,不是让模型重写。 3. **场景模板**(N3):首批五个——周期看板、审批流程、异常处置、填报登记、核对写回。骨架锁住,助手只能填模板声明的「关节」:取数的 SQL、口径卡的指标式;其余关节由蓝图确定性地填。 4. **用真数试运行**(N4):跑一次探索运行,草稿应用直接对着这次运行解析,印章写「试运行」。流程类的审批步骤派给试运行的人自己点,写入一律走测试环境或演练。 5. **一次确认发布**(N5):服务端按「发布单」编排——保存版本、发布工作流、建应用、绑岗位、建定时任务、授权、跑第一次——每步可续做。确认前给一页白话摘要;查出的问题分成「你来定」「助手可以修」「要找管理员」三类。 6. **在页面上直接改**(N6):应用页进入编辑状态后点哪改哪;改取数逻辑就回到蓝图,重走试运行和发布单,口径变不变用白话说清。 7. **补两个能力缺口**: - 记录表(N8):平台托管、只许追加,用序号钉版本,按授权分行可见; - 文件(N9):按哈希存,以引用进运行输入,可以交给 agent 读,也可以作为一期数据导入。 8. **白话层**(N10)和**生成的耗时与稳定性**(N11):所有检查问题有一份白话说法;补上问数据和画布两端「助手反问」的收尾。 **已拍板**(2026-10-14、10-15): - 两类人都做,共用一条链路(N1); - 填报登记要做行级可见(N8); - 表单只许加字段,改字段或删字段就出新的表版本,旧记录照原样保留(N8); - 试运行只给有管理权限的人看(N4); - 先写设计文档,签字后实施;验收结束后实施(已结束)。 **待拍板**:见第八部分,共 8 项,每项附推荐。 --- ## 第一部分 现状与问题 ### 1.1 现在建一个应用要走的路 一个从零开始的看板应用,最少要按顺序做这些事,每步各自提交: 1. 在画布上搭工作流并保存,形成版本快照; 2. 发布工作流(受管级只有管理员能发); 3. 建定时任务——必须在第 4 步之前,否则派生出来的视图不绑定时任务(`apps/derive.py:85-90`); 4. 派生应用草稿(`POST /api/apps/derive`,不落库); 5. 建应用(`POST /api/apps`); 6. 绑岗位(`PATCH draft_bindings`); 7. 授权(`PUT grants`,立即生效); 8. 发布应用。 中途失败会留下半截状态:已发布但没有应用的工作流、按 cron 在跑的定时任务、停在草稿的应用。草稿应用的授权已经生效,使用者打开会得到 409「还没有发布」(`api/apps.py:83`)。这些步骤分散在画布、定时任务、应用编辑器三处。 ### 1.2 普通人卡在哪 | 卡点 | 现象 | 出处 | |---|---|---| | 要会搭图 | 应用只能钉已发布版本 | 总方案 A1 | | 术语 | 口径卡表达式、ISO 8601 时长、出具契约 JSON、视图来源、处置 | 端到端验收「普通用户视角」13 条 | | 看不到真数 | 草稿预览只读钉住版本已有的封存运行,新应用没有 | `apps/resolve.py:454-476` | | 路径长 | 问数据做成应用要跳四次 | `ChatPage.tsx:484-492` | | 做不了 | 没有文件控件;没有平台托管的可写表 | `apps/spec.py:35-38`;`data/table_versions.py:1-33` | ### 1.3 现有零件(直接复用) - **生成管线**:`Generation` 加 `Applier`/`Checker` 协议,新的生成目标插进来就能用(`api/copilot_gen.py:567`,`look/base.py:87-110`)。 - **按需查看**:表、能力、工作流版本三类对象(`look/service.py`)。 - **护栏**: - 润色边界 `polish_problem`(`apps/ops.py:264`); - 自动修复的 `forbidden_changes`(`engine/autofix.py:1240`)。 两者都是「只许改哪些」的确定性护栏,模板关节照这个写法。 - **确定性派生**:`derive_app`(`apps/derive.py`)。 - **从运行提取工作流**:`/copilot/from-run`,带来源记录 `workflows.origin`(`api/copilot.py:2468-2550`)。 - **数据目录**:表的中文名、粒度、业务日期、列的含义和度量类型(`data/catalog.py:12-21`),是把白话对到表的依据。 - **按运行解析**:视图来源 `instance` 已经能按给定的那次运行解析,不查类别和哈希(`apps/resolve.py:541-546`)。 - **所有运行都封存**,探索运行也一样(`engine/runner.py:1013-1045`);探索运行的写入走测试环境或演练(`capabilities/endpoints.py:101`)。 - **问数据对使用者的约束**(`auth/chat_policy.py`): - 节点和工具的白名单; - 强加 `MASK_RESULTS`、`READ_ONLY`(`api/runs.py:108-123`); - 记忆隔离。 - **文件相关**: - 上传限额 `read_upload`(`api/datasources.py:905-918`); - 按哈希存原件 `raw_store`(`data/raw_store.py:33-43`); - 按配方导入「上传新一期」(`api/table_imports.py:160-486`)。 - **表版本链**:快照的版本链,以及「改列不兼容」的判定 `INCOMPATIBLE_KINDS`(`data/recipe_accumulate.py:38-41`)。 - **数据版本**:data_versions 进封存范围和印章(`engine/runner.py:242-262`,`apps/resolve.py:238-240`)。 - **名录**:`auth/directory.py`,按登录方式核对人和组。 - **白话分类的依据**: - 检查问题已经有 `code`; - 自动修复有 `kind: auto|choice|assist`(`engine/autofix.py:125-141`); - 只能由人处理的有 `APP_HUMAN_ONLY`(`api/copilot_gen.py:416-421`)。 ### 1.4 核实过的缺口 | # | 缺口 | 出处 | |---|---|---| | G1 | 没有带参数的模板,也没有「模板里哪些地方能改」的机制;GraphSpec 只有 nodes/edges/viewport/defaults;「以此模板新建」只是复制 | `engine/schema.py:115`;`WorkflowPicker.tsx:354`;`api/workflows.py:249` | | G2 | 应用视图只认正式运行;印章没有「探索」「试运行」 | `apps/resolve.py:95,149,203-221` | | G3 | 探索运行没有版本号和哈希,三种视图来源都取不到它 | `engine/launch.py:166-174` | | G4 | 跨步没有事务:工作流发布撤不回(只能退役),应用只能归档、不能删 | `api/apps.py:273` | | G5 | 检查问题只有技术说法,没有白话 | `engine/governance.py:217-271` | | G6 | 只问不改的一轮:问数据的 store 没有 ask 分支,可能一直停在「进行中」、问题不进历史;画布也不记这一轮 | `store/chat.ts:1433`;`store/studio.ts:2149` | | G7 | 使用者不能建应用;应用视图、印章、读权限都只认正式运行 | `api/apps.py:191`;`auth/access.py:74-96` | | G8 | 工件库只存 JSON;运行输入不能引用文件;入口提交是 JSON | `core/artifact_store.py:45-115`;`engine/nodes/io.py:14-33` | | G9 | 平台托管的表都是不可变快照,只能整期替换或按期累积;没有逐行追加,也没有行级可见 | `data/table_versions.py:1-33`;`engine/masking.py` | | G10 | 没有按名字检索的口径库;复用口径只能逐级下拉选 caliber_from | `engine/schema.py:1460`;`Inspector.tsx:1065` | | G11 | 组件没有「隐藏」字段,只能删掉或写可见性表达式;改动要先保存才看得到 | `apps/spec.py:97-107`;`apps/resolve.py:632` | | G12 | 生成没有并行、没有提示缓存;问数据校验不过时前端整轮重来 | `look/service.py:192`;`store/chat.ts:1241` | ### 1.5 理念对照 本方案不加新理念,把总方案的八条用到「创建」上: - **P1 照实说**:试运行、探索、个人应用的数都在印章上写明来历;检查问题用白话说清是什么、为什么、谁来改。 - **P2 留痕**: - 蓝图每一次改动都留痕; - 发布单每一步的结果都留痕; - 生成的工作流记下出自哪个蓝图、哪个模板。 - **P3 不挡生成**:模型失败时,模板照样能按蓝图确定性地实例化,缺的关节标「待填」交给人。 - **P4 只读与遮罩**: - 个人应用只读、结果遮罩; - 记录表按授权分行可见; - 文件只给读得到那次运行或那一行的人。 - **P5 一种说法**:白话层只有一份,按问题编号映射。 - **P6 模板定骨架**:模板的节点和连线锁死,助手只填声明过的关节。 - **P7 界面层不放逻辑**:卡片的修改是对蓝图的确定性操作;页面上直接改走的仍是应用操作。 - **P8 生效可查可对账**:记录表的写入走动作(生效记录、幂等键);文件的哈希进封存。 不可回退决定的对照见 §2.12。 --- ## 第二部分 关键决定 每个决定写四样:决定、理由、否决的做法、代价。 ### 2.1 N1 一条链路,两种终点 **决定** 链路是:说需求 → 蓝图 → 选模板并实例化 → 试运行 → 终点。 - **起点三个**,都落到同一个蓝图: 1. **说说你要什么**:开一段蓝图对话,助手先问清楚,再整理成卡片; 2. **从场景模板开始**:选一个模板,卡片按模板预置; 3. **从已有的开始**:从问数据的一轮回答(运行)开始,或者从一个已发布的工作流版本开始。前者用 from-run 提取,后者直接导入图,模板记为「自由形」。 - **终点两种**: - **搭建者**:发布单(N5)一次确认,产出已发布的工作流、正式应用、定时任务、授权; - **使用者**:存成个人应用(N7),自己用。想给别人用就「提交审核」,搭建者打开同一个蓝图,走试运行和发布单。 **理由**:一条链路只维护一份,「问 → 建」的沉淀路径是现成的(P2)。使用者沉淀出来的东西不绕过发布门禁:个人应用永远是探索,正式应用只能由搭建者发布(不可回退决定:正式运行只从已发布版本发起)。 **否决的做法** - 给使用者一个简化版画布:等于让使用者搭图,权限和门禁都要另开口子; - 使用者的应用也走正式发布:让 user 角色产出承诺,违背角色分层。 **代价**:个人应用和正式应用是两种对象状态,界面要说清区别。 ### 2.2 N2 应用蓝图 `agentlab.blueprint/1` **决定** - **定位**:蓝图是用户的话和工作流、应用之间的结构化中间件,存成一个可变对象 `Blueprint`。它有: - 拥有者、可见范围(个人 / 共享); - 状态:起草中 / 试运行过 / 已发布 / 个人应用 / 审核中; - 来源:对话、模板、运行、工作流版本。 - **一张蓝图由几张「卡片」组成**,每张卡片对应用户能理解的一件事: | 卡片 | 内容 | 落到哪里 | |---|---|---| | 目的 | 一句话:这个应用解决什么问题、给谁用 | 应用名称、说明 | | 场景 | 选定的模板(N3) | 骨架 | | 看哪些数 | 每一项:白话名、对到哪个库哪张表(或复用哪张已发布的口径卡)、粒度、业务日期、筛选条件、单位 | SQL 关节、口径卡关节 | | 多久更新 | 每天、每周一、每月 1 日……时区;统计哪一期(本周、上周、上月) | 定时任务、入口字段的模板值 | | 谁来做 | 发起人、各步处理人(人、组、岗位)、几个人同意算通过、时限、超时给谁 | 人工节点配置、岗位绑定 | | 填什么 | 表单字段:名称、类型、必填、上下限、选项、文件 | 入口表单、待办表单、记录表的列 | | 写回哪里 | 目标能力(从能力登记里选)、字段对应、要不要人确认;能力没登记查询的,再定结果不明时谁去核实(只由人定,同「谁能看」) | 调用工具节点、应用的核实岗位 | | 谁能看 | 使用、查看全部、管理三档的人和组 | 授权 | - **每一项都带状态**,照数据目录项的写法 `{value, source, status}`: - `source`:用户填的 / 助手提的 / 模板默认 / 从目录或口径卡对到的; - `status`:已确认 / 待确认 / 缺。 试运行前,所有「缺」都要补齐;「待确认」可以先试运行,但发布时必须确认。 - **助手只提议**:输出 `bp_*` 操作(`bp_set`、`bp_add`、`bp_remove`、`bp_ask`),由 `BlueprintApplier` 确定性落地。用户在卡片上改,走的也是同一组操作,只是 `source` 记成用户。 - **对到数据靠 look**:助手看「看哪些数」时,先查数据目录(表的中文名、粒度、业务日期、列的度量类型),再查已发布工作流里的口径卡(N2 新增 look 类别 `caliber`,补 G10)。对到已有口径卡时优先复用(caliber_from),不重写指标式。 - **反问**:缺的信息由助手用 `bp_ask` 问,一次问一件事、给出候选(例如「销售额按下单日还是支付日算?」并附两个选项)。这一轮就此结束,回答是下一轮(G6 一并修好,N11)。 **理由** - 卡片是用户能判断对错的颗粒度(P1); - 模型产出的是提议,落地是确定性的(P7); - 每一项都有来源和状态,不会把模型猜的当成用户说的(P2)。 **否决的做法** - 让模型直接生成工作流再给用户看图:普通人看不懂图; - 一问一答的长表单向导:问题顺序固定,答不上来就卡住; - 蓝图只存在前端:刷新就丢,也没法交给搭建者审核。 **代价**:多了一种对象和一套操作;模板变了,蓝图的卡片要跟着兼容(蓝图记下模板版本)。 ### 2.3 N3 场景模板 `agentlab.template/1`:骨架锁住,关节可填 **决定** - **模板是一份登记**,和组件目录同一种做法(总方案 A2): - 后端 `app/apps/templates/`,每个模板一个模块; - 开发者扩展时,模板定义、实例化函数、测试三样都要给,缺一样启动即报错。 - **一个模板包含**: - **骨架**:带占位的图(节点和连线固定); - **关节**:骨架里允许填的位置,每个都有类型、来源、校验; - **应用骨架**:页面布局,用派生规则加模板自带的组件排布; - **卡片映射**:蓝图的哪张卡片填哪些关节; - **说明和适用条件**:给用户和助手看。 - **关节分两种**: - **确定性关节**:由蓝图直接填,不经模型。包括: - 表单字段 → 入口节点 fields; - 处理人和策略 → 人工节点 assignee/policy/due/on_timeout; - 金额线这类规则 → 分支节点的比较条件,只许「字段 比较 常量」; - 更新频率 → 定时任务; - 写回目标 → 调用工具节点的 tool 和参数映射。 - **生成关节**:只有两类交给模型—— - 取数的 SQL(`sql`); - 口径卡的指标式(`metric_expr`,只许 `cell()` 取格加口径卡的算术)。 模型在 `TemplateApplier` 下工作,只收 `fill_joint` 操作,而且只收这个模板声明过的关节 id,别的一律拒绝并照实交回(照 `polish_problem` 的写法)。 - **护栏**:实例化后的图和骨架逐节点比对,只许关节位置不同,用 `forbidden_changes` 的同一套比对。之后照常走 validate、GraphChecker(SQL 猜列、数据源范围)和发布前检查。 - **来源记录**:生成的工作流写 `workflows.origin = {blueprint_id, template: "@", run_id?}`,补上「只写不读」的现状:WorkflowOut 和版本历史显示「出自蓝图 X、模板 Y」。 - **首批五个**(详见 §3.3):周期看板、审批流程、异常处置、填报登记、核对写回。外加「自由形」:从已有的运行或工作流导入,没有关节,改图只能回画布。 - **模型失败时**(P3):生成关节留空、标「待填」,蓝图卡片上提示「这一项助手没填上,请补充或换个说法」;确定性关节照样落地。 **理由** - 模板定骨架、助手填关节,正是项目路线; - 关节越少,生成越快、越稳(N11),检查也越有效; - 锁骨架就锁住了治理关注的结构:出具契约、人工节点、写入位置。 **否决的做法** - 模板只是一张样例图,复制了让助手随便改:等于自由生成,护栏全靠事后检查; - 用模板引擎在 JSON 上做字符串替换:类型和校验都丢了; - 一个模板覆盖所有场景、用大量开关:组合爆炸,测不过来。 **代价**:每个模板要写实例化和测试;用户的需求落在模板之外时,只能走「自由形」,或者由搭建者新增模板。 ### 2.4 N4 用真数试运行 **决定** - **试运行**是一次探索运行,跑的是蓝图实例化出来的、还没发布的图,来源记为 `origin_kind=trial`、`origin={blueprint_id}`。 - **草稿应用对着这次运行解析**: - 新增解析模式 `resolve_page(draft=True, trial=run)`; - 草稿里所有视图来源(最近封存、按需、本实例)都指向这次运行; - 钉住项记成 `{workflow_id, version: null, graph_hash: <这次运行的图哈希>}`。 沿用 `instance` 分支「按给定运行解析」的路径(`apps/resolve.py:541-546`)。不改 `agentlab.app/1` 的视图来源种类,试运行只是解析时的旁路参数。 - **印章**: - 来源写「试运行」; - 注明「数据是真的;这不是正式结果,发布后才会按定时任务出数」; - 出具档位照实算;没封存的照实写。 趋势只有一个点时写「试运行只有一次,没有趋势」,不报错(补 `_series` 拿不到钉住项就报错的问题,`apps/resolve.py:813`)。 - **输入**: - 看板类:按「多久更新」卡片算出最近一期(例如上一 ISO 周),用户可以改; - 流程类:由用户填一份示例表单。 - **流程类的试运行**: - 人工步骤派给试运行的人自己(来源记为 `trial`),按钮写「以试运行身份同意/驳回」; - 写入一律走测试环境;没有测试环境的演练,不发送; - 时限按真实设置显示,但试运行不触发升级——调度跳过 `origin_kind=trial` 的待办。 - **谁能看**:只有对这个蓝图有管理权的人(拍板项):搭建者看自己的、有 manage 的看共享蓝图的;个人应用的试运行只有拥有者看。试运行是探索运行,遮罩按看的人的角色照常处理。 - **发布时对齐**:发布单按图哈希判断「发布的版本就是试运行过的那张图」: - 一致:钉住项直接换成版本,节点 id 一一对上; - 不一致(试运行之后又改过):摘要里写明「发布的内容和最近一次试运行不同」,建议再试一次,不强拦。 **理由**:先看真数再决定(P1);不发布就不产生正式运行(不可回退决定);写入不出测试环境(总方案 A5)。 **否决的做法** - 先发布一个隐藏版本再试:版本表里满是试验版本,退役和口径升版的判断都被污染; - 用假数据预览:普通人恰恰要看真数才判断得了。 **代价**:试运行要花一次模型或查询的钱;流程类的试运行要人点一遍。 ### 2.5 N5 一次确认发布:发布单 **决定** - **发布单**(`PublishPlan`)是服务端的编排对象:先**算出**要做的每一步和白话摘要,用户确认后**按序执行**,每步的结果记下来;失败了停在那一步,修好后从那一步**接着做**。 - **步骤**(顺序就是为了让中途失败时留下的状态无害): 1. **保存版本**:按图哈希判断,已有相同的版本就复用; 2. **全量预检**(只读):工作流发布前检查 + 按将要生成的版本跑应用检查 + 定时任务预览 + 名录核对。任何 error 都在这一步停下,什么都没写; 3. **发布工作流**(受管级只有管理员); 4. **建定时任务**(停用状态),这样派生时视图能绑上它; 5. **建应用、派生、写岗位绑定**; 6. **发布应用**:钉住项升版时按蓝图里的处置;第一次发布没有处置; 7. **启用定时任务**; 8. **授权**:最后一步,避免使用者先看到一个打不开的应用; 9. **跑第一次**(看板类默认勾选):立即运行一次,应用一打开就有数。 - **白话摘要**:每一步一句「会发生什么」,例如: - 「发布『门店周报』:此后每周一 09:00 按这一版自动出数」; - 「张三、李四可以使用;财务组的人负责审批」; - 「口径变了:以前的周报按新口径重算(recompute),旧数据留底」。 - **问题分三类**,按问题编号走白话层(N10): - **你来定**:岗位没绑人、处置没选; - **助手可以修**:autofix 的 auto/assist、应用检查里不在 `APP_HUMAN_ONLY` 的; - **要找管理员**:受管级、降级、内网端点、能力治理。 - **补偿**: - 第 3 步之后的失败不撤回已发布的工作流(发布本来就撤不回),摘要照实说「工作流已发布,应用还没有」; - 停用的定时任务在应用发布前不会跑,不产生无主的正式运行; - 应用停在草稿时不授权,使用者看不到。 - **权限**:执行发布单的人要有其中每一步的权限,预检时就按角色算好(例如搭建者发不了受管级,摘要里直接说)。 **理由**:一次确认、照实摘要(P1);每一步留痕(P2);中途失败的状态无害、可续做。 **否决的做法** - 一个大事务把八步包起来:跨定时任务、应用、工作流的事务在 SQLite 单进程下做得到,但出错后整体回滚会吞掉已经通过的检查结果,用户得从头来;而且发布动作本身就带审计,不应该回滚; - 前端按顺序调现有接口:刷新页面就丢进度,权限和补偿都在客户端,不可靠。 **代价**:多一个对象和一张表;现有各接口的校验要能被发布单按「只检查不提交」的方式调用。 ### 2.6 N6 在页面上直接改 **决定** - **改呈现**: - 有 manage 的人在应用页点「编辑」进入编辑状态,ViewRenderer 叠一层编辑层:按 `data-node` 选中组件(`apps/ViewRenderer.tsx:75-84`),就地改文字、隐藏、上下挪、删除。 - 落地走现有的应用操作 `app_update`、`app_move`、`app_remove`,自动存草稿,然后按草稿重新解析。 - **加「隐藏」**:组件加 `hidden: true` 字段(补 G11),解析时跳过、编辑状态下照常画成半透明。这和可见性表达式不是一回事:可见性是按运行时条件显示,隐藏是作者不要它。 - **加一张数据卡片**:候选从钉住版本里列,用同一份派生规则 `_results`(口径卡指标、报告、取数节点、趋势),新接口 `GET /api/apps/{id}/candidates`。用户挑一个,就落成一个 `app_add`。 - **改取数逻辑**(「销售额要去掉退款」): - 不在页面上改,回到蓝图:改「看哪些数」卡片 → 重新实例化 → 试运行 → 发布单; - 发布单在摘要里用白话说清口径变了、以前的数怎么处理(处置),沿用总方案的口径升版规则。 - **改完要发布**:呈现的改动只进草稿,用户点「发布」走应用发布(不动工作流),沿用现有规则。 **理由**:普通人改看得见的东西最直接(P1);改取数会动口径,必须经过试运行和处置(不可回退决定:口径升版须处置)。 **否决的做法** - 页面编辑器允许拖拽任意布局:像素级设计不是目标(总方案 §1.3); - 页面上直接改 SQL:绕过试运行和处置。 **代价**:编辑状态和查看状态要共用一套渲染,交互细节要打磨。 ### 2.7 N7 个人应用 **决定** - **对象**: - 个人应用是 `scope=personal` 的 App,拥有者是创建它的使用者; - 它的图不进工作流表(使用者没有工作流权限),存在蓝图上,带图哈希; - 没有应用版本,改了就是改了,每次刷新按当前的图跑。 - **能用的东西**,与问数据同一份白名单(`auth/chat_policy.py`): - 节点:input、llm、agent、report、output、retrieve、transform、branch、metrics、merge; - 工具:只读内置工具和只读数据源查询; - 运行时强加 `MASK_RESULTS`、`READ_ONLY`; - 不能有人工节点、不能写入、不能调写入能力。 能用的模板因此只有:周期看板、异常处置的「只看」部分、填报登记的「只看自己的」汇总。 - **刷新**: - 打开时用最近一次成功的刷新; - 点「刷新」发起一次探索运行(`origin_kind=personal_app`),身份是拥有者; - 受每日上限约束,和按需视图共用设置项 `apps.on_demand_daily_limit`,按人计。 - 是否允许每天自动刷新,见待拍板 Q4。 - **视图来源**:新增 `exploratory`,只在个人应用里合法——取这个应用、这个拥有者、这张图哈希下最近一次成功封存的探索运行。印章来源写「探索 · 未经发布审核」,档位照实写。 - **谁看得见**:只有拥有者。管理员在设置里看得到「谁有几个个人应用」,可以归档,但不看内容(P4)。 - **提交审核**: - 拥有者点「提交审核」,选一位搭建者,或者交给搭建者组; - 开一条待办(种类 `review`,派给对方); - 搭建者打开时得到这个蓝图的一份**共享副本**(原个人应用不动),在副本上走试运行和发布单; - 发布后通知提交人,提交人自己决定个人应用留不留。 - **开关**:管理员设置 `apps.user_personal_apps`,默认关(同问数据的开关,待拍板 Q3)。 **理由**:使用者能自助(产品目标);个人应用永远是探索、只读、遮罩(P4,不可回退决定:没发布的图只能跑探索);共享必须经搭建者发布(承诺由搭建者签)。 **否决的做法** - 给使用者开「个人工作流」:工作流表的权限、门禁、定时任务全要另开一套; - 个人应用允许分享给别人:等于绕过发布门禁。 **代价**:视图来源多一种、印章多一种;个人应用的图和工作流的图是两种存法。 ### 2.8 N8 记录表(填报登记) **决定** - **存储**:每个记录表一个平台托管的 SQLite 文件(`data/records/<表 id>.db`),只许追加。 - 每行带系统列:`_seq`(自增序号)、`_id`、`_submitted_by`、`_submitted_at`、`_run_id`(哪次运行写的)、`_supersedes`(更正时指向被更正的行)、`_schema_version`。 - 业务列由「填什么」卡片生成。 - **作为数据源**:登记成 `origin=records` 的只读数据源,看板照常用 `db_query__<名字>` 查,数据目录照常起草。 - **写入只经动作**: - 新增平台内置能力 `records.append`(效果:写入;允许无人值守,因为平台自己的表、幂等键就是动作键); - 填报登记模板的骨架是:入口 → [可选的审批] → 调用工具节点 `records.append` → 成果; - 生效记录、幂等、审计全部沿用总方案 A5,一次提交只落一行,重放不会重复写。 - **更正不改原行**:更正是一次新的提交,带 `_supersedes`。默认查询看「当前有效」的行(没被更正过的),更正历史可查。 - **钉版本**: - 运行开始时,在 data_versions 里记下这张表的高水位 `_seq`; - 查询时连接上建同名的 **TEMP VIEW**(SQLite 解析不带库名的表名时,先找 temp 库),只露出 `_seq ≤ 高水位` 的行。 于是同一次运行查几遍都是同一份数,印章写「数据截至第 N 条」,按需缓存键也带上它(P8)。 - **行级可见**: - 同一套临时视图再加一条按人过滤:`use` 只看 `_submitted_by = 自己`,`observe` 和 `manage` 看全部(拍板项); - 过滤在查询工具的连接上做,模型和 SQL 都绕不过去。 - **看板的汇总**:看板读的是正式运行的封存结果,正式运行以服务身份跑、看到全部行,所以汇总是全量的。这是作者的选择,发布单摘要里写明「使用者能看到全部提交的汇总数」。 - **逐行列表**:用新组件 `records_list`,每次打开按看的人实时过滤(同 `instance_list`),不走封存。 - **表单变了**: - 只加可选字段:`ALTER TABLE ADD COLUMN`,同一表版本; - 改类型、删字段、加必填字段:出新的表版本(新文件 `<表 id>.v2.db`),旧记录照原样保留。不兼容的判定沿用 `INCOMPATIBLE_KINDS`。 - 应用版本钉住表版本;看板要跨版本汇总时,由模板生成 `UNION ALL` 的视图,列对不上的补空。 - **遮罩**:业务列可以在「填什么」卡片里标「敏感」,进数据源的遮罩列表(`mask_columns`)。 **理由**:只追加加序号,在单进程 SQLite 下就能做到可复现(P8,不可回退决定:不上 Postgres、不加节点锁);写入走动作,审批和对账都是现成的;行级可见在连接层做,模型和 SQL 都绕不过去(P4)。 **否决的做法** - 每次提交出一个新快照(复用 table_versions):一次提交一个文件,量一大就扛不住; - 允许原地改行:数就不可复现了; - 行级可见交给模型在 SQL 里加 where:靠不住。 **代价**: - 多一种数据源和一个写入服务;写入走单进程锁,和导入的锁分开; - 高频填报(每秒多次)不是目标场景,写在 README 里。 ### 2.9 N9 文件 **决定** - **存储**: - 按 sha256 内容寻址存,复用 `raw_store` 的做法:`data/files/<前 2 位>/`,文件只读(0444); - 新表 `files` 记 id、sha256、原名、大小、类型、上传人、上传时间、绑定的运行或记录(可空)、用途; - 大小限额沿用 `max_upload_mb`;类型白名单见待拍板 Q5。 - **上传两步走**: 1. `POST /api/files` 带上下文(应用 id 加入口,或者待办 id),先上传,拿回引用 `{file_id, name, size, sha256, type}`; 2. 再提交表单。表单字段 `type: object`、`x-widget: file`,值就是引用。 - **提交时绑定**:服务端核对引用属于提交人、还没被别的提交用过,然后把它绑到这次运行(一次性),引用连同 sha256 进运行输入,随封存一起固定(P8)。没绑定的上传 24 小时后清理。 - **怎么用文件**: - **交给 agent 或报告读**:新增模板函数 `file_text(input.附件)`,运行时按类型抽文本(PDF、Word、表格、纯文本),沿用知识库上传的抽取,有长度上限,超出照实截断并标出; - **作为一期数据导入**:平台内置能力 `table_import.new_period(source, file)`,按这个上传源已有的配方导入,效果是写入(数据变了),走动作和确认; - **附在记录上**:记录表的文件列存引用。 - **下载**:`GET /api/files/{id}`,只给读得到绑定的那次运行、或那一行记录的人(复用 `can_read_run` 和记录的行级可见)。文件不做遮罩,界面在上传处写明「能看到这条记录的人都能下载附件」。 - **使用者**:个人应用里可以上传文件交给 agent 读(只读),不能导入。 **理由**:内容寻址加封存,证据站得住(P8);上传和提交分开,表单仍是 JSON,人工节点的表单也能用;读权限跟运行走,没有新的权限面(P4)。 **否决的做法** - 文件放进工件库:工件库只存 JSON,按运行归属,上传时还没有运行; - base64 塞进表单值:撑爆输入和事件流。 **代价**:多一张表、一个清理任务;抽文本的质量受文件格式影响,照实标「抽取不完整」。 ### 2.10 N10 白话层 **决定** - 新增 `app/plain.py`:问题编号 → `{title 一句话, why 为什么要紧, who: you|assistant|admin, action 怎么办}`。覆盖: - 工作流发布门禁(`contract.*`、`governed.*`、`action.*`、`process.*`); - 应用检查(`app.*`); - 发布单的步骤失败。 - 没登记白话的问题照旧显示技术说法,并在检查脚本里报「有问题编号没有白话」,防止遗漏。 - 前端的发布摘要、蓝图卡片、试运行页都用白话;画布和应用编辑器保留技术说法,另加一行白话。 - `who` 的判定照这三样:自动修复的 `kind`、`APP_HUMAN_ONLY`、需要管理员的问题编号。 **理由**:P5 一种说法;P1 照实说给听得懂的人。 **否决的做法**:让模型临场解释——每次说法不同,还可能说错。 **代价**:每加一个问题编号都要写白话,检查脚本会提醒。 ### 2.11 N11 生成:耗时与稳定性 **决定** - **调用次数的预算**: - 起草蓝图:1 次主调用,加最多 2 次查看; - 填关节:每一组独立的关节(例如两组互不相干的指标)各 1 次调用,**并行**执行(新增,补 G12),各自最多 1 次查看; - 其余全部是确定性的。 - **模型**:蓝图和关节都用设置里的助手模型;首次使用时提示「推荐用快速模型」。验收按真实模型记录每一步的耗时(第六部分 W4)。 - **反问收尾**(补 G6): - 问数据:store 加 ask 分支——这一轮记成「助手反问」,问题进历史;输入框提示「回答助手的问题」; - 画布:这一轮照样记下,问题进历史; - 蓝图对话:问题和候选画成可点的选项。 - **问数据校验不过时**:不再整轮重来,改为把问题交回同一轮的自查修正(Checker 已有),省一次完整生成。 **理由**:普通人等不了几分钟(验收里有模型在画布写 SQL 首个操作要 486 秒);反问是蓝图的核心交互,不能丢。 **否决的做法**:一次调用生成整张图加应用——慢、不稳、难修。 **代价**:并行调用增加瞬时并发,受模型服务的限流约束,失败时退回串行。 ### 2.12 和不可回退决定的对照 | 不可回退决定 | 本方案 | |---|---| | 正式运行只从已发布的不可变版本、按 graph_hash 发起 | 试运行和个人应用都是探索运行;发布单发布后才有正式运行;按图哈希对齐试运行和版本 | | 口径升版必须处置 | 改取数回到蓝图,发布单要求处置,摘要用白话说明 | | 叙述层不做算术 | 关节里的指标式只许 `cell()` 加口径卡算术;卡片上的数来自绑定 | | 没发布的图只能跑探索 | 个人应用永远是探索;印章写明 | | 不上 Postgres、不加节点锁 | 记录表单文件 SQLite、只追加、单进程锁;临时视图钉版本 | --- ## 第三部分 规格 ### 3.1 数据模型 **新表** | 表 | 主要字段 | |---|---| | `blueprints` | id、name、owner、scope(personal / shared)、status(drafting / trialed / published / personal / in_review)、spec(`agentlab.blueprint/1`)、template_id、template_version、source(conversation / template / run / workflow_version,带 id)、workflow_id、app_id、last_trial_run_id、graph(个人应用的图)、graph_hash、copied_from(审核副本指向原蓝图)、created_at、updated_at | | `blueprint_events` | blueprint_id、seq、actor、verified、op(`bp_*`)、before、after、at——蓝图的留痕 | | `publish_plans` | id、blueprint_id、actor、steps(每步 kind、status、result、error_code、at)、summary(白话摘要)、level、status(planned / running / stopped / done)、created_at | | `files` | id、sha256、name、size、mime、uploaded_by、uploaded_at、context(app_id、entry 或 task_id)、bound_run_id、bound_record(表 id、`_id`)、purpose | | `records_tables` | id、app_id、name、source_id(登记的数据源)、version(表版本)、columns(列定义 + 敏感标记)、created_at;表版本另起一行,旧版本只读保留 | **新列**(走 `_COLUMN_MIGRATIONS`) - `apps.scope`(默认 shared)、`apps.owner`、`apps.blueprint_id`; - `runs.origin_kind` 新增取值 `trial`、`personal_app`(总方案 X3 的列表加两项,仍只能由服务端写); - `tasks.kind` 新增 `review`(个人应用提交审核); - 组件 spec 加 `hidden`(`apps/spec.py` 的组件模型,`extra=forbid` 下显式加字段)。 ### 3.2 蓝图 `agentlab.blueprint/1` ```json { "format": "agentlab.blueprint/1", "purpose": {"value": "每周看各门店销售额、订单数和退款率", "source": "user", "status": "confirmed"}, "template": {"id": "periodic_board", "version": 1}, "data": [ {"id": "gmv", "label": {"value": "销售额"}, "unit": {"value": "元"}, "table": {"value": {"source": "shop", "table": "orders"}, "source": "catalog", "status": "proposed"}, "measure": {"value": "sum(amount)", "source": "assistant", "status": "proposed"}, "date": {"value": {"column": "paid_at", "rule": "按支付日"}, "source": "catalog", "status": "confirmed"}, "filter": {"value": "不含已退款", "source": "user", "status": "confirmed"}, "reuse": null} ], "cadence": {"value": {"preset": "weekly", "weekday": 1, "time": "09:00", "tz": "Asia/Shanghai", "period": "last_iso_week"}}, "people": [], "fields": [], "writeback": null, "access": {"use": [{"kind": "group", "name": "门店"}], "observe": [], "manage": []}, "joints": {"sql_main": {"status": "filled", "by": "assistant"}, "metric_gmv": {"status": "pending"}} } ``` - 每一项都是 `{value, source, status}`;`source` 取 user / assistant / template / catalog / caliber;`status` 取 confirmed / proposed / missing。 - `measure` 是白话或简单聚合的提议,**不是**最终表达式;最终的 SQL 和指标式在关节里,由 `TemplateApplier` 落地并经过检查。 - `reuse`:对到已发布口径卡时写 `{workflow_id, version, node_id, metric_id}`,实例化成 `caliber_from`。 **操作**:`bp_set(path, value)`、`bp_add(path, item)`、`bp_remove(path)`、`bp_ask(question, options[])`、`bp_template(id)`。路径用 JSON 指针。模型和用户共用这组操作;`BlueprintApplier` 按卡片的白名单校验(比如 `people[].policy.kind` 只能是 any / all / quorum)。 ### 3.3 模板 `agentlab.template/1` 与首批五个 **模板定义**(后端模块): ```text id, version, label, description, applies_when(给助手看的适用条件) cards: 用到哪些卡片、各自的约束 skeleton(bp) -> GraphSpec # 带关节占位 joints: [{id, kind, node_id, field, from_card | generated, check}] app_skeleton(bp, graph) -> agentlab.app/1 # 在 derive_app 的结果上按模板排布 schedule(bp) -> 定时任务参数 | None tests: 每个模板至少一张完整蓝图的实例化 + 检查 + 运行 ``` **首批五个** | 模板 | 骨架 | 生成关节 | 确定性关节 | 应用页 | |---|---|---|---|---| | 周期看板 `periodic_board` | 入口(统计期)→ 取数(tool)→ 口径卡 → 成果(指标出具) | SQL、指标式 | 统计期字段、定时任务、出具契约的必需指标 | 印章、指标块、趋势、明细表、「按条件查看」(按需) | | 审批流程 `approval` | 入口(表单)→ [按金额线分支] → 人工(一级或多级)→ [写回] → 已通过 / 已退回 | 无 | 表单、金额线、处理人与策略、时限与升级、写回目标 | 发起按钮、我发起的、实例进度、待办 | | 异常处置 `exception_handling` | 定时:入口 → 取数(异常清单)→ 分支(有 / 无)→ 人工(负责人填处置说明,表格附在待办上)→ [复核] → 成果 | 异常的 SQL | 负责人、时限、复核策略 | 异常数指标、本期异常表、处置进度 | | 填报登记 `records` | 入口(表单)→ [审批] → `records.append` → 成果;另配一张汇总看板(周期看板的骨架,查记录表) | 汇总的 SQL、指标式 | 表单与表结构、审批、行级可见 | 填报按钮、我的记录(`records_list`)、汇总看板 | | 核对写回 `reconcile_writeback` | 定时:取数 → 口径卡(核对差异)→ 人工复核(确认要写回的)→ 写回 → 成果 | 取数的 SQL、差异的指标式 | 复核人、写回目标与字段对应、确认 | 差异指标、待复核清单、写回记录 | - 每个模板都能被个人应用用到的部分(只读)标出来:周期看板全部、异常处置去掉人工步骤只看清单、填报登记只看汇总和「我的记录」。 - 「自由形」:从已有运行或工作流版本导入,没有关节,蓝图只有目的、谁能看、多久更新三张卡片;要改图只能回画布(搭建者)。 ### 3.4 关节类型 | kind | 填什么 | 谁填 | 检查 | |---|---|---|---| | `sql` | 调用工具节点的 `args.sql` | 模型(look 表) | 只读守卫、SQL 猜列、数据源范围、目录检查(GraphChecker) | | `metric_expr` | 口径卡一个指标的 `expression` | 模型 | 只许 `cell()`、口径卡算术、比较;静态追到原始数据(`engine/lineage.static_stops`) | | `fields` | 入口或人工节点的表单 | 蓝图「填什么」 | `agentlab.form/1` 校验 | | `assignee` / `policy` / `due` / `on_timeout` | 人工节点配置 | 蓝图「谁来做」 | 流程检查(`engine/lint_process.py`)、岗位派生 | | `threshold` | 分支条件「字段 比较 常量」 | 蓝图 | 表达式解析器,禁止算术 | | `capability` | 调用工具节点的 tool 和参数映射 | 蓝图「写回哪里」 | 能力登记、写入只在调用工具节点(总方案 A5)、参数按声明类型 | | `contract` | 成果节点的出具契约(必需指标) | 蓝图「看哪些数」 | 出具契约检查(含指标出具) | `TemplateApplier.apply(op)` 只收 `fill_joint {joint, value}`,`joint` 必须是这个模板声明的生成关节;`TemplateChecker.issues()` 跑上表的检查加骨架比对,交回模型改(最多 2 轮,沿用 `Generation` 的修正阶段)。 ### 3.5 试运行解析 - `POST /api/blueprints/{id}/trial {inputs?}`:实例化 → 保存草稿图 → 发起探索运行(`origin_kind=trial`)→ 返回 run_id;结束后蓝图状态记 trialed、`last_trial_run_id`。 - `GET /api/blueprints/{id}/preview?page=…&run=…`:派生(或沿用)应用草稿,调 `resolve_page(draft=True, trial=run)`: - 所有视图来源解析到这次运行;趋势、按需视图只给这一个点; - 印章 `from = 试运行`、`trial = true`、notes 加「这不是正式结果」; - 流程组件(实例进度、待办)对着这次运行;待办卡按钮写「以试运行身份…」。 - 权限:蓝图的 manage(共享)或拥有者(个人)。 ### 3.6 发布单 - `POST /api/blueprints/{id}/publish-plan {level}`:只读,返回 `{steps[], summary[], problems[{code, plain, who, fix?}], ok}`。 - `POST /api/publish-plans/{plan_id}/run`:按步执行;每步结束写 `steps[i]`;某步失败时状态 stopped,返回白话原因。 - `POST /api/publish-plans/{plan_id}/resume`:修好后从停下的那一步接着做;已完成的步骤按结果里的 id 核对,不重做。 - 每一步调用现有服务函数(发布、定时任务、派生、应用发布、授权),不经 HTTP;预检阶段这些函数以「只检查」方式调用。 - 审计:发布单的开始、每步、结束各写一条(`publish_plan.*`),和各对象自己的审计互相引用。 ### 3.7 记录表 - **写入服务**:`records/store.py`。 - `append(table, values, run_id, actor, action_key)`:在单进程锁内校验列、写一行、返回 `_seq` 和 `_id`; - 同一个 `action_key` 第二次调用返回第一次的结果(幂等)。 - **能力** `records.append`: - 内置登记;参数是 `{table, values}`; - 效果是写入,允许无人值守,查询能力是 `records.lookup(action_key)`。 - **查询连接**: - `data/engine.py` 打开 `origin=records` 的源时,在连接上建 TEMP VIEW:每张业务表一个同名视图; - 视图条件是 `_seq <= 高水位`,加 `_superseded = 0`,加行级过滤; - 高水位来自运行的 data_versions;行级过滤取自运行的发起人和这个应用的授权:正式运行以服务身份跑、不过滤;`records_list` 按看的人过滤。 - **表版本**:「填什么」卡片变化时,按 `INCOMPATIBLE_KINDS` 判定,兼容的就 ADD COLUMN,否则新建版本文件并登记一行 `records_tables`。 ### 3.8 文件 - **接口**: - `POST /api/files`(multipart;context 必填)→ 引用; - `GET /api/files/{id}`(按绑定对象核对读权限); - `DELETE /api/files/{id}`(只能删自己还没绑定的)。 - **控件**:`x-widget: file`,字段 `type: object`,属性 `{file_id, name, size, sha256, type}`;`accept`、`max_mb` 进 `x-file`。前后端校验同一份:`apps/forms.py`、`lib/schemaForm.ts`。 - **模板函数** `file_text(ref, max_chars=…)`:运行时抽文本,结果记进运行事件(字数、是否截断),抽取的文本按工件存,进证据。 - **清理**:调度循环每小时清一次 24 小时前上传、没有绑定的文件。 ### 3.9 个人应用 - `POST /api/blueprints/{id}/save-personal`:校验图满足 `chat_policy`(节点、工具、无人工、无写入),建或更新 `scope=personal` 的 App,图存在蓝图上。 - `POST /api/apps/{id}/refresh`(个人):发起探索运行(`origin_kind=personal_app`,身份是拥有者,强加 `MASK_RESULTS`、`READ_ONLY`),受每日上限约束。 - 视图来源 `exploratory`:只在 `scope=personal` 的应用里合法(应用检查拦),解析时取最近一次成功封存的 `personal_app` 运行。 - `POST /api/apps/{id}/submit-review {to}`:开 `review` 待办;搭建者处理时调 `POST /api/blueprints/{id}/copy` 得到共享副本。 - `can_read_run` 加一条:拥有者能读自己个人应用的运行(`auth/access.py:74-96`)。 ### 3.10 接口一览 | 接口 | 权限 | |---|---| | `POST /api/blueprints`(起点:对话 / 模板 / 运行 / 版本) | builder;个人:user + 开关 | | `GET/PATCH /api/blueprints/{id}`(PATCH 收 `bp_*` 操作数组) | 拥有者或共享蓝图的 manage | | `POST /api/copilot/blueprint-stream` | 同上 | | `POST /api/copilot/fill-joints-stream` | 同上 | | `POST /api/blueprints/{id}/trial`、`GET …/preview` | 同上 | | `POST /api/blueprints/{id}/publish-plan`、`/api/publish-plans/{id}/run|resume` | builder(受管级再要 admin) | | `POST /api/blueprints/{id}/save-personal`、`/api/apps/{id}/refresh`、`/submit-review` | user + 开关,拥有者 | | `POST /api/blueprints/{id}/copy` | builder | | `GET /api/templates` | 登录即可 | | `GET /api/apps/{id}/candidates` | manage | | `POST/GET/DELETE /api/files…` | 见 §3.8 | | `GET /api/apps/{id}/records`(`records_list` 用) | use(只看自己的)/ observe | ### 3.11 事件 - 运行事件:`run.origin` 的 kind 加 trial、personal_app;`file.extracted`(文件抽文本:字数、是否截断);`records.appended` 是动作事件(`action.*`)的一种,不另起。 - 蓝图事件存 `blueprint_events`,不进运行事件。 ### 3.12 权限矩阵(开登录时) | 操作 | admin | builder | user | |---|---|---|---| | 建共享蓝图、试运行、发布单(已发布级) | ✓ | ✓ | ✗ | | 发布单(受管级、降级) | ✓ | ✗ | ✗ | | 建个人应用、刷新、提交审核 | ✓ | ✓ | 开关开着时 ✓ | | 审核个人应用(得到共享副本) | ✓ | ✓ | ✗ | | 看别人的个人应用内容 | ✗ | ✗ | ✗ | | 上传文件 | 有对应入口或待办的人 | 同左 | 同左 | | 下载文件 | 读得到绑定的运行或记录的人 | 同左 | 同左 | | 记录表逐行查看 | 全部 | 全部(有 observe) | 只看自己的(use) | 关登录时全部放行,个人应用的开关照样生效(关着就没有入口)。 ### 3.13 新增检查 - **蓝图**(`bp.*`,蓝图卡片和发布单预检里显示): - `bp.missing`:缺必填卡片项; - `bp.unconfirmed`:待确认项未确认就发布; - `bp.data_unmapped`:看哪些数没对到表或口径; - `bp.people_unbound`:岗位没有人; - `bp.reconciler_unbound`:写回的能力没登记查询,结果不明时没人核实(同上,只挡发布;发布单上应用检查说同一件事的 `app.reconciler_unbound` 和核实岗位的 `app.slot_unbound` 不再重复列); - `bp.template_mismatch`:卡片内容超出模板能力。 - **模板**(`tpl.*`): - `tpl.skeleton_changed`:实例化的图改了骨架; - `tpl.joint_unfilled`:生成关节没填; - `tpl.joint_refused`:模型想改非关节的位置。 - **应用**: - `app.view_source_personal`:`exploratory` 来源出现在共享应用里,error; - `app.hidden_bound`:被隐藏组件是唯一绑定某个视图的组件,warning。 - `app.records_rows_exposed`:看板的表格绑定的查询直接列出记录表的逐行(没有聚合),warning。正式运行看得到全部行,这样等于所有使用者都能看到别人的记录,绕过了行级可见;逐行查看请用 `records_list`。 - **个人应用**:`personal.policy`,图里有使用者不能用的节点或工具(沿用 `chat_graph_problem` 的说法)。 - **白话**:以上全部登记进 `app/plain.py`。 --- ## 第四部分 界面 ### 4.1 新建应用 - **入口**:应用首页右上「新建应用」。使用者在开关开着时也有这个入口,只能建个人应用。 - **三个起点卡片**: - 说说你要什么:大输入框加例句; - 从场景模板开始:五张模板卡,每张写适用场景、会产出什么、要准备什么; - 从已有的开始:最近的问数据回答,加上已发布的工作流(后者只给搭建者)。 - **问数据的「做成应用」按钮**:改为「做成应用(蓝图)」,直接带着这次运行进蓝图,不再跳画布发布框。 ### 4.2 蓝图页 - **左侧**:对话。助手的反问画成可点的选项,「这一轮在等你回答」常显。 - **右侧**:卡片列,每张卡片头上标状态(已确认、待确认、缺)。卡片里每一项都可以点开改,改完标来源「你」。 - **对到数据的项**:显示对到的表、中文名、粒度、业务日期,带一句「为什么对到这张表」,来自 look 和目录。 - **底部**:「用真数试一次」(缺项时置灰,写清缺什么);试过之后才有「发布」(搭建者)、「存成我的应用」(使用者)。 ### 4.3 试运行页 - 就是应用的样子,顶部横幅写「试运行:数据是真的,但这不是正式结果」,配试运行的输入和「换一期再试」。 - 流程类:右侧列出试运行里的待办,自己点同意或驳回;写入步骤标「测试环境」或「演练」。 - 出错时用白话说哪一步、为什么,加「交给助手修」或「回到卡片改」。 ### 4.4 发布摘要 - 一页:每一步一句白话。问题分三栏:你来定、助手可以修、要找管理员。 - 确认按钮写清结果,例如「发布并每周一 09:00 自动出数」。 - 执行时逐步打勾;停下时写在哪一步、为什么、修好后「接着做」。 ### 4.5 页面上直接改 - 应用页右上「编辑」(manage)。 - 编辑状态下,组件悬停描边,点中出小工具条:改文字、隐藏、上移、下移、删除。 - 页底「加一张数据卡片」,弹出候选列表,按口径卡、报告、明细、趋势分组。 - 顶部「改取数」回到蓝图。 - 改动自动存草稿,顶部写「有未发布的改动」和「发布」。 ### 4.6 个人应用 - 应用首页「我的应用」分组,卡片角标「个人」。 - 印章写「探索 · 未经发布审核」,顶部有「刷新」(写明今天还剩几次)和「提交审核」。 - 审核中写状态;审核发布后写「已有正式版本:去看看」。 ### 4.7 新组件和控件 - **文件控件**:拖放或点选;显示文件名、大小、上传进度、类型和大小限制;上传失败照实说。 - **`records_list`**:列表、筛选、按时间排序;每行可以展开看全部字段和附件;被更正过的行显示「已更正」,可以展开看历史;使用者只看自己的,标题写「我的记录」。 ### 4.8 文案与检查脚本 - **文案**:`termsApp.ts` 新增蓝图、模板、试运行、发布单、个人应用、记录、文件各组说法;后端白话层 `app/plain.py`。 - **新检查脚本**: - `check-blueprint`:起点、卡片、反问、试运行、发布摘要; - `check-personal`:使用者建、刷新、提交审核; - `check-records`:填报、行级可见、更正、表版本; - `check-files`:上传、绑定、下载权限。 - **要更新的**:`check-apps`(编辑状态、隐藏、候选)、`check-chat`(反问收尾、做成应用改道)、`check-studio`(反问记录)。 - **检查脚本的新约束**:每个问题编号都要有白话。 --- ## 第五部分 实施组织(供下次审批,这一步不执行) - **分支**:从最新 main 开集成分支 `feat/normal-user-apps`,所有包在同一分支、同一工作区。 - **第 0 步契约骨架**:编排者一次提交,包括: - 新表和新列、`agentlab.blueprint/1` 和 `agentlab.template/1` 的 pydantic 模型; - 关节类型和操作的白名单、`app/plain.py` 的框架; - 新路由的桩(501)、`records.append` 和 `table_import.new_period` 的能力登记; - 前端类型和接口桩、占位路由。 之后冻结热点文件:`db/models*.py`、`db/base.py`、`core/events.py`、`typesApp.ts`。 - **一个 workflow,六个包**,每包一个完整代理,然后一个统一复核、一个集中修复: | 包 | 范围 | |---|---| | B1 蓝图与模板(后端) | Blueprint 对象和操作、模板登记和五个模板、实例化和骨架比对、`BlueprintApplier`/`TemplateApplier` 与 Checker、两个生成入口、look 新类别 `caliber`、并行填关节 | | B2 试运行与发布单(后端) | 试运行解析(`trial` 旁路)、派生自图、发布单的预检 / 执行 / 续做、白话层的内容、反问收尾的服务端部分 | | B3 记录表与文件(后端) | 记录表存储与连接层的临时视图、`records.append`、表版本、`records_list` 解析、文件存储 / 接口 / 绑定 / 清理、`file_text`、`table_import.new_period` | | B4 个人应用(后端) | scope 与拥有者、`exploratory` 视图来源与印章、刷新与上限、提交审核与副本、`can_read_run`、开关 | | F5 创建链路(前端) | 新建应用三个起点、蓝图页、试运行页、发布摘要、问数据「做成应用」改道、反问在问数据和画布的收尾 | | F6 页面编辑与新组件(前端) | 编辑层、隐藏、候选、文件控件、`records_list`、个人应用的界面 | - **合并顺序**:B1 → B2 → B3 → B4 → F5 → F6,编排者做集成修正。 - **验证分层**(沿用):代理只跑相关测试和 tsc、check-copy;完整关口由编排者在合并后跑一次。 - **真实模型验收和端到端验收**:都在关口之后跑。 - **合入 main 之前**:先问用户。 --- ## 第六部分 验证与验收 - **W1 看板(搭建者,从一句话开始)**:「每周一看各门店上周销售额、订单数、退款率,不含退款单」。 - 反问一次(按下单日还是支付日); - 卡片对到正确的表和业务日期; - 试运行出真数,点数字追到单元格; - 发布摘要一页,确认后 9 步全部完成,应用打开就有数; - 从说完需求到看到试运行结果,记录用时和模型调用次数。 - **W2 审批(搭建者,从模板开始)**:报销审批,金额线 5000,财务组全员同意,48 小时升级给总监,通过后写到假外部系统。 - 试运行里自己点完审批,写入走测试环境; - 发布后真实走一单; - 中途故意让岗位没绑人:发布单停在预检,白话说「谁来做」缺人。 - **W3 填报登记**:门店每日报损。 - 两位使用者各提交几条,彼此看不到对方的记录;观察者看全部;汇总看板是全量的; - 更正一条:原行保留,汇总按更正后的算; - 改表单:加一个可选字段不出新版本;把数字字段改成文字,出 v2,旧记录还在。 - 同一次运行查两遍,数一样(高水位)。 - **W4 真实模型**:本机助手模型、deepseek-v4.1-flash、glm-5.3,跑 W1、W2 的蓝图和关节,记录: - 调用次数、查看次数、首个操作时间、总用时; - 反问是否发生、关节检查的修正轮数。 目标: - 起草蓝图 ≤ 3 次调用; - 填关节在快速模型上一分钟内完成; - 卡片和关节出错时,用户能看懂、能改。 - **W5 个人应用(使用者)**: - 开关打开后,使用者从问数据的一次回答建「我的退款周报」; - 刷新受每日上限约束;印章写「探索」;结果遮罩; - 提交审核后,搭建者在副本上发布,提交人收到通知; - 管理员看不到个人应用的内容。 - **W6 文件**: - 报销单附发票 PDF,agent 读出金额; - 门店上传本周报表 Excel,作为一期导入(需确认); - 不能读那次运行的人下载 403;没绑定的上传 24 小时后被清理(用缩短的时间测)。 - **关口**:后端全量 pytest、tsc、check-copy、泄露扫描、`check-all --stacks 2`(含新增的四个检查)、check-auth;真浏览器端到端按 W1–W6 截图复核。 --- ## 第七部分 风险与对策 | 风险 | 对策 | |---|---| | 蓝图对错数据(白话对到错的表) | 对到的项默认「待确认」,卡片上写为什么对到这张表;试运行看真数;数据目录越完整越准,卡片上提示补目录 | | 模型慢或不稳 | 关节少、并行、确定性兜底(P3);验收按真实模型记录耗时 | | 模板覆盖不了需求 | 「自由形」加搭建者回画布;模板登记可扩展;收集落在模板外的需求 | | 发布单中途失败 | 步骤顺序让失败无害;可续做;摘要照实说停在哪 | | 记录表并发写 | 单进程锁、事务短;README 写明不面向高频写入 | | 行级可见被绕过 | 在连接层建临时视图,模型和 SQL 都看不到底表;测试覆盖直接写 `main.<表>` 的 SQL 被只读守卫拒 | | 看板把记录逐行列给所有人 | 正式运行看全部行是设计如此;应用检查对「逐行列出记录表」的表格给警告(`app.records_rows_exposed`),发布摘要写明使用者能看到什么 | | 个人应用变成「影子正式应用」 | 永远探索、印章写明、不能分享;共享只能经审核发布 | | 文件泄露 | 下载按绑定对象核对读权限;界面写明谁能下载;不做公开链接 | | 白话说错 | 白话层只有一份,按编号映射,检查脚本防遗漏 | --- ## 第八部分 待拍板 | # | 问题 | 推荐 | |---|---|---| | Q1 | 记录表的存储 | 平台托管 SQLite、只追加、序号高水位钉版本、连接层临时视图做行级可见(N8) | | Q2 | 填报提交走不走工作流 | 走:模板里 `records.append` 写入节点,可以加审批,沿用动作的幂等和对账 | | Q3 | 使用者建个人应用的开关 | 管理员开关,默认关(同问数据) | | Q4 | 个人应用的刷新 | 只手动刷新,加每日上限;不做定时自动刷新(自动刷新等于给使用者开定时任务) | | Q5 | 文件的类型和大小 | 白名单:PDF、Word、Excel、CSV、图片(PNG/JPG)、纯文本;大小沿用 `max_upload_mb`(默认 50MB);未绑定的 24 小时清理 | | Q6 | 首批模板 | 五个:周期看板、审批流程、异常处置、填报登记、核对写回,外加自由形 | | Q7 | 流程类试运行的审批 | 派给试运行的人自己点;写入走测试环境或演练;不触发超时升级 | | Q8 | 发布单里「跑第一次」 | 看板类默认勾选,流程类不跑 | --- ## 附录 A 代码位置索引(main@69c5c74) | 位置 | 内容 | |---|---| | `backend/app/api/copilot_gen.py:567` | Generation;`:589` 起草、`:593` 修正、`:38` 修正轮数 | | `backend/app/look/base.py:22-27,87-110` | 查看上限;Applied / Applier / Checker 协议 | | `backend/app/api/copilot.py:1679` | `/generate-stream`;`:1744` 只问不改的一轮;`:2364` `/app-stream`;`:2468-2550` from-run 与来源记录 | | `backend/app/apps/ops.py:264` | `polish_problem`(护栏的写法) | | `backend/app/engine/autofix.py:1240` | `forbidden_changes`(骨架比对的写法) | | `backend/app/apps/derive.py:85-90` | 派生时视图绑定时任务 | | `backend/app/apps/resolve.py:82-108,454-476,541-546,203-221,813` | 最近封存的规则;草稿解析;按运行解析;印章来源;趋势无钉住项报错 | | `backend/app/apps/spec.py:35-42,47,82,97-107` | 表单控件、视图来源、组件模型 | | `backend/app/engine/launch.py:95-176` | 运行类别的判定 | | `backend/app/auth/chat_policy.py:25-73` | 使用者能用的节点和工具 | | `backend/app/api/runs.py:108-123` | 使用者运行的限制 | | `backend/app/auth/access.py:74-96` | `can_read_run` | | `backend/app/data/catalog.py:12-21` | 数据目录项的形状 | | `backend/app/data/raw_store.py:33-43` | 按哈希存原件 | | `backend/app/data/table_versions.py:1-33,344-360` | 表的版本模型和存放 | | `backend/app/data/recipe_accumulate.py:38-41` | 不兼容的列变化 | | `backend/app/engine/runner.py:242-262,1013-1045` | data_versions 的固定;封存 | | `backend/app/api/workflows.py:427-503` | 发布 | | `backend/app/api/schedules.py:461`,`scheduler/service.py:39-56` | 建定时任务;模板变量 | | `backend/app/api/apps.py:186,203,251,370,474,273` | 建应用、派生、草稿、发布、授权、归档 | | `frontend/src/store/chat.ts:1241,1433` | 问数据整轮重来;没有反问分支 | | `frontend/src/apps/ViewRenderer.tsx:75-84` | 组件包在 `data-node` 里 | | `frontend/src/pages/ChatPage.tsx:479-523` | 问数据的「做成应用」 | ## 附录 B 术语 - **蓝图**:用户的需求被整理成的一组白话卡片,是生成工作流和应用的依据。 - **卡片**:蓝图里用户能判断对错的一件事(看哪些数、谁来做……)。 - **场景模板**:一类应用的骨架,带可填的关节。 - **关节**:模板骨架里允许填的位置;确定性关节按卡片填,生成关节由助手填。 - **试运行**:跑一次还没发布的图,用真数看应用的样子;不是正式结果。 - **发布单**:一次确认发布的编排,逐步执行、可续做。 - **个人应用**:使用者自己建、只有自己看得见的应用,永远是探索。 - **记录表**:平台托管、只许追加的表,填报登记的每次提交是一行。 - **高水位**:一次运行开始时记录表的最大序号,这次运行只看到这之前的行。 - **白话层**:问题编号到白话说法的唯一映射。