v2 platform runtime (docs 22 Phase 2/4/7 + 17): Project/Environment + Config + Storage + Observability

- Project/Environment model with default environments (dev/staging/prod)
- Config Runtime: cascade resolution (platform→org→project→env), secrets via vault
- Storage Runtime: local provider, sha256 dedup, integrity check, file list/upload/delete
- Observability: execution query, per-action summary (success rate, avg, p95), health
- Realtime SSE: workspace event stream endpoint (GET /api/v1/v2/realtime/events)
- Phase 1 acceptance: core.health.check, core.user.create, core.user.list
- Actions: core.project.*, core.config.*, core.file.*, core.user.* (62 total)
This commit is contained in:
phptits
2026-09-11 21:11:07 +00:00
parent 72012a7da3
commit 851ccf1d7f
28 changed files with 87763 additions and 0 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,586 @@
# 20_UI_SHELL_FRONTEND_RUNTIME.md
# DevOS UI Shell / Frontend Runtime
> DevOS 前端不是一个普通 Admin Dashboard。
>
> 它应该是一个可扩展的 **Application UI Shell + Resource UI Runtime + Plugin UI Runtime + Agent UI Runtime**。
>
> Plugin、MCP、Agent、Task、Resource、Action、Workflow 都必须能够以统一方式进入 DevOS UI,而不能各自重新实现 Layout、Router、Permission、Menu、Settings、Table、Detail、Form、Task、Notification、Realtime 等基础设施。
---
# 1. 目标
DevOS UI Runtime 必须解决以下问题:
```text
一个 DevOS
↓
多个 Plugin
↓
多个 Resource
↓
多个 Action
↓
多个 Agent / Skill
↓
多个 Task / Workflow / MCP
```
最终用户看到的应该仍然是:
```text
┌─────────────────────────────────────────────────────┐
│ DevOS │
├───────────────┬─────────────────────────────────────┤
│ Navigation │ Main Content │
│ │ │
│ Dashboard │ Resource / Task / Agent / Plugin │
│ Projects │ │
│ Resources │ │
│ Tasks │ │
│ Agents │ │
│ Plugins │ │
│ Settings │ │
└───────────────┴─────────────────────────────────────┘
```
而不是:
```text
Plugin A
└── 自己的 Layout
└── 自己的 Router
└── 自己的 Permission
Plugin B
└── 自己的 Layout
└── 自己的 Router
└── 自己的 Permission
```
DevOS 必须提供统一:
- App Shell
- Router
- Navigation
- Permission
- Resource UI
- Action UI
- Table
- Form
- Detail
- Search
- Filter
- Task UI
- Agent UI
- Plugin UI
- Settings
- Notification
- Realtime
- Modal / Drawer
- Command Palette
- Global Search
- Breadcrumb
- Page State
- Error Boundary
- Loading
- Empty State
- Responsive
- Theme
- i18n
---
# 2. 核心原则
## 2.1 UI 不拥有业务逻辑
禁止:
```text
React Component
↓
直接调用数据库
```
禁止:
```text
React Component
↓
直接调用第三方 API
```
禁止:
```text
Plugin Page
↓
自己实现权限
```
正确:
```text
UI
↓
Query / Action
↓
API
↓
DevOS Runtime
↓
Resource / Action Runtime
```
---
# 3. UI 总体架构
```text
DevOS UI
│
▼
┌────────────┐
│ UI Shell │
└─────┬──────┘
│
┌───────────────┼────────────────┐
▼ ▼ ▼
Navigation Router Global UI
│ │ │
▼ ▼ ▼
Menu Registry Route Registry Search / Cmd
│ │
└───────────────┼────────────────┐
▼ ▼
Resource UI Plugin UI
│ │
▼ ▼
Action UI Agent UI
│ │
└───────┬────────┘
▼
API / Runtime
```
---
# 4. Frontend Package Architecture
推荐:
```text
packages/
├── ui/
├── ui-shell/
├── ui-router/
├── ui-navigation/
├── ui-resource/
├── ui-action/
├── ui-form/
├── ui-table/
├── ui-detail/
├── ui-search/
├── ui-task/
├── ui-agent/
├── ui-plugin/
├── ui-settings/
├── ui-notification/
├── ui-realtime/
├── ui-command/
├── ui-theme/
├── ui-i18n/
└── frontend-runtime/
```
应用:
```text
apps/
└── web/
├── src/
│ ├── bootstrap/
│ ├── app/
│ ├── routes/
│ ├── providers/
│ └── main.tsx
└── package.json
```
Plugin:
```text
plugins/
├── seo/
│ └── ui/
├── devops/
│ └── ui/
└── crm/
└── ui/
```
---
# 5. UI Runtime
核心 Runtime:
```ts
export interface UIRuntime {
registerRoute(route: UIRoute): void
registerNavigation(item: NavigationItem): void
registerResource(resource: ResourceUI): void
registerAction(action: ActionUI): void
registerWidget(widget: UIWidget): void
registerPage(page: UIPage): void
registerCommand(command: UICommand): void
registerSettings(settings: SettingsDefinition): void
resolveRoute(path: string): UIRoute | undefined
resolveNavigation(): NavigationItem[]
canRender(target: UIVisibilityContext): Promise<boolean>
}
```
---
# 6. UI Context
所有页面都应该拥有统一 Context:
```ts
export interface UIContext {
user: UserContext
organization?: OrganizationContext
project?: ProjectContext
environment?: EnvironmentContext
permissions: PermissionSet
locale: string
timezone: string
theme: ThemeMode
featureFlags: FeatureFlagSet
}
```
页面不得自行重新获取:
```text
currentUser
currentOrganization
permissions
environment
theme
locale
```
应该统一由 UI Runtime 提供。
---
# 7. Application Shell
Shell 是 DevOS 的最外层 UI。
```tsx
<DevOSApp>
<AuthProvider>
<UIRuntimeProvider>
<ThemeProvider>
<RealtimeProvider>
<NotificationProvider>
<AppShell>
<Navigation />
<MainContent />
</AppShell>
</NotificationProvider>
</RealtimeProvider>
</ThemeProvider>
</UIRuntimeProvider>
</AuthProvider>
</DevOSApp>
```
---
# 8. Shell Layout
建议:
```text
┌──────────────────────────────────────────────────────┐
│ Logo Global Search Command User │
├────────────┬─────────────────────────────────────────┤
│ │ Breadcrumb │
│ Navigation ├─────────────────────────────────────────┤
│ │ │
│ Dashboard │ Page │
│ Projects │ │
│ Resources │ │
│ Tasks │ │
│ Agents │ │
│ Plugins │ │
│ │ │
│ Settings │ │
└────────────┴─────────────────────────────────────────┘
```
支持:
- sidebar collapse
- mobile drawer
- top navigation
- nested navigation
- workspace navigation
- project navigation
- plugin navigation
---
# 9. Navigation Runtime
Plugin 不应该修改主 Layout。
Plugin 只注册 Navigation:
```ts
export interface NavigationItem {
id: string
label: string
icon?: string
path?: string
parentId?: string
order?: number
permission?: PermissionRequirement
featureFlag?: string
badge?: NavigationBadge
children?: NavigationItem[]
}
```
例如 SEO Plugin:
```ts
{
id: "seo.websites",
label: "Websites",
path: "/seo/websites",
parentId: "seo",
permission: {
action: "seo.website.read"
}
}
```
---
# 10. Navigation 不等于 Permission
Navigation:
```text
用户可以看到什么
```
Permission:
```text
用户可以做什么
```
因此:
```text
Navigation Filter
↓
Permission Filter
↓
Scope Filter
↓
Visible Navigation
```
但:
> UI 隐藏按钮不能代替后端授权。
---
# 11. Router Runtime
使用:
```text
React Router
```
统一 Route Registry:
```ts
export interface UIRoute {
id: string
path: string
component: React.ComponentType
parentId?: string
layout?: string
permission?: PermissionRequirement
metadata?: RouteMetadata
}
```
---
# 12. Plugin Route
Plugin 可以注册:
```ts
plugin.registerRoute({
id: "seo.website.list",
path: "/seo/websites",
component: WebsiteListPage,
permission: {
action: "seo.website.read"
}
})
```
但 Plugin 不允许:
```ts
createBrowserRouter(...)
```
Plugin 不允许重新创建:
```text
Router
Shell
Auth
Permission
Theme
Notification
```
---
# 13. Route Metadata
```ts
export interface RouteMetadata {
title?: string
icon?: string
breadcrumb?: BreadcrumbDefinition
keepAlive?: boolean
fullscreen?: boolean
searchable?: boolean
commands?: string[]
featureFlag?: string
}
```
---
# 14. Resource UI Runtime
这是 DevOS UI 的核心。
Resource 定义:
```text
Resource
├── Schema
├── Actions
├── Permissions
├── Relations
└── UI Definition
```
因此 Resource 可以自动生成:
```text
List
Detail
Create
Edit
Delete
Bulk Actions
Search
Filter
Sort
Relation
Timeline
Activity
```
---
# 15. Resource UI Definition
```ts
export interface ResourceUI {
resource: string
label: string
icon?: string
list?: ResourceListDefinition
detail?: ResourceDetailDefinition
form?: ResourceFormDefinition
search?: ResourceSearchDefinition
actions?: string[]
}
```
例如:
```ts
{
resource: "seo.website",
label: "Website
@@ -0,0 +1,761 @@
# 动态 FPS 截图推送模块
## 1. 目标
现有 Browser SSE 已经支持 Screenshot 推送,本次只增加**动态 Screenshot 推送频率**。
核心规则:
- 无操作时:每 **2 秒** 推送一张 Screenshot
- 用户发生浏览器操作后:进入高频模式
- 高频模式:每 **500ms** 推送一张 Screenshot
- 高频模式持续 **5 秒**
- 5 秒的计算基于**最后一次浏览器操作**
- 新操作发生时,只延长高频模式时间
- 操作本身**不立即触发 Screenshot**
- 不使用真正的视频流
- 不使用 WebRTC
- 不使用 VNC
- Screenshot 仍然是独立 JPEG 图片
- Screenshot 仍然通过现有 SSE 推送
---
# 2. 配置
默认配置:
```ts
const screenshotConfig = {
quality: 30,
normalInterval: 2000,
activeInterval: 500,
activeDuration: 5000
}
```
参数:
| 参数 | 默认值 | 说明 |
|---|---:|---|
| `quality` | `30` | JPEG 质量 |
| `normalInterval` | `2000` | 普通模式截图间隔,单位 ms |
| `activeInterval` | `500` | 活跃模式截图间隔,单位 ms |
| `activeDuration` | `5000` | 最后一次操作后保持活跃模式的时间,单位 ms |
所有时间参数统一使用 `ms`。
---
# 3. Screenshot Mode
只有两种状态:
```ts
type ScreenshotMode = 'normal' | 'active'
```
### Normal
```text
interval = 2000ms
```
即:
```text
Screenshot
↓
等待 2 秒
↓
Screenshot
↓
等待 2 秒
↓
...
```
### Active
```text
interval = 500ms
```
即:
```text
Screenshot
↓
等待 500ms
↓
Screenshot
↓
等待 500ms
↓
...
```
---
# 4. 状态
Scheduler 至少维护:
```ts
interface ScreenshotSchedulerState {
mode: 'normal' | 'active'
lastActivityAt: number
activeUntil: number
running: boolean
}
```
说明:
### `mode`
当前截图模式:
```text
normal
active
```
### `lastActivityAt`
最后一次成功 Browser Action 的时间。
### `activeUntil`
Active Mode 的结束时间。
例如:
```text
当前时间:10:00:00
用户操作
↓
activeUntil = 10:00:05
```
---
# 5. 操作触发 Active Mode
任何**成功的浏览器操作**完成后调用:
```ts
notifyActivity()
```
实现:
```ts
function notifyActivity() {
const now = Date.now()
lastActivityAt = now
activeUntil = now + activeDuration
}
```
同时:
```ts
mode = 'active'
```
---
# 6. 操作不立即 Screenshot
这是必须遵守的规则。
错误:
```text
Browser Action
↓
Screenshot
↓
SSE
```
正确:
```text
Browser Action
↓
Action 执行成功
↓
notifyActivity()
↓
切换 Active Mode
↓
Scheduler 按 500ms 推送 Screenshot
```
也就是说:
> **Browser Action 只修改 Screenshot Scheduler 状态,不主动调用 Screenshot。**
---
# 7. Active Mode 生命周期
例如:
```text
00:00
用户 click
↓
Active Until = 00:05
00:00.5 Screenshot
00:01.0 Screenshot
00:01.5 Screenshot
00:02.0 Screenshot
...
00:05.0
↓
恢复 Normal
```
恢复后:
```text
每 2000ms Screenshot
```
---
# 8. 连续操作
如果 Active Mode 中再次发生操作:
```text
00:00 click
↓
activeUntil = 00:05
00:02 fill
↓
activeUntil = 00:07
00:04 scroll
↓
activeUntil = 00:09
```
因此:
> **Active Mode 的 5 秒永远从最后一次操作重新计算。**
最终:
```text
00:04 最后一次操作
↓
保持 Active 到 00:09
↓
恢复 Normal
```
---
# 9. 多次操作不会创建多个 Timer
禁止:
```ts
setInterval(...)
setInterval(...)
setInterval(...)
```
也禁止每次操作创建一个新的 Screenshot Loop。
整个 Browser Session 只允许存在:
```text
1 个 Screenshot Scheduler
```
所有操作统一调用:
```ts
notifyActivity()
```
---
# 10. Scheduler
推荐采用单一循环。
示例:
```ts
class ScreenshotScheduler {
private running = false
private mode: 'normal' | 'active' = 'normal'
private activeUntil = 0
async start() {
if (this.running) {
return
}
this.running = true
while (this.running) {
const now = Date.now()
this.mode =
now < this.activeUntil
? 'active'
: 'normal'
await this.capture()
const interval =
this.mode === 'active'
? this.config.activeInterval
: this.config.normalInterval
await sleep(interval)
}
}
notifyActivity() {
const now = Date.now()
this.mode = 'active'
this.activeUntil =
now + this.config.activeDuration
}
stop() {
this.running = false
}
}
```
实际代码可以根据项目结构调整,但必须保持相同的行为。
---
# 11. Screenshot 执行顺序
Scheduler 每次执行:
```text
检查当前 Mode
↓
Screenshot
↓
SSE 推送
↓
根据当前 Mode 等待
↓
进入下一轮
```
例如 Normal:
```text
Screenshot
↓
wait 2000ms
↓
Screenshot
↓
wait 2000ms
```
Active:
```text
Screenshot
↓
wait 500ms
↓
Screenshot
↓
wait 500ms
```
---
# 12. Screenshot 不允许并发
必须保证同一 Browser Session:
```text
同时最多 1 个 Screenshot
```
禁止:
```text
Screenshot A
↓
还没完成
Screenshot B
↓
还没完成
Screenshot C
```
正确:
```text
Screenshot A
↓
完成
↓
等待 interval
↓
Screenshot B
```
原因:
- 避免 CPU 突增
- 避免内存占用增加
- 避免多个 Screenshot 同时进入 SSE
- 避免页面状态竞争
---
# 13. Browser Action 与 Scheduler 的关系
Browser Controller 在 Action 成功后:
```ts
await executeAction(action)
scheduler.notifyActivity()
```
不要:
```ts
await executeAction(action)
await screenshot()
```
也不要:
```ts
scheduler.start()
```
Browser Session 创建时只启动一次 Scheduler。
---
# 14. Action 失败
Action 失败:
```text
Browser Action
↓
失败
↓
不调用 notifyActivity()
```
因此不会进入 Active Mode。
例如:
```text
click element
↓
Element Not Found
↓
Action Failed
↓
保持 Normal Mode
```
---
# 15. Navigation Action
以下操作成功后同样进入 Active Mode:
```text
goto
reload
back
forward
```
例如:
```text
goto
↓
页面加载完成
↓
notifyActivity()
↓
Active Mode
↓
500ms Screenshot
```
---
# 16. Batch Action
Batch Action 只调用一次:
```ts
scheduler.notifyActivity()
```
例如:
```text
Action 1
↓
Action 2
↓
Action 3
↓
全部成功
↓
notifyActivity()
```
不要:
```text
Action 1 → notifyActivity()
Action 2 → notifyActivity()
Action 3 → notifyActivity()
```
Batch 本身只需要最终刷新一次 Active Mode 时间。
---
# 17. SSE
动态 FPS 不改变现有 SSE API。
仍然:
```http
GET /profiles/:profileId/browser/events
```
Screenshot Event 保持现有格式。
Scheduler 只决定:
> **什么时候生成下一张 Screenshot。**
SSE 只负责:
> **把 Screenshot 推送给前端。**
两者职责保持分离。
---
# 18. SSE State
建议在 `state` event 中增加:
```json
{
"mode": "active",
"normalInterval": 2000,
"activeInterval": 500,
"activeUntil": 1780000000000
}
```
前端可以知道当前 Screenshot Scheduler 状态。
如果现有 `state` 已经定义完成,也可以不修改协议,仅在服务端内部维护状态。
---
# 19. SSE Client 数量
如果当前没有 SSE Client:
```text
SSE Client = 0
```
可以暂停 Screenshot Scheduler。
重新建立 SSE:
```text
SSE Connected
↓
启动 Screenshot Scheduler
↓
发送当前 Screenshot
↓
进入 Normal Mode
```
如果当前项目已经有 Scheduler 生命周期管理,则只增加动态 interval,不修改现有连接管理。
---
# 20. Active Mode 不等于视频流
必须保持:
```text
Active Mode
↓
每 500ms
↓
生成一张 JPEG
```
而不是:
```text
Active Mode
↓
视频编码
↓
持续视频流
```
所以本功能仍然是:
> **Adaptive Screenshot Streaming**
而不是 Live View / Video Streaming。
---
# 21. 推荐默认参数
第一版固定默认:
```ts
{
quality: 30,
normalInterval: 2000,
activeInterval: 500,
activeDuration: 5000
}
```
即:
```text
无操作:
2 秒 / 张
操作后:
500ms / 张
最后一次操作后:
5 秒恢复正常
JPEG:
quality 30
```
后续如果需要,再将这些参数开放为系统配置。
---
# 22. 核心流程
```text
Browser Action
│
▼
Playwright
│
操作成功
│
▼
notifyActivity()
│
▼
Active Mode
interval=500ms
│
▼
Screenshot
│
▼
SSE
│
▼
Console
│
│
500ms 后
│
▼
Screenshot
│
...
│
最后操作 + 5 秒
│
▼
Normal Mode
interval=2000ms
│
▼
Screenshot
│
2 秒后
│
▼
Screenshot
```
---
# 23. 验收标准
- [ ] Normal Mode 默认 2000ms
- [ ] Active Mode 默认 500ms
- [ ] Active Duration 默认 5000ms
- [ ] Browser Action 成功后进入 Active Mode
- [ ] Browser Action 不立即触发 Screenshot
- [ ] Action 失败不进入 Active Mode
- [ ] Active Mode 从最后一次操作重新计时
- [ ] 连续操作不会创建多个 Timer
- [ ] 每个 Browser Session 只有一个 Screenshot Scheduler
- [ ] Screenshot 不允许并发执行
- [ ] Active Mode 结束后自动恢复 Normal Mode
- [ ] SSE 接口保持不变
- [ ] Screenshot 仍然使用 JPEG
- [ ] Screenshot quality 默认 30
- [ ] 不引入 WebSocket
- [ ] 不引入 WebRTC
- [ ] 不引入 VNC
- [ ] 不引入 CDP
- [ ] 不实现视频流
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff