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:
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
Reference in New Issue
Block a user