空间内嵌应用:应用模板

Knodo 应用模板机制:把工作空间的「形态」沉淀为可复用的模板,AI 辅助开发,一键分发给团队

应用生态

Knodo 应用生态(App Ecosystem)让你把一个工作空间的「形态」 —— 包括侧边栏布局、自定义面板、业务视图 —— 沉淀成可复用的模板,然后一键分发给团队其他人。

比起从零搭建一个新空间,基于 App 模板创建的空间开箱即用:侧边栏已经按业务流程排好,自定义面板(比如"客户列表"、"销售漏斗")直接显示业务数据,团队成员打开即用。

核心概念

空间应用与独立应用

App 通过 app.yaml 的 type 选择宿主形态,默认值为 workspace:

type: standalone

空间应用继续由平台解释 views 和 layout,显示在目标空间的应用模板设置中。独立应用则由源码中的 src/index.tsx 接管整个页面,发布后生成 index.js;平台只负责登录、组织授权、运行时上下文和资源加载,不渲染平台 Header、Sidebar 或菜单。两种形态仍可共用 src/panels/、views、layout 和 AppView,修改 type 后重新发布才会切换线上宿主。

独立应用统一使用 /apps/:appSlug/* 地址,应用可以自行定义任意深度的菜单、列表和详情路由。例如 /apps/crm/customers/42。可选的 workspaceId 查询参数用于指定当前空间;省略时应用仍可运行,但不会伪造空间请求头。应用可通过现有 @knodo/api 调用平台接口,也可使用运行时提供的 apiBaseUrl 调用 App API Proxy。

独立应用会在能力中心的自建应用列表中直接提供「打开」入口;市场应用只有在平台内置或当前组织已安装且启用后才提供入口。安装或更新独立应用时,平台会提示其属于同源可执行代码,请确认信任应用发布者及其后续更新。

什么是 App(应用模板)

App = 工作空间的形态。它描述了:

  • 侧边栏布局:显示/隐藏哪些内置面板(任务、知识、变更等),追加哪些自定义面板
  • 面板组件:自定义面板的 UI 是什么样、放哪些数据
  • 文案重命名:把「知识」改为「代码」、把「任务」改为「跟进」之类
  • 元数据:名称、版本、图标、作者、分类

一个 App 可被多个工作空间同时绑定,每个空间独立拥有自己的数据,但 UI 形态完全一致。

App vs Plugin 的区别

Knodo 平台现有两个扩展机制,分工清晰:

维度Plugin(插件)App(应用模板)
解决什么AI 能做什么空间长什么样
组成Agent 角色、Skill 技能、命令、Hook面板、侧边栏、布局
绑定关系多对多(一个空间装多个插件)一对一(一个空间用一个 App)
运行时AI 对话期间调用打开空间就生效

两者独立运作:一个基于「销售 CRM」App 创建��空间,可以同时装 Javis 插件跟 AI 对话、装 Pagecraft 插件生成页面。

内置 App

平台预置两个 App 模板:

  • 通用(general):默认布局——知识库 + 任务 + 对话三栏,大多数场景开箱即用。老工作空间自动绑定此 App
  • 应用模板开发(app-dev):平台内 AI 辅助开发应用模板的专用模板。在空白工作空间的设置中切换到此 App 后,一句话描述需求就能生成面板 → 实时预览 → 发布到应用模板库。详见应用模板开发章节

使用流程

1. 发现和使用

从左侧导航进入 能力中心 → 应用。页面内分为两个 Tab:

  • 自建应用:本组织上传或发布的组织级应用,默认已对当前组织可用,不需要再安装。上传入口「上传组织应用」也在这个 Tab 内。
  • 应用市场:平台内置应用和可安装的公开应用。平台内置应用(如「通用」「应用模板开发」)会标记为「平台内置 / 默认可用」,不展示安装按钮;非内置市场应用需要先安装到当前组织,才可被本组织工作空间使用。市场列表只展示当前组织自己的安装状态,不展示其他组织的安装数量。

组织管理员或应用创建者可以打开 App 详情,可以看:

  • 面板定义:这个 App 有哪些自定义面板
  • API 代理:这个 App 是否声明了可被面板调用的 App API Proxy endpoints

详情页中的编辑、重新上传 ZIP 等管理操作只对当前组织内有权限的成员展示。公开应用来自其他组织时,可以查看和安装到当前组织,但不会显示这些管理按钮。

2. 为现有空间切换 App

空白新建工作空间时不选择应用模板,默认使用「通用」布局;基于空间模板创建时,新空间会继承该空间模板绑定的 App。创建完成后,可进入空间 → 设置 → 应用模板 Tab 切换:

  • 看到当前绑定的 App 信息
  • 点 「切换模板」 展开选择器
  • 确认后侧边栏立即按新 App 配置重新渲染
  • 空间内容(知识库、任务、对话记录)不受影响,只是「外壳」变了

切换APP模板

3. App 布局生效规则

切换 App 后:

  • 侧边栏、内置面板隐藏、面板重命名、自定义面板按 App manifest 渲染
  • 空白创建且未切换 App 的空间使用内置「通用」布局
  • 基于空间模板创建的空间默认继承模板绑定的 App
  • 工作空间创建流程保持轻量,手动 App 选择集中在空间设置里完成

应用模板开发

为什么这么做

传统做法开发��个前端应用要:搭脚手架 → 选组件库 → 写代码 → 调样式 → 打包 → 部署。对非程序员用户,这条路基本走不通。

Knodo 的做法是把开发环境做成一个工作空间,让 AI 完成所有技术细节:

  • 知识库(= 代码目录):AI 把源码写在这里
  • AI 对话:你用自然语言提需求、改样式、加功能
  • 预览面板:浏览器端编译 + 实时预览,所见即所得
  • 一键发布:AI 自动打包上传

整个过程你只需要:想清楚要什么 + 看预览说满意还是不满意。

创建开发空间

先新建一个空白工作空间。新建流程不需要选择应用模板,创建完成后进入空间 → 设置 → 应用模板 → 切换为 「应用模板开发」。

进入后左栏是 预览 面板,默认展开;还能看到 代码 面板(其实是知识库,专门为此模板重命名)。

创建开发空间

跟 AI 说需求

在对话里描述你要做什么,越具体越好:

/javis:app-developer 做一个电影收藏管理应用,要能新增电影(名字、年份、评分)、按年份筛选、按评分排序

AI 会:

  1. javis 插件内置了一个 app-developer 的 skill,他知道如何构建应用模板
  2. 根据需求推导 slug(例如 movies)
  3. 写 apps/movies/app.yaml 定义面板和布局;需要开发预览专用配置时再补 apps/movies/app.dev.yaml
  4. 写 apps/movies/src/panels/movies.tsx 实现 UI 逻辑;需要时再补 apps/movies/src/libs/** 或 *.module.css
  5. 保存文件

几秒后 预览 面板会自动编译、自动挂载你的组件。

如果 App 使用第三方 npm 依赖,预览默认从 https://esm.sh 获取。默认 CDN 不可用 或需要使用兼容的内部源时,可以在 apps/<slug>/app.dev.yaml 顶层配置:

cdnBase: https://esm.sh

该地址必须使用 HTTPS,且服务需兼容预览编译器的依赖 bundle 请求。此配置仅影响 开发预览,不进入发布包;正式发布仍会在 sandbox / local agent 中安装依赖并编译。 https://bytepack.coze.cn 曾作为默认源使用,目前对浏览器跨域请求有限制;如果后续 恢复,可以通过 cdnBase 显式切换回该地址。

预览模式

预览面板顶部提供实时编译和预览操作:

操作效果
重新编译手动重新编译当前 App 源码,刷新预览结果
暂停实时编译临时停止源码变更后的自动编译,适合连续修改时使用
独立预览workspace App 按现有预览能力渲染;standalone App 加载 src/index.tsx

预览模式

预览模式下右下角有 「返回开发」 悬浮球,双击 ESC 也能退出。即使 AI 写出了死循环代码,逃生球一样能点。点击「返回开发」后会自动回到预览面板。

独立预览按 App 类型加载对应的预览入口:

  • 空间应用:沿用当前空间应用的多 Panel 预览能力。空间详情资源分类和资源合并逻辑暂不在本次预览能力中额外模拟。
  • 独立应用:以 src/index.tsx 为唯一界面入口,Logo、布局、菜单和路由全部由 App 自己控制;views 只用于根应用按需加载 Panel,不会被平台自动转译成菜单。

预览模式

迭代优化

看到预览后,可以继续跟 AI 说:

把电影卡片的背景改成渐变色

加一个「已看过」的筛选条件

把评分数字字体放大一点

AI 会 Read → Edit 现有文件。由于 AppPreview 监听了源码变化,保存后自动重新编译刷新。

样式

默认有两档方式:

  • 基础模式:直接在 TSX 里写 className,配合 Tailwind utility class。大多数场景够用,也是兼容性最稳的方式
  • 增强模式:在当前环境已启用时,使用 *.module.css 做局部样式抽离,并可在 App 根容器上定义局部 CSS 变量

增强模式的边界:

  • 只支持 apps/<slug>/src/panels/**、apps/<slug>/src/libs/** 下的 *.module.css
  • 不支持普通 .css
  • 不支持 :global、html、body、:root、.dark
  • App 根容器变量只保证作用于当前 App 的非 Portal 子树

发布

满意后点 预览 工具栏的 「发布」 按钮。会弹出确认对话框,点击确认后:

  • 指令会自动发到对话框(你不需要手动打字)
  • AI 执行 app-developer Skill 的 pack-and-upload.sh 脚本
  • 脚本用 build-panels.mjs 编译 TSX、校验 import / CSS 规则 → 打 ZIP → 直接调 POST /api/v1/apps
  • 返回新 App ID

完成后:

  • 此 App 出现在 能力中心 → 应用 → 自建应用 Tab 里
  • 有权限的成员可以在空间设置里把工作空间切换到此 App

外部开发 + 手动上传

如果你有现成的应用包 ZIP(含 app.yaml + panels/*.js,必要时带 sibling panels/*.css),在 能力中心 → 应用 → 自建应用 → 上传组织应用 上传即可。上传后的组织应用默认已对当前组织可用,不需要经过应用市场安装。

参考示例:apps/app-plugins/customer-management/(源码结构 + 打包脚本)。

可见性与管理

可见性管理

可见性谁能看到典型用途
私有(PRIVATE)仅创建者自用的个人工具、开发中未稳定的版本
组织内(INTERNAL)同组织成员团队内部复用的标准应用
公开(PUBLIC)所有组织平台级模板、合作伙伴开放的公共应用

上传后,当前组织内的组织管理员或应用创建者可在 能力中心 → 应用 → 某个 App 详情 → 编辑 中修改可见性。来自其他组织的公开应用不会展示编辑和重新上传入口。

组织安装与内置应用

  • 自建应用:属于当前组织,上传或发布后自动可用,主要通过「自建应用」Tab 管理。
  • 应用市场应用:来自公开市场,组织管理员需要先在「应用市场」Tab 点击「安装」,安装后本组织工作空间才可选择和绑定。普通成员可以查看安装状态,但安装、启用、禁用和卸载按钮会置灰,并提示联系组织管理员处理。市场中只展示当前组织的安装状态,不展示该应用被多少其他组织安装。
  • 平台内置应用:例如「通用」和「应用模板开发」,展示在「应用市场」Tab 中,但标记为「默认可用」,不需要安装,也不能卸载。

覆盖发布(迭代已有 App)

同一个 slug 的 App 再次发布时,系统会判断:

  • 你是原创建者 → 弹出确认对话框,提示会覆盖当前版本,所有绑定此 App 的工作空间会立即使用新版。你点「覆盖」后完成更新
  • 你不是原创建者 → 拒绝覆盖,必须修改 slug 后重新发布(或联系原作者)

⚠️ 覆盖发布会立即影响所有绑定此 App 的工作空间 —— 如果跨团队共用,建议版本号同步升级,并提前告知相关成员

使用应用模板开发空间修改后再发布时,如果是同一个 slug,AI 会先发一次请求 → 后端返回 409 冲突 → AI 问你是否覆盖。你确认后 AI 再发一次带覆盖标识的请求,成功后告诉你「已覆盖更新」。

权限保护:只有原作者能覆盖。如果你在别人的空间里发布一个同 slug 的 App,会被后端拒绝(403),需要你改 slug 重试。

删除 App

在 能力中心 → 应用 → 自建应用 删除后:

  • 如果当前没有空间使用此 App → 直接删除
  • 如果有空间使用此 App → 弹出确认对话框,列出受影响的空间。确认后:
    • App 被删除
    • 这些空间的 appId 自动降级为「通用」App
    • 空间内容完全保留(只是失去了自定义面板,回到默认布局)

面板可用的平台能力

除了纯 React 组件能力,面板还可以调用平台暴露的三类能力做更复杂的事。

1. @knodo/api — 调用平台前端行为

用在「面板想让 AI 帮我做事」这类场景。这些能力不是后端 API(没有 HTTP endpoint),必须走 @knodo/api。

方法作用
chat.send(text, options?)把一段文本直接作为用户消息发送到当前或指定对话 Tab,AI 立即开始回复
chat.fill(text, options?)把文本填进当前或指定对话 Tab 的输入框,留给用户修改和确认
chat.open()打开工作空间右侧对话面板
chat.close()关闭工作空间右侧对话面板
chat.getPanelState()读取右侧对话面板当前是否可见、是否可打开、当前布局模式
chat.subscribePanelState(listener)订阅右侧对话面板状态变化,返回取消订阅函数
app.preventUnload.enable(options?)开启离开保护,适合长任务运行中使用
app.preventUnload.disable()关闭离开保护,任务完成、失败或取消后调用
app.preventUnload.isEnabled()查询当前是否已开启离开保护

示例:

import { app, chat } from "@knodo/api"; <button onClick={() => chat.send("帮我分析这条订单异常")}>交给 AI 分析</button>; <button onClick={() => chat.open()}>打开对话</button>; <button onClick={() => chat.close()}>关闭对话</button>;

这些方法同步触发且没有返回值。send / fill 不会自动改变对话面板的开关状态;需要展示右侧对话区时,先调用 chat.open()。open / close 只控制可见性,不会创建、切换或关闭对话 Tab,也不会清空会话状态。

自定义面板需要展示打开/收起图标时,通过状态 API 判断当前状态:

import { useEffect, useState } from "react"; import { chat } from "@knodo/api"; function ChatPanelToggle() { const [state, setState] = useState(() => chat.getPanelState()); useEffect(() => chat.subscribePanelState(setState), []); return ( <button disabled={!state.canOpen} onClick={() => (state.open ? chat.close() : chat.open())} > {state.open ? "收起对话" : "打开对话"} </button> ); }

chat.getPanelState() 返回 { open, canOpen, mode, reason }。open 表示对话区当前是否对用户可见,canOpen=false 表示宿主不允许打开,reason 可能是 manifest-hidden 或 fullscreen-preview。chat.subscribePanelState(listener) 会在订阅时立即回调一次当前状态,并在后续状态变化时继续通知。

在移动端,应用预览和对话区不是并排布局:chat.open() 会关闭覆盖在对话区上的应用预览抽屉,chat.close() 不会隐藏移动端主对话区。如果 App manifest 已通过 layout.sidebar.hide 隐藏 chat,面板不能用 chat.open() 绕过该配置。

长任务的离开保护是运行时能力,不写在 app.yaml。面板在任务开始时调用 app.preventUnload.enable(),任务完成、失败或取消后调用 app.preventUnload.disable()。开启后会保护浏览器刷新、关闭标签页/浏览器,以及切换空间详情菜单、切到其他 App 面板、关闭移动端 App 预览层等应用内离开动作。浏览器刷新/关闭的确认文案由浏览器控制;应用内离开确认可通过 message 自定义。

import { app } from "@knodo/api"; async function runLongTask() { app.preventUnload.enable({ message: "任务还在运行,确定离开吗?" }); try { await doLongTask(); } finally { app.preventUnload.disable(); } }

2. @knodo/ui — 复用平台组件

有些组件实现极复杂,自己写不划算。平台把它们以 @knodo/ui 形式对面板开放。

当前可用说明
ChatInterface完整的 AI 对话窗口。包含消息列表、流式渲染、命令选择、文件上传、模型切换等。面板可以直接嵌入用

示例(把对话嵌进自己的面板):

import { ChatInterface } from "@knodo/ui"; export default function MyPanel({ workspaceId }) { return ( <div className="flex h-full flex-col"> <header>...我的业务内容...</header> <div className="flex-1"> <ChatInterface workspaceId={workspaceId} welcomeMessage="💡 可以问我任何关于这份数据的问题" hideFileSelector /> </div> </div> ); }

@knodo/ui 会随着平台迭代逐步增加新组件(看板、Markdown 查看器等)。

3. 直接 fetch 后端 API

面板运行在 same-origin,所有 /api/v1/... 接口都能用标准 fetch 调用,凭证自动携带:

fetch(`/api/v1/workspaces/${workspaceId}/tasks`, { credentials: "include", }).then((r) => r.json());

注意事项

  • 面板跑在平台主 React 树里,不是 iframe。基础主题 token 会在非 Portal 子树中自动继承;高级场景可在启用后使用 *.module.css 和 App 根容器 CSS 变量
  • @knodo/api 和 @knodo/ui 的类型提示需要编辑器支持,AI 直接写就行
  • 所有能力都有版本承诺:@knodo/api 和 @knodo/ui 的破坏性变更会通过 Skill 文档和发版说明告知
  • 不要尝试绕过去 import @/...,会编译失败
  • 不要指望 App 根容器变量自动影响 Dialog、Popover、Tooltip 等 Portal 浮层

对接自定义后端服务

App 模板可以在选中的 App 根目录里的 app.yaml 中声明自定义后端端点,让面板通过 Knodo 的 App API Proxy 调用外部服务。面板只调用平台注入的 apiBaseUrl,不要手写平台代理 URL。

api: endpoints: - path: /customers methods: [GET, POST] targetUrl: https://crm.example.com/customers
interface PanelProps { workspaceId: string; appSlug?: string; apiBaseUrl?: string; } export default function CustomersPanel({ apiBaseUrl }: PanelProps) { async function loadCustomers() { if (!apiBaseUrl) return; const response = await fetch(`${apiBaseUrl}/customers`, { credentials: "include", }); return response.json(); } return <button onClick={loadCustomers}>加载客户</button>; }

代理会剥离浏览器传入的 Cookie、raw Authorization 和伪造的 Javis-* 头,只向自定义后端注入服务端生成的可信上下文:

Header说明
Javis-Login-User-Email当前登录用户邮箱
Javis-Login-User-Id当前登录用户 ID
Javis-Organization-Id当前工作空间所属组织
Javis-Workspace-Id当前工作空间 ID

如果扩展后端还需要 elevoOrgId / elevoUserId,不要继续扩展 App Proxy 透传头;改为调用 GET /api/v1/organizations/{organizationId}/member-access/elevo-identity?userId=...,把 Knodo 的组织 ID 和用户 ID 映射到 elevoOne 身份。这个接口属于 App Token 管理类,不需要用户级 JWT。

开发预览态会读取当前工作空间选中的 App 根目录里的 app.dev.yaml(优先)或 app.yaml 草稿,不要求 App 已发布或绑定;发布运行态要求 App 已发布并绑定到当前工作空间。

如果自定义后端还需要反向调用 Knodo API,可以使用 M2M API 密钥获取受控 Token。对于代表具体用户的调用,后端先获取 App Token,再通过 Token Exchange 换取单组织 User JWT;如果只需要查 App Token 管理类接口,则直接使用 App Token 即可。详细凭证创建、App Token 管理类接口和用户身份 Token Exchange 流程见平台级应用:认证对接。

技术规范

文件约定

AI 生成的文件结构(你能在「代码」面板里看到):

<workspace-root>/ ├── apps/ │ └── <slug>/ │ ├── app.dev.yaml # 开发预览配置(可选) │ ├── app.yaml # App Manifest(必需) │ ├── package.json # npm 依赖声明(可选) │ ├── src/ │ │ ├── panels/ │ │ │ ├── <view-key-1>.tsx │ │ │ ├── <view-key-1>.module.css │ │ │ └── <view-key-2>.tsx │ │ └── libs/ # 共享组件 / hooks / utils(可选) │ └── dist/ # 打包产物(脚本生成,无需手动编辑) │ └── <slug>.zip ├── docs/ # 共用文档(不打包) └── ...

严格命名约定

view key ⇄ entry 文件名 ⇄ 源码文件名 必须三者对齐:

  • app.yaml 的 views.<key>.entry = ./panels/<key>.js(发布后的路径)
  • 源码 apps/<slug>/src/panels/<key>.tsx
  • 如果启用了 CSS Modules,构建后可能还会生成 ./panels/<key>.css 作为样式 sidecar;这是产物文件,不是源码入口
  • 已发布 App 会尝试加载同名 CSS sidecar;旧 App 没有 ./panels/<key>.css 时继续只加载 JS,不会白屏

违反约定会导致预览报"源文件不存在"。AI 会严格遵守这个约定。

app.yaml 约束

name: 客户管理 slug: crm version: 1.0.0 description: CRM 客户关系管理 category: CRM views: customers: title: 客户列表 type: component entry: ./panels/customers.js api: endpoints: - path: /customers methods: [GET, POST] targetUrl: https://crm.example.com/customers layout: sidebar: hide: [tasks, changes] # 隐藏的内置面板 rename: files: 资料 # 把"知识"重命名为"资料" panels: - key: customers title: 客户列表 icon: Users showInHeader: true # 可固定到空间 Header 快捷入口
字段说明
name必填
slug必填,小写字母 + 数字 + 连字符
version必填,x.y.z 格式
views必填,至少一个面板定义
api.endpoints可选,自定义后端代理端点列表,详见对接自定义后端服务
layout.sidebar.hide可选,要隐藏的内置面板 ID 数组
layout.sidebar.rename可选,内置面板重命名映射(如 { files: 代码 })
layout.sidebar.panels[].showInHeader可选,对所有自定义面板生效;设为 true 时固定展示在空间 Header 快捷入口

showInHeader 用于把高频自定义面板平铺到空间详情页右上角的资源快捷入口,例如「数据看板」「客户列表」这类用户经常打开的面板。未设置时,自定义面板仍会出现在资源「+」菜单中;UnifiedTabManager 内容区右上角保持统一的「+」下拉入口,不会因为 showInHeader 额外平铺。

api.endpoints 中每一项包含:

字段说明
path面板调用的代理路径,必须以 / 开头
methods允许转发的 HTTP 方法,如 [GET, POST]
targetUrl自定义后端目标地址,必须是 http 或 https URL

发布后,这份 manifest + 编译好的面板资产(panels/<key>.js,必要时带 panels/<key>.css sibling CSS)会被平台注册到应用模板库,供组织内/外成员复用。运行时会尝试加载同名 CSS sidecar;旧 App 没有 CSS 文件时仍按原有 JS 面板加载。

隐藏 / 重命名内置面板

默认平台空间有 7 个内置面板:

ID默认文案说明
tasks任务任务管理
files / knowledge知识知识库文件树(两个 ID 互为别名)
changes变更Git 变更视图
views分析数据视图
automation自动化定时任务
guideline指引工作指引
chat——右侧对话区

你可以告诉 AI:「不要任务和变更面板」→ AI 会在 manifest 加 hide: [tasks, changes]。也可以重命名:「把知识改名为资料」→ AI 加 rename: { files: 资料 }。完全沉浸式应用(只留一个面板)也可以:「只保留我的面板,其他全部隐藏,包括对话区」。

隐藏面板

TSX 文件约束

AI 生成的面板组件需要满足:

  • export default 导出 React 组件
  • 组件接收平台注入的运行时 props:
    • workspaceId:当前工作空间 ID,必定存在
    • executionEnvironmentId:当前选中的执行环境绑定 ID,可选;未提供时表示工作空间主目录
    • appId:当前 App ID,预览未发布时可能为空
    • appSlug:当前 App slug
    • appName:当前 App 名称
    • apiBaseUrl:当前 App 在当前工作空间下的 API Proxy 基础路径;开发预览态会读取当前选中的 App 根目录里的 app.dev.yaml(优先)或 app.yaml 草稿,发布运行态会读取已发布并绑定的 App 配置
  • 可以 import react(Hook 都能用)
  • 可以 import 下列平台能力(详见面板可用的平台能力):
    • @knodo/api:发送或预填对话消息,以及打开或关闭右侧对话面板
    • @knodo/ui:复用平台桥接组件(目前稳定开放:ChatInterface)
    • 可以直接 fetch 任何 /api/v1/... 后端接口(same-origin 自动带凭证)
    • 不能 import 平台私有模块(@/components/...、@/stores/... 等)
  • Tailwind 类名可用
  • 在当前环境已启用时,可以 import *.module.css 并与 className 混用
  • 普通 .css 不可用;也不要写 :global、html、body、:root、.dark

示例:

interface PanelProps { workspaceId: string; executionEnvironmentId?: string; appId?: string; appSlug?: string; appName?: string; apiBaseUrl?: string; } export default function CustomersPanel({ apiBaseUrl }: PanelProps) { if (!apiBaseUrl) { return <div>当前 App 未提供 API Proxy 地址</div>; } // API Proxy 基础路径由平台注入,面板无需手动拼接 appSlug。 fetch(`${apiBaseUrl}/customers`, { credentials: "include" }); return <div>客户列表</div>; }

读取当前执行环境中的文件

工作空间选择执行环境后,平台会把对应的 executionEnvironmentId 注入自定义面板。面板不需要自行查询当前选择,也不要把该值当作 Host ID;它是当前工作空间下不透明的执行环境绑定 ID。

调用支持执行环境的文件接口时,应把该参数原样透传。未提供该参数时,接口继续读取工作空间主目录:

interface PanelProps { workspaceId: string; executionEnvironmentId?: string; } export default function ArtifactPanel({ workspaceId, executionEnvironmentId, }: PanelProps) { async function readFile(filePath: string) { const query = new URLSearchParams({ filePath, purpose: "preview" }); if (executionEnvironmentId) { query.set("executionEnvironmentId", executionEnvironmentId); } const response = await fetch( `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/files/content?${query}`, { credentials: "include" }, ); if (!response.ok) throw new Error(`读取文件失败:HTTP ${response.status}`); return response.text(); } return <button onClick={() => void readFile("spec.md")}>读取 spec.md</button>; }

目录和搜索接口使用相同参数:

  • 文件列表:GET /api/v1/workspaces/{workspaceId}/files?prefix=...&executionEnvironmentId=...
  • 文件搜索:GET /api/v1/workspaces/{workspaceId}/files/search?keyword=...&executionEnvironmentId=...
  • 文件正文:GET /api/v1/workspaces/{workspaceId}/files/content?filePath=...&purpose=preview&executionEnvironmentId=...

注意:

  • executionEnvironmentId 可能在面板生命周期内变化;请求和缓存应把它作为文件上下文的一部分。
  • 不要把执行环境 ID 写死或持久化为 App 配置。始终使用宿主本次注入的值。
  • 前端透传不代表获得额外权限,后端仍会校验当前用户、工作空间和执行环境绑定关系。

开发预览态和发布运行态的代理入口不同,但面板不需要感知差异:

  • 开发预览态:平台注入 /api/v1/app-preview/workspaces/{workspaceId}/apps/{appRootId}/proxy
  • 发布运行态:平台注入 /api/v1/apps/{appSlug}/proxy/workspaces/{workspaceId}

开发预览态不要求 App 已发布或绑定到当前工作空间;发布运行态仍要求 App 已发布并绑定。

适用场景

适合做工作空间内嵌业务工具:

  • 数据看板(内嵌 echarts / recharts 后可视化)
  • 清单工具(待办、笔记、收藏)
  • 表单类应用
  • 轻量 CRM / 跟进工具
  • 文档展示 / 知识库门户
  • 通过 App API Proxy 对接自定义后端服务的业务面板

目前不适合做的:

  • 需要重度 UI 组件库的应用(暂不共享 shadcn/ui,但可以用 Tailwind + 原生 HTML)
  • 强依赖全局 CSS 覆盖或复杂 Portal 主题联动的应用(当前只保证局部 CSS Modules 和非 Portal 子树变量)

这些会在后续迭代中放开。

常见问题

Q: 预览报"未找到面板组件"?

A: 大概率是 view key 和源码文件名不对齐。让 AI 检查 app.yaml 里 views.<key>.entry 是否等于 ./panels/<key>.js,对应源码是否在 apps/<slug>/src/panels/<key>.tsx。

Q: 预览报"编译失败"?

A: AppPreview 顶部会显示编译错误,点击具体 view 的「失败」badge 看详细错误行。把错误原文发给 AI,让 AI 修。

Q: 面板里能发消息给对话区吗?

A: 能。import { chat } from '@knodo/api'; chat.send('你的消息') 就会把消息发到当前激活的对话 Tab。如果你只想预填入输入框让用户确认,用 chat.fill('...')。

Q: 面板里能打开或关闭右侧对话区吗?

A: 能。调用 chat.open() 打开,调用 chat.close() 关闭。它们只改变右侧对话面板的可见性,不会关闭 Conversation Tab 或清空对话状态。需要显示打开/收起图标时,用 chat.getPanelState() 读取当前状态,或用 chat.subscribePanelState(listener) 持续同步状态。移动端调用 chat.open() 会退出应用预览并回到对话区,chat.close() 不隐藏移动端主对话区;manifest 已隐藏 chat 时不能强制打开。

Q: 面板执行长任务时能防止误关闭或误切换吗?

A: 能。任务开始时调用 app.preventUnload.enable(),任务完成、失败或取消后调用 app.preventUnload.disable()。不要写成 app.yaml 的固定面板配置,否则面板空闲时也会打扰用户。

Q: 面板里能调用平台后端 API 吗?

A: 可以,直接 fetch('/api/v1/...', { credentials: 'include' }),凭证会自动带,后端按当前登录用户鉴权。

Q: 面板如何读取当前执行环境里的文件?

A: 从平台注入的可选 executionEnvironmentId prop 获取当前执行环境绑定 ID,并在文件列表、搜索或正文请求中原样透传。该 prop 为空时读取工作空间主目录;用户切换执行环境后 prop 会更新,面板应按新的文件上下文重新加载。

Q: 发布失败 409 但我是作者?

A: 重启 backend 让 DTO 字段生效(overwrite 字段是新加的)。正常情况下会弹确认对话框让你点「覆盖」。

Q: 预览模式下页面崩了,怎么回去?

A: 右下角逃生悬浮球永远可点,或者双击 ESC。

Q: 发布的 App 别人看不到?

A: 检查可见性:默认是 PRIVATE(只有你自己)。在能力中心的应用详情里编辑可见性为 INTERNAL(组织内)或 PUBLIC(跨组织)。公开 App 可被其他组织看到并安装,但其他组织不会看到编辑、重新上传 ZIP 等归属组织管理入口。

服务端编译与预览编译错误参考(spec 192)

微应用的预览与发布编译已收敛到平台后端的统一编译器(backend 常驻编译服务,预览与发布共用同一套规则)。预览时前端把面板/库源码一次提交给编译 API,服务端用原生 esbuild 编译后内联返回产物——不再逐依赖请求公网 CDN,大应用首屏更快,且预览与发布产物保证一致("预览好的,发布出来也是好的")。

该能力由平台开关(appCompile.previewEnabled)灰度控制:开关关闭时预览走旧的浏览器编译路径,行为不变。

编译规则

  • 面板源码位于 src/panels/、共享代码位于 src/libs/;面板之间不能互相引用,共享逻辑放 src/libs/
  • src/libs/ 中的代码不能反向引用 src/panels/
  • 应用代码可以引用 src/ 目录内的任何文件(src/panels、src/libs、src/utils、src/components 等均可),但不能逃逸到应用 src/ 之外
  • 样式只能使用 CSS Modules(*.module.css);不支持普通 .css 与 :global/:root/html/body/.dark 等全局选择器
  • @knodo/ui 只能整包导入(import { X } from '@knodo/ui'),不支持子路径导入
  • npm 依赖必须在 package.json dependencies 中声明,版本必须为精确版本(x.y.z),数量 ≤20 个
  • 单文件 ≤200KB、源码总量 ≤4MB、文件数 ≤500

编译错误参考(ruleId → 文案 → 修复建议)

ruleId错误文案修复建议
PANEL_CROSS_IMPORT面板之间不能互相引用把共享代码提取到 src/libs/ 后双方引用它
LIBS_IMPORT_PANELlibs 不能引用 panels修正依赖方向:只能是 panels → libs
SRC_BOUNDARY_ESCAPE不允许引用 src/panels 与 src/libs 之外的文件将共享代码收进 src/libs/
PLAIN_CSS_IMPORT暂不支持普通 .css改用 *.module.css 样式模块
UI_SUBPATH_IMPORT不支持 @knodo/ui 子路径导入使用 import { 组件名 } from "@knodo/ui"
CSS_GLOBAL_SELECTORCSS Modules 不支持全局选择器只在组件局部类名内定义样式与 CSS 变量
UNDECLARED_BARE_IMPORT依赖未在 dependencies 中声明先在 package.json 声明精确版本再使用
MODULE_NOT_FOUND模块未找到检查导入路径
LOCKFILE_MISMATCHlockfile 与 dependencies 不一致重新 npm install 生成 package-lock.json 后提交
LIMIT_DEPENDENCY_COUNT 等 LIMIT_*各限额超限按文案提示精简依赖/文件/体积

预览行为要点

  • 编辑源码后约 2 秒内自动重新编译刷新(热路径实测 p95 <300ms)
  • 编译失败时预览保留上一次成功的产物(不白屏),错误信息按上表文案展示
  • 受限网络/内网环境:只要平台后端可达配置的 npm registry,预览与发布即可用(registry 为部署配置,可指向内网镜像)

相关文档