Files
ThearchyHelios 277982ead3 普通用户建应用(后端):
- 蓝图:bp_* 操作和逐卡白名单、蓝图检查、留痕、按蓝图判权限;五个场景模板加自由形(骨架、关节、应用骨架、定时任务),实例化与骨架比对;BlueprintApplier / TemplateApplier(只收模板声明过的关节)与自查;look 新类别口径卡;蓝图对话和填关节两个流式接口,不同组的关节并行填、出错的组串行重试;模板列表
- 试运行与发布单:四种起点(对话、模板、问数据的一轮、已发布版本);试运行是探索运行,草稿应用按这次运行解析、印章写试运行,流程类派给自己、不触发升级;发布单九步、预检只读、每步留痕、停下后接着做、核对卡片指纹和应用管理权、启动时整理中断的单;白话层覆盖全部问题编号
- 记录表与文件:平台托管只追加的记录表、按高水位固定数据、连接上的临时视图做行级可见、写入走动作(records_append)、表版本在发布时切换、草稿不影响线上、记录列表遮敏感列;文件按哈希存、两步上传、提交时绑定运行、下载按读权限、清理、file_text、按配方导入一期(table_import_period)
- 个人应用:存成个人应用(问数据的约束)、刷新(遮罩、只读、每日上限)、只有拥有者看得见、提交审核与共享副本、开关下发到认人接口;页面编辑的候选;隐藏组件不下发;三条新检查
2026-10-15 12:09:29 +08:00

67 lines
3.2 KiB
Python

"""页面编辑的候选(docs/design/normal-user-apps.md N6、§4.5「加一张数据卡片」):GET /api/apps/{id}/candidates。
从草稿里各视图固定的版本(共享应用是工作流版本,个人应用是蓝图上的图)列出能加的数据卡片:口径卡的指标、趋势、报告、
取数节点的表格,规则同派生(apps/derive.data_candidates)。每个候选交回一个可以直接 app_add 的组件:绑定写好了视图,
id 在这份草稿里不重名。草稿里已经有一模一样的(同类组件、同一个绑定)不再列——要再显示一次,把隐藏的那个取消隐藏。
实例视图不列:实例页上的数跟着某一次实例走,「加一张数据卡片」是给应用页的。还没有图的视图(固定的版本不存在、试运行
派生的草稿没有版本)也不列,界面照实写「没有能加的」。
"""
from __future__ import annotations
import copy
import json
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
#: 候选组件 id 的前缀
PREFIX = "add"
#: 视图来源在候选分组里的说法:草稿里不止一个视图时,分组写清是哪一种来源的数
SOURCE_TEXT = {"latest_sealed": "最近一次出数", "on_demand": "按条件查看", "exploratory": "最近一次刷新"}
def _key(comp: dict[str, Any]) -> tuple[str, str]:
return str(comp.get("type")), json.dumps(comp.get("bind"), sort_keys=True, ensure_ascii=False)
async def app_candidates(session: AsyncSession, spec: dict[str, Any]) -> list[dict[str, Any]]:
"""[{kind: metric|series|report|table, label, group, component}],按视图、再按派生规则的顺序。"""
from app.apps.derive import data_candidates
from app.apps.pins import load_pins
from app.apps.tree import components, node_map
pins = await load_pins(session, spec)
views = {vid: v for vid, v in (spec.get("views") or {}).items()
if isinstance(v, dict) and ((v.get("source") or {}).get("kind")) != "instance"}
present = {_key(c) for c, _, _ in components(spec)}
taken = {str(c.get("id")) for c, _, _ in components(spec)} | {
str(p.get("id")) for p in spec.get("pages") or [] if isinstance(p, dict)}
out: list[dict[str, Any]] = []
for vid, view in views.items():
source = view.get("source") or {}
state = pins.get(source.get("pin"))
graph = state.graph if state is not None else {}
if not graph:
continue
for item in data_candidates(node_map(graph), vid, PREFIX):
comp = copy.deepcopy(item["component"])
if _key(comp) in present:
continue
comp["id"] = _unique(f"{comp['id']}_{vid}" if len(views) > 1 else comp["id"], taken)
taken.add(comp["id"])
group = item["group"]
if len(views) > 1:
group = f"{group} · {SOURCE_TEXT.get(str(source.get('kind')), vid)}"
out.append({"kind": item["kind"], "label": item["label"], "group": group, "component": comp})
return out
def _unique(base: str, taken: set[str]) -> str:
if base not in taken:
return base
n = 2
while f"{base}_{n}" in taken:
n += 1
return f"{base}_{n}"