From c38c0043a4d5c07cbf67a2d4b45659b1eb6a0d3e Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Wed, 14 Oct 2026 17:56:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A5=91=E7=BA=A6=E9=AA=A8=E6=9E=B6=E8=A1=A5?= =?UTF-8?q?=E5=85=85=EF=BC=9A=E8=BF=90=E8=A1=8C=E8=B7=AF=E5=BE=84=E4=B8=8A?= =?UTF-8?q?=E7=9A=84=E8=83=BD=E5=8A=9B=E7=99=BB=E8=AE=B0=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E7=BB=99=E5=87=BA=E5=92=8C=E4=BB=8A=E5=A4=A9=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E7=9A=84=E9=BB=98=E8=AE=A4=E5=AE=9E=E7=8E=B0=EF=BC=88=E5=B9=B6?= =?UTF-8?q?=E8=A1=8C=E5=BC=80=E5=8F=91=E4=BA=92=E4=B8=8D=E5=8D=A1=E4=BD=8F?= =?UTF-8?q?=EF=BC=89=EF=BC=8Cbuild=5Ftools=20=E5=8A=A0=20allow=5Fwrite=20?= =?UTF-8?q?=E5=8F=82=E6=95=B0=EF=BC=9B=E5=89=8D=E7=AB=AF=E8=A1=A5=20ViewRe?= =?UTF-8?q?nderer=E3=80=81PrincipalPicker=20=E7=9A=84=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=A1=A9=E5=92=8C=E6=8C=89=E8=8D=89=E7=A8=BF=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E7=9A=84=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/capabilities/registry.py | 14 +++++++---- backend/app/tools/registry.py | 5 +++- frontend/src/api/apps.ts | 5 +++- frontend/src/apps/ViewRenderer.tsx | 27 +++++++++++++++++++++ frontend/src/components/PrincipalPicker.tsx | 22 +++++++++++++++++ 5 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 frontend/src/apps/ViewRenderer.tsx create mode 100644 frontend/src/components/PrincipalPicker.tsx diff --git a/backend/app/capabilities/registry.py b/backend/app/capabilities/registry.py index 295a953c..15dc1cc3 100644 --- a/backend/app/capabilities/registry.py +++ b/backend/app/capabilities/registry.py @@ -28,25 +28,29 @@ async def sync_all(session: AsyncSession) -> int: raise NotImplementedError("BE-1") +# 运行路径上会被别的包调到的几个函数,契约骨架里给的是「和今天行为一致」的默认实现(不查登记、不改变任何判定), +# 并行开发时一个包没写完不会弄挂别的包的测试。BE-1 换成真实现,签名不变。 + + async def get_capability(session: AsyncSession, name: str) -> Any | None: """一行能力登记(db.models_capabilities.Capability),没有就是 None。""" - raise NotImplementedError("BE-1") + return None def effective_effect(capability: Any) -> str: """治理后的效果:effect 为空时按 effect_system。""" - raise NotImplementedError("BE-1") + return getattr(capability, "effect", None) or getattr(capability, "effect_system", None) or "dynamic" async def capability_snapshot(session: AsyncSession, tool_names: list[str]) -> dict[str, Any]: """运行发起时用到的能力的治理快照:{名字: {effect, approval, unattended_write, version, endpoint_id, masked}}。 写进 Run.options["capabilities"],恢复和重放用同一份。""" - raise NotImplementedError("BE-1") + return {} async def capability_versions_for(session: AsyncSession, tool_names: list[str]) -> dict[str, int]: """发布时记下的能力版本:{名字: version}。""" - raise NotImplementedError("BE-1") + return {} def is_platform_query(name: str) -> bool: @@ -66,4 +70,4 @@ async def endpoint_request(session: AsyncSession, capability: Any, args: dict[st def masked_paths(capability: Any) -> list[str]: """能力的遮罩字段(JSON 指针)。""" - raise NotImplementedError("BE-1") + return list(getattr(capability, "masked", None) or []) diff --git a/backend/app/tools/registry.py b/backend/app/tools/registry.py index aaddb729..e7ea3c35 100644 --- a/backend/app/tools/registry.py +++ b/backend/app/tools/registry.py @@ -140,10 +140,13 @@ def build_tool(spec: ToolSpec, ctx: ToolContext) -> BaseTool: async def build_tools( - names: list[str], ctx: ToolContext, *, session: Any = None + names: list[str], ctx: ToolContext, *, session: Any = None, allow_write: bool = False ) -> list[BaseTool]: """按名字列表组装工具。 + allow_write(应用平台 A5):写入能力只能放在调用工具节点里——只有调用工具节点传 True;agent、协作节点用默认值, + 能力登记里治理后效果为 write 的工具在这里就被拒绝(BE-1 实现判定;契约骨架阶段不拦,行为同以前)。 + 四种来源:内置(静态注册)、MCP(mcp:/)、数据源 (db_query__<源名> / db_schema__<源名>)、自定义(数据库里的 CustomTool)。 后两种都是运行时从库里读出来动态构造的,不在静态注册表里。 diff --git a/frontend/src/api/apps.ts b/frontend/src/api/apps.ts index d61c7938..a1cb5ab5 100644 --- a/frontend/src/api/apps.ts +++ b/frontend/src/api/apps.ts @@ -32,9 +32,12 @@ export const apps = { grants: (id: string) => get<{ items: AppGrant[] }>(`${at(id)}/grants`), setGrants: (id: string, items: AppGrant[]) => put<{ items: AppGrant[] }>(`${at(id)}/grants`, { items }), /** 页面解析:服务端解析好的视图模型(不可见的组件不下发,遮罩值不出服务端) */ - page: (id: string, page: string, params?: { instance?: string; params?: Record }, opts?: RequestOptions) => + page: (id: string, page: string, + params?: { instance?: string; params?: Record; draft?: boolean }, opts?: RequestOptions) => get(`${at(id)}/pages/${encodeURIComponent(page)}${qs({ instance: params?.instance, params: params?.params ? JSON.stringify(params.params) : undefined, + // 编辑器预览:按草稿解析(不发起运行,视图取钉住版本已有的封存运行) + draft: params?.draft ? 1 : undefined, })}`, opts), /** 发起一个实例 */ start: (id: string, entry: string, values: Record) => diff --git a/frontend/src/apps/ViewRenderer.tsx b/frontend/src/apps/ViewRenderer.tsx new file mode 100644 index 00000000..d65f2f50 --- /dev/null +++ b/frontend/src/apps/ViewRenderer.tsx @@ -0,0 +1,27 @@ +/** + * 视图渲染器:按服务端解析好的视图模型(agentlab.view/1)渲染应用页(FE-5 实现)。 + * + * 前端只有一张「组件名 → 渲染器」的表;不认识的组件显示「此组件需要更新前端」,不报错、不空白。应用编辑器的预览 + * (FE-6)也用它:传 draft 解析出的视图模型即可。契约骨架里只有接口和最简实现,props 不变。 + */ +import type { ViewModel, ViewNode } from '../typesApp' + +export interface ViewRendererProps { + model: ViewModel + appId: string + /** 实例页传入:组件里的待办、动作、进度按这个实例解析 */ + runId?: string + /** 预览:不发起运行、不提交表单,点数字照样能看出处 */ + preview?: boolean + /** 页内跳转(页签、页面链接) */ + onNavigate?: (pageId: string) => void + /** 视图刷新、实例发起之后要重新取视图模型 */ + onReload?: () => void +} + +export function ViewRenderer({ model }: ViewRendererProps) { + const walk = (nodes: ViewNode[]) => nodes.map((n) => ( +
{n.children && walk(n.children)}
+ )) + return
{walk(model.tree)}
+} diff --git a/frontend/src/components/PrincipalPicker.tsx b/frontend/src/components/PrincipalPicker.tsx new file mode 100644 index 00000000..50bff22e --- /dev/null +++ b/frontend/src/components/PrincipalPicker.tsx @@ -0,0 +1,22 @@ +/** + * 选人或组(FE-5 实现):岗位绑定、应用授权、改派共用。关登录时只能手填名字(没有用户表)。 + * 契约骨架里只有接口和最简实现,props 不变。 + */ +export interface Principal { + kind: 'user' | 'group' | 'everyone' + name: string +} + +export interface PrincipalPickerProps { + value: Principal[] + onChange: (value: Principal[]) => void + /** 允许选「所有人」(应用授权用) */ + allowEveryone?: boolean + /** 只能选一个 */ + single?: boolean + label?: string +} + +export function PrincipalPicker({ value }: PrincipalPickerProps) { + return
{value.map((p) => p.name).join('、')}
+}