@zhin.js/client 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -432
- package/client/agent-stream.ts +121 -0
- package/client/app.test.ts +37 -0
- package/client/app.ts +130 -138
- package/client/bootstrap/loadConsoleEntries.test.ts +23 -12
- package/client/bootstrap/loadConsoleEntries.ts +40 -27
- package/client/console-client-context.ts +24 -0
- package/client/console-client.test.ts +46 -0
- package/client/console-client.ts +70 -0
- package/client/console-events.ts +38 -0
- package/client/console-utils/remoteApi.test.ts +91 -0
- package/client/console-utils/remoteApi.ts +47 -9
- package/client/index.ts +92 -28
- package/client/mediaSrc.test.ts +49 -0
- package/client/mediaSrc.ts +84 -11
- package/client/message-content/MarkdownContent.tsx +128 -0
- package/client/message-content/markdown.test.ts +48 -0
- package/client/message-content/markdown.ts +247 -0
- package/client/persistence/idb-store.test.ts +121 -0
- package/client/persistence/idb-store.ts +40 -21
- package/client/segments.test.ts +16 -0
- package/client/segments.ts +39 -0
- package/client/transport/console-transport.test.ts +394 -0
- package/client/transport/console-transport.ts +676 -0
- package/client/transport/hooks.test.ts +21 -0
- package/client/transport/hooks.ts +535 -0
- package/client/transport/index.ts +23 -0
- package/client/{websocket → transport}/types.ts +16 -17
- package/client/types.ts +5 -8
- package/client/workroom/WorkroomRunsPage.tsx +355 -0
- package/client/workroom/registerWorkroomConsole.test.ts +25 -0
- package/client/workroom/registerWorkroomConsole.ts +17 -0
- package/dist/agent-stream.d.ts +17 -0
- package/dist/agent-stream.d.ts.map +1 -0
- package/dist/agent-stream.js +73 -0
- package/dist/agent-stream.js.map +1 -0
- package/dist/app.d.ts +17 -52
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +110 -95
- package/dist/app.js.map +1 -1
- package/dist/bootstrap/loadConsoleEntries.d.ts +10 -7
- package/dist/bootstrap/loadConsoleEntries.d.ts.map +1 -1
- package/dist/bootstrap/loadConsoleEntries.js +37 -11
- package/dist/bootstrap/loadConsoleEntries.js.map +1 -1
- package/dist/console-client-context.d.ts +9 -0
- package/dist/console-client-context.d.ts.map +1 -0
- package/dist/console-client-context.js +13 -0
- package/dist/console-client-context.js.map +1 -0
- package/dist/console-client.d.ts +25 -0
- package/dist/console-client.d.ts.map +1 -0
- package/dist/console-client.js +45 -0
- package/dist/console-client.js.map +1 -0
- package/dist/console-events.d.ts +8 -0
- package/dist/console-events.d.ts.map +1 -0
- package/dist/console-events.js +26 -0
- package/dist/console-events.js.map +1 -0
- package/dist/console-utils/remoteApi.d.ts +2 -1
- package/dist/console-utils/remoteApi.d.ts.map +1 -1
- package/dist/console-utils/remoteApi.js +46 -8
- package/dist/console-utils/remoteApi.js.map +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -8
- package/dist/index.js.map +1 -1
- package/dist/mediaSrc.d.ts +1 -1
- package/dist/mediaSrc.d.ts.map +1 -1
- package/dist/mediaSrc.js +91 -11
- package/dist/mediaSrc.js.map +1 -1
- package/dist/message-content/MarkdownContent.d.ts +11 -0
- package/dist/message-content/MarkdownContent.d.ts.map +1 -0
- package/dist/message-content/MarkdownContent.js +68 -0
- package/dist/message-content/MarkdownContent.js.map +1 -0
- package/dist/message-content/markdown.d.ts +47 -0
- package/dist/message-content/markdown.d.ts.map +1 -0
- package/dist/message-content/markdown.js +219 -0
- package/dist/message-content/markdown.js.map +1 -0
- package/dist/persistence/idb-store.d.ts +5 -2
- package/dist/persistence/idb-store.d.ts.map +1 -1
- package/dist/persistence/idb-store.js +29 -20
- package/dist/persistence/idb-store.js.map +1 -1
- package/dist/segments.d.ts +9 -0
- package/dist/segments.d.ts.map +1 -0
- package/dist/segments.js +35 -0
- package/dist/segments.js.map +1 -0
- package/dist/transport/console-transport.d.ts +217 -0
- package/dist/transport/console-transport.d.ts.map +1 -0
- package/dist/transport/console-transport.js +563 -0
- package/dist/transport/console-transport.js.map +1 -0
- package/dist/transport/hooks.d.ts +253 -0
- package/dist/transport/hooks.d.ts.map +1 -0
- package/dist/transport/hooks.js +483 -0
- package/dist/transport/hooks.js.map +1 -0
- package/dist/transport/index.d.ts +4 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +4 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/{websocket → transport}/types.d.ts +14 -13
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/{websocket → transport}/types.js +4 -9
- package/dist/transport/types.js.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/workroom/WorkroomRunsPage.d.ts +2 -0
- package/dist/workroom/WorkroomRunsPage.d.ts.map +1 -0
- package/dist/workroom/WorkroomRunsPage.js +111 -0
- package/dist/workroom/WorkroomRunsPage.js.map +1 -0
- package/dist/workroom/registerWorkroomConsole.d.ts +4 -0
- package/dist/workroom/registerWorkroomConsole.d.ts.map +1 -0
- package/dist/workroom/registerWorkroomConsole.js +16 -0
- package/dist/workroom/registerWorkroomConsole.js.map +1 -0
- package/package.json +18 -8
- package/client/runtime/index.ts +0 -12
- package/client/websocket/hooks.ts +0 -289
- package/client/websocket/index.ts +0 -6
- package/client/websocket/instance.ts +0 -24
- package/client/websocket/manager.ts +0 -426
- package/client/websocket/remote-settings.ts +0 -25
- package/dist/bootstrap/loadConsoleEntries.test.d.ts +0 -2
- package/dist/bootstrap/loadConsoleEntries.test.d.ts.map +0 -1
- package/dist/bootstrap/loadConsoleEntries.test.js +0 -27
- package/dist/bootstrap/loadConsoleEntries.test.js.map +0 -1
- package/dist/runtime/index.d.ts +0 -6
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/index.js +0 -10
- package/dist/runtime/index.js.map +0 -1
- package/dist/websocket/hooks.d.ts +0 -105
- package/dist/websocket/hooks.d.ts.map +0 -1
- package/dist/websocket/hooks.js +0 -247
- package/dist/websocket/hooks.js.map +0 -1
- package/dist/websocket/index.d.ts +0 -7
- package/dist/websocket/index.d.ts.map +0 -1
- package/dist/websocket/index.js +0 -7
- package/dist/websocket/index.js.map +0 -1
- package/dist/websocket/instance.d.ts +0 -5
- package/dist/websocket/instance.d.ts.map +0 -1
- package/dist/websocket/instance.js +0 -21
- package/dist/websocket/instance.js.map +0 -1
- package/dist/websocket/manager.d.ts +0 -113
- package/dist/websocket/manager.d.ts.map +0 -1
- package/dist/websocket/manager.js +0 -403
- package/dist/websocket/manager.js.map +0 -1
- package/dist/websocket/remote-settings.d.ts +0 -5
- package/dist/websocket/remote-settings.d.ts.map +0 -1
- package/dist/websocket/remote-settings.js +0 -23
- package/dist/websocket/remote-settings.js.map +0 -1
- package/dist/websocket/types.d.ts.map +0 -1
- package/dist/websocket/types.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,476 +1,138 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zhin.js/client
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> - **`client/`**:浏览器端源码(路由、Redux、WebSocket 等)。
|
|
5
|
-
> - **`dist/`**:`pnpm build` 后的发布产物;`package.json` 的 `main`/`types` 指向此处。
|
|
6
|
-
> 包根目录 `packages/client/` 表示 npm 包 `@zhin.js/client`,内层 `client/` 表示「Web 客户端实现」,与 [仓库结构与模块化约定](../../docs/contributing/repo-structure.md) 中 §3.4 一致。
|
|
3
|
+
Remote Console **浏览器 SDK**(entries 加载、路由/工具注册、带 Token 的 REST)。**不含 UI** — 壳层在独立仓库 [zhin-console](https://github.com/zhinjs/console)。
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
## 特性
|
|
11
|
-
|
|
12
|
-
### 路由系统
|
|
13
|
-
- 🌳 **树形路由结构** - 使用树形结构管理页面路由,支持任意深度的嵌套
|
|
14
|
-
- ✅ **动态页面管理** - 运行时添加、删除、更新页面
|
|
15
|
-
- ✅ **React Router 7.0** - 使用最新的 React Router
|
|
16
|
-
- ✅ **TypeScript 支持** - 完整的类型定义
|
|
17
|
-
- ✅ **WebSocket 集成** - 支持动态加载插件入口脚本
|
|
18
|
-
- ✅ **Redux 状态管理** - 集成 Redux 持久化
|
|
19
|
-
|
|
20
|
-
### 配置系统
|
|
21
|
-
- 🔧 **Schema 驱动** - 支持 15 种 Schema 数据类型
|
|
22
|
-
- 📝 **自动表单生成** - 根据 Schema 自动生成配置表单
|
|
23
|
-
- 🎨 **智能 UI 组件** - 针对不同类型自动选择最佳 UI 控件
|
|
24
|
-
- 🔄 **实时配置更新** - 支持配置文件的实时读取和保存
|
|
25
|
-
- 🧩 **模块化设计** - 17 个独立的字段渲染器,易于扩展
|
|
5
|
+
> **路径约定**:本包位于 `packages/console/client/`(npm `@zhin.js/client`)。源码在 `client/`,构建产物在 `dist/`。
|
|
26
6
|
|
|
27
7
|
## 安装
|
|
28
8
|
|
|
29
9
|
```bash
|
|
30
|
-
pnpm add
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## 基本使用
|
|
34
|
-
|
|
35
|
-
### 1. 设置页面路由
|
|
36
|
-
|
|
37
|
-
```tsx
|
|
38
|
-
// main.tsx
|
|
39
|
-
import { addPage, DynamicRouter } from '@zhin.js/client'
|
|
40
|
-
|
|
41
|
-
// 添加页面 - 使用 Component 而不是 element,icon 使用字符串
|
|
42
|
-
addPage({
|
|
43
|
-
key: 'home',
|
|
44
|
-
path: '/',
|
|
45
|
-
title: '首页',
|
|
46
|
-
icon: 'Home', // 图标名称对应 lucide-react 的组件名
|
|
47
|
-
Component: HomePage
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
addPage({
|
|
51
|
-
key: 'dashboard',
|
|
52
|
-
path: '/dashboard',
|
|
53
|
-
title: '仪表盘',
|
|
54
|
-
icon: 'LayoutDashboard',
|
|
55
|
-
Component: DashboardPage
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
// 渲染应用
|
|
59
|
-
createRoot(document.getElementById('root')).render(
|
|
60
|
-
<Provider store={store}>
|
|
61
|
-
<PersistGate loading={null} persistor={persistor}>
|
|
62
|
-
<DynamicRouter />
|
|
63
|
-
</PersistGate>
|
|
64
|
-
</Provider>
|
|
65
|
-
)
|
|
10
|
+
pnpm add @zhin.js/client
|
|
66
11
|
```
|
|
67
12
|
|
|
68
|
-
|
|
13
|
+
Peer:`react >= 18`(`createPluginRegisterHostApi` 需要 React 引用)。
|
|
69
14
|
|
|
70
|
-
|
|
71
|
-
import { addPage, removePage, updatePage } from '@zhin.js/client'
|
|
72
|
-
|
|
73
|
-
// 添加页面
|
|
74
|
-
addPage({
|
|
75
|
-
key: 'settings',
|
|
76
|
-
path: '/settings',
|
|
77
|
-
title: '设置',
|
|
78
|
-
icon: <Settings className="w-5 h-5" />,
|
|
79
|
-
element: <SettingsPage />
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
// 删除页面
|
|
83
|
-
removePage('/settings')
|
|
84
|
-
|
|
85
|
-
// 更新页面
|
|
86
|
-
updatePage('/settings', {
|
|
87
|
-
key: 'settings',
|
|
88
|
-
path: '/settings',
|
|
89
|
-
title: '设置(更新)',
|
|
90
|
-
icon: <Settings className="w-5 h-5" />,
|
|
91
|
-
element: <UpdatedSettingsPage />
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
// 注:RouteMenuItem 同时支持 element(ReactNode)和 Component(ComponentType)两种属性
|
|
95
|
-
|
|
96
|
-
// 向后兼容的旧 API(已废弃)
|
|
97
|
-
import { addRoute, removeRoute, updateRoute } from '@zhin.js/client'
|
|
98
|
-
// 这些 API 仍然可用,但推荐使用新的 addPage 等 API
|
|
99
|
-
```
|
|
15
|
+
## 概览
|
|
100
16
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
addPage({
|
|
118
|
-
key: 'admin',
|
|
119
|
-
path: '/admin',
|
|
120
|
-
title: '管理',
|
|
121
|
-
Component: AdminLayout
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
// 2. 自动查找父路由:/admin/users
|
|
125
|
-
// 会查找 /admin 是否存在,如果存在,插入 users 到 /admin
|
|
126
|
-
addPage({
|
|
127
|
-
key: 'admin-users',
|
|
128
|
-
path: '/admin/users',
|
|
129
|
-
title: '用户管理',
|
|
130
|
-
icon: 'Users',
|
|
131
|
-
Component: UsersPage
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
addPage({
|
|
135
|
-
key: 'admin-settings',
|
|
136
|
-
path: '/admin/settings',
|
|
137
|
-
title: '系统设置',
|
|
138
|
-
icon: 'Settings',
|
|
139
|
-
Component: AdminSettingsPage
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
// 3. 自动查找父路由:/admin/users/detail
|
|
143
|
-
// 会查找 /admin/users 是否存在,如果存在,插入 detail 到 /admin/users
|
|
144
|
-
addPage({
|
|
145
|
-
key: 'user-detail',
|
|
146
|
-
path: '/admin/users/detail',
|
|
147
|
-
title: '用户详情',
|
|
148
|
-
Component: UserDetailPage
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
// 4. 动态添加嵌套页面
|
|
152
|
-
setTimeout(() => {
|
|
153
|
-
// 会自动查找 /admin 是否存在,如果存在,插入 analytics 到 /admin
|
|
154
|
-
addPage({
|
|
155
|
-
key: 'admin-analytics',
|
|
156
|
-
path: '/admin/analytics',
|
|
157
|
-
title: '分析',
|
|
158
|
-
Component: AnalyticsPage
|
|
159
|
-
})
|
|
160
|
-
}, 2000)
|
|
161
|
-
|
|
162
|
-
// 5. 动态添加更深层嵌套
|
|
163
|
-
setTimeout(() => {
|
|
164
|
-
// 会自动查找 /admin/analytics 是否存在,如果存在,插入 reports 到 /admin/analytics
|
|
165
|
-
addPage({
|
|
166
|
-
key: 'analytics-reports',
|
|
167
|
-
path: '/admin/analytics/reports',
|
|
168
|
-
title: '分析报告',
|
|
169
|
-
Component: ReportsPage
|
|
170
|
-
})
|
|
171
|
-
}, 4000)
|
|
172
|
-
```
|
|
17
|
+
| 导出 | 用途 |
|
|
18
|
+
|------|------|
|
|
19
|
+
| `loadConsoleEntries` | 拉 `GET /entries`、动态 import 各 entry、调用 `register(hostApi)` |
|
|
20
|
+
| `createConsoleClient` | 创建一个拥有路由、工具、运行时环境与网络连接的 Console 实例 |
|
|
21
|
+
| `ConsoleClientProvider` | 将 Console 实例提供给 `useConsoleTransport` 等 React hooks |
|
|
22
|
+
| `apiFetch` | 相对 Host API Base 的 `fetch`,自动附加 `Authorization: Bearer` |
|
|
23
|
+
| `getApiBase` / `getToken` | 读取登录页写入 `localStorage` 的 API Base 与 Token |
|
|
24
|
+
| `createRegistryStore` / `useRegistry` | 可选 registry store |
|
|
25
|
+
| `ENDPOINT_RPC` / `INBOX_RPC` / `SIDE_EVENT_RPC` / `SIDE_EVENT_PUSH` | `@zhin.js/console-protocol` 的规范 RPC 与推送名称 |
|
|
26
|
+
| `parseConsoleInboxEvent` | 校验并分类 canonical message/request/notice 推送 |
|
|
27
|
+
| `ConsoleEndpointSummary` / `EndpointManagementCapability` | Host 与 Remote Console 共享的 Endpoint wire 类型 |
|
|
28
|
+
| `fetchConsoleEventHistory` | 按 `(runtimeId, eventId)` 拉取有界事件历史 |
|
|
29
|
+
| `ConsoleTransport.onConsoleEvent` | 订阅带 `live/history` 投递来源的强类型事件 |
|
|
30
|
+
| `ConsoleTransport.onConsoleEventRecoveryGap` | 观察不可续接游标并触发领域全量重同步 |
|
|
31
|
+
| `ConsoleInboxNoticesQuery` / `ConsoleInboxNoticesResult` | 以 `unreadOnly` 从持久 Inbox 重建未读通知 |
|
|
173
32
|
|
|
174
|
-
|
|
33
|
+
类型与 Entry 契约来自 `@zhin.js/contract`。
|
|
175
34
|
|
|
176
|
-
|
|
177
|
-
import { routerManager } from '@zhin.js/client'
|
|
178
|
-
|
|
179
|
-
// 监听路由变化
|
|
180
|
-
routerManager.onRouteChange(() => {
|
|
181
|
-
console.log('Routes changed')
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
// 监听路由添加
|
|
185
|
-
routerManager.onRouteAdd((route) => {
|
|
186
|
-
console.log('Route added:', route.path)
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
// 监听路由删除
|
|
190
|
-
routerManager.onRouteRemove((path) => {
|
|
191
|
-
console.log('Route removed:', path)
|
|
192
|
-
})
|
|
193
|
-
```
|
|
35
|
+
业务 UI 应使用 SDK 导出的协议常量和 Endpoint 类型,不要复制 RPC 字符串或 `EndpointInfo`。请求使用顶层 camelCase payload,Host 推送使用 `message.receive` / `request.receive` / `notice.receive` 和 canonical `adapter`、`endpointKey`、`channelId` 字段。
|
|
194
36
|
|
|
195
|
-
##
|
|
37
|
+
## 启动:加载插件 Console Entry
|
|
196
38
|
|
|
197
|
-
|
|
39
|
+
Remote Console 壳层在登录后调用:
|
|
198
40
|
|
|
199
41
|
```tsx
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
/** @deprecated 请使用 updatePage */
|
|
229
|
-
updateRoute(path: string, route: RouteMenuItem): void
|
|
230
|
-
|
|
231
|
-
/** @deprecated 请使用 getPage */
|
|
232
|
-
getRoute(path: string): RouteMenuItem | undefined
|
|
233
|
-
|
|
234
|
-
/** @deprecated 请使用 getAllPages */
|
|
235
|
-
getAllRoutes(): RouteMenuItem[]
|
|
236
|
-
|
|
237
|
-
/** @deprecated 请使用 clearPages */
|
|
238
|
-
clearRoutes(): void
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### routerManager 对象
|
|
242
|
-
|
|
243
|
-
```tsx
|
|
244
|
-
// 事件监听方法
|
|
245
|
-
routerManager.onRouteChange(callback: () => void): () => void
|
|
246
|
-
routerManager.onRouteAdd(callback: (route: RouteConfig) => void): () => void
|
|
247
|
-
routerManager.onRouteRemove(callback: (path: string) => void): () => void
|
|
248
|
-
routerManager.onRouteUpdate(callback: (path: string, route: RouteConfig) => void): () => void
|
|
249
|
-
routerManager.onRouteClear(callback: () => void): () => void
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
### RouteMenuItem 接口
|
|
253
|
-
|
|
254
|
-
```tsx
|
|
255
|
-
interface RouteMenuItem {
|
|
256
|
-
key: string // 唯一标识
|
|
257
|
-
path: string // 路由路径
|
|
258
|
-
title: string // 页面标题
|
|
259
|
-
icon?: ReactNode // 图标元素(直接传入 React 元素,如 <Home className="w-5 h-5" />)
|
|
260
|
-
element?: ReactNode // React 组件
|
|
261
|
-
children?: RouteMenuItem[] // 子路由
|
|
262
|
-
meta?: {
|
|
263
|
-
order?: number // 排序
|
|
264
|
-
hideInMenu?: boolean // 是否在菜单中隐藏
|
|
265
|
-
requiresAuth?: boolean // 是否需要认证
|
|
266
|
-
[key: string]: any
|
|
267
|
-
}
|
|
42
|
+
import React from "react";
|
|
43
|
+
import {
|
|
44
|
+
ConsoleClientProvider,
|
|
45
|
+
createConsoleClient,
|
|
46
|
+
loadConsoleEntries,
|
|
47
|
+
} from "@zhin.js/client";
|
|
48
|
+
|
|
49
|
+
const client = createConsoleClient({
|
|
50
|
+
getRuntimeEnv: () => import.meta.env.DEV ? "development" : "production",
|
|
51
|
+
});
|
|
52
|
+
const hostApi = client.createPluginRegisterHostApi(React);
|
|
53
|
+
|
|
54
|
+
await loadConsoleEntries({
|
|
55
|
+
hostApi,
|
|
56
|
+
// Remote Console:Host 监听地址,用于解析 /@dev、/@assets 模块 URL
|
|
57
|
+
assetOrigin: "http://127.0.0.1:8086",
|
|
58
|
+
fetchInit: () => ({
|
|
59
|
+
headers: { Authorization: `Bearer ${localStorage.getItem("zhin_api_token")}` },
|
|
60
|
+
}),
|
|
61
|
+
onFetchError: (status) => console.error("entries fetch failed", status),
|
|
62
|
+
onEmpty: () => console.warn("no console entries"),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 壳层根组件必须为 hooks 提供同一个 client 所有者。
|
|
66
|
+
export function ConsoleRoot({ children }: { children: React.ReactNode }) {
|
|
67
|
+
React.useEffect(() => () => client.dispose(), []);
|
|
68
|
+
return <ConsoleClientProvider client={client}>{children}</ConsoleClientProvider>;
|
|
268
69
|
}
|
|
269
70
|
```
|
|
270
71
|
|
|
271
|
-
|
|
72
|
+
`loadConsoleEntries` 内部:`fetchConsoleEntries` → `registerConsolePluginsFromEntries` → 各 entry 模块的 `register` 或 `default.register`。
|
|
272
73
|
|
|
273
|
-
|
|
74
|
+
插件 `client/` 入口示例:
|
|
274
75
|
|
|
275
76
|
```tsx
|
|
276
|
-
import {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
Component: DynamicPage
|
|
286
|
-
})
|
|
287
|
-
}, 2000)
|
|
288
|
-
|
|
289
|
-
// 动态更新页面
|
|
290
|
-
setTimeout(() => {
|
|
291
|
-
const UpdatedPage = () => <div>Updated Page</div>
|
|
292
|
-
updatePage('/dynamic', {
|
|
293
|
-
key: 'dynamic',
|
|
294
|
-
path: '/dynamic',
|
|
295
|
-
title: '动态页面(已更新)',
|
|
296
|
-
Component: UpdatedPage
|
|
297
|
-
})
|
|
298
|
-
}, 4000)
|
|
299
|
-
|
|
300
|
-
// 动态删除页面
|
|
301
|
-
setTimeout(() => {
|
|
302
|
-
removePage('/dynamic')
|
|
303
|
-
}, 6000)
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### WebSocket 动态加载
|
|
307
|
-
|
|
308
|
-
```tsx
|
|
309
|
-
import { useWebSocket, addPage } from '@zhin.js/client'
|
|
310
|
-
|
|
311
|
-
function App() {
|
|
312
|
-
// 连接 WebSocket,接收动态入口脚本
|
|
313
|
-
const ws = useWebSocket({
|
|
314
|
-
onMessage: (message) => {
|
|
315
|
-
console.log('收到消息:', message)
|
|
316
|
-
}
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
return (
|
|
320
|
-
<div>
|
|
321
|
-
<p>WebSocket 状态: {ws.connected ? '已连接' : '未连接'}</p>
|
|
322
|
-
<p>已加载入口: {ws.entries.length}</p>
|
|
323
|
-
<DynamicRouter />
|
|
324
|
-
</div>
|
|
325
|
-
)
|
|
77
|
+
import type { PluginRegisterHostApi } from "@zhin.js/contract";
|
|
78
|
+
|
|
79
|
+
export async function register(hostApi: PluginRegisterHostApi) {
|
|
80
|
+
hostApi.addRoute({
|
|
81
|
+
path: "/my-plugin",
|
|
82
|
+
name: "My Plugin",
|
|
83
|
+
element: hostApi.React.createElement(MyPage),
|
|
84
|
+
icon: "Puzzle",
|
|
85
|
+
});
|
|
326
86
|
}
|
|
327
|
-
|
|
328
|
-
// 插件入口脚本示例(my-plugin-entry.ts)
|
|
329
|
-
import { addPage } from '@zhin.js/client'
|
|
330
|
-
|
|
331
|
-
addPage({
|
|
332
|
-
key: 'my-plugin',
|
|
333
|
-
path: '/my-plugin',
|
|
334
|
-
title: '我的插件',
|
|
335
|
-
icon: 'Puzzle',
|
|
336
|
-
Component: MyPluginPage
|
|
337
|
-
})
|
|
338
87
|
```
|
|
339
88
|
|
|
340
|
-
|
|
89
|
+
## Host API 请求
|
|
341
90
|
|
|
342
|
-
|
|
343
|
-
// 路由统计
|
|
344
|
-
let routeCount = 0
|
|
345
|
-
routerManager.onRouteAdd(() => {
|
|
346
|
-
routeCount++
|
|
347
|
-
console.log(`Total routes: ${routeCount}`)
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
routerManager.onRouteRemove(() => {
|
|
351
|
-
routeCount--
|
|
352
|
-
console.log(`Total routes: ${routeCount}`)
|
|
353
|
-
})
|
|
354
|
-
```
|
|
91
|
+
登录页将 API Base 与 Token 存入 `localStorage`(键 `zhin_api_base`、`zhin_api_token`)。业务请求使用 `apiFetch`:
|
|
355
92
|
|
|
356
|
-
|
|
93
|
+
```ts
|
|
94
|
+
import { apiFetch } from "@zhin.js/client";
|
|
357
95
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
})
|
|
96
|
+
const res = await apiFetch("/api/console/request", {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: { "Content-Type": "application/json" },
|
|
99
|
+
body: JSON.stringify({ method: "system.status", params: {} }),
|
|
100
|
+
});
|
|
365
101
|
```
|
|
366
102
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
### Schema 支持的数据类型
|
|
370
|
-
|
|
371
|
-
Zhin Client 配置系统完整支持所有 15 种 Schema 数据类型:
|
|
372
|
-
|
|
373
|
-
#### 基础类型
|
|
374
|
-
- `string` - 字符串(支持枚举/多行/单行)
|
|
375
|
-
- `number` / `integer` - 数字(支持 min/max 限制)
|
|
376
|
-
- `boolean` - 布尔值(开关控件)
|
|
103
|
+
401 时清除 Token 并派发 `zhin:auth-required`。
|
|
377
104
|
|
|
378
|
-
|
|
379
|
-
- `percent` - 百分比(滑块 + 数字输入)
|
|
380
|
-
- `date` - 日期(日期选择器)
|
|
381
|
-
- `regexp` - 正则表达式(带验证)
|
|
382
|
-
- `const` - 常量(只读显示)
|
|
105
|
+
辅助:`resolveApiUrl`。
|
|
383
106
|
|
|
384
|
-
|
|
385
|
-
- `list` - 列表(支持嵌套)
|
|
386
|
-
- `tuple` - 元组(固定字段)
|
|
387
|
-
- `object` - 对象(嵌套结构)
|
|
388
|
-
- `dict` - 字典(JSON 编辑器)
|
|
107
|
+
Client 在连接 `/api/events` 前先补拉 `/api/events/history`,再携带最新游标进入 SSE;Host 会原子重放 HTTP 与订阅之间产生的事件。游标只在 Inbox 持久化成功后推进,重复的 history/live 投递按 `(runtimeId, eventId)` 幂等写入 IndexedDB。`gap` 不会被吞掉,页面可订阅恢复缺口并改走领域 HTTP 全量投影。
|
|
389
108
|
|
|
390
|
-
|
|
391
|
-
- `union` - 联合类型(多选一)
|
|
392
|
-
- `intersect` - 交叉类型(满足所有)
|
|
109
|
+
## Console 实例所有权
|
|
393
110
|
|
|
394
|
-
|
|
395
|
-
- `any` - 任意类型(JSON 编辑器)
|
|
396
|
-
- `never` - 永不类型(警告提示)
|
|
111
|
+
每个 Remote Console mount 创建一个 `ConsoleClient`。插件注册、React hooks 和卸载必须使用同一个实例,多个 mount 之间不会共享路由、工具、运行时环境或连接状态:
|
|
397
112
|
|
|
398
|
-
|
|
113
|
+
```ts
|
|
114
|
+
import React from "react";
|
|
115
|
+
import { createConsoleClient } from "@zhin.js/client";
|
|
399
116
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
<PluginConfigForm
|
|
404
|
-
pluginName="my-plugin"
|
|
405
|
-
onClose={() => setDialogOpen(false)}
|
|
406
|
-
onSuccess={() => refetchPlugin()}
|
|
407
|
-
/>
|
|
117
|
+
const client = createConsoleClient();
|
|
118
|
+
const hostApi = client.createPluginRegisterHostApi(React);
|
|
119
|
+
await loadConsoleEntries({ hostApi });
|
|
408
120
|
```
|
|
409
121
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
```typescript
|
|
413
|
-
import { Schema } from 'zhin.js'
|
|
414
|
-
|
|
415
|
-
export const config = Schema.object({
|
|
416
|
-
// 基础类型
|
|
417
|
-
name: Schema.string('插件名称').required(),
|
|
418
|
-
enabled: Schema.boolean('是否启用').default(true),
|
|
419
|
-
port: Schema.number('端口').min(1).max(65535).default(3000),
|
|
420
|
-
|
|
421
|
-
// 特殊类型
|
|
422
|
-
opacity: Schema.percent('透明度').default(0.8),
|
|
423
|
-
startDate: Schema.date('开始日期'),
|
|
424
|
-
pattern: Schema.regexp('匹配模式'),
|
|
425
|
-
|
|
426
|
-
// 集合类型
|
|
427
|
-
tags: Schema.list(Schema.string(), '标签'),
|
|
428
|
-
server: Schema.object({
|
|
429
|
-
host: Schema.string().default('localhost'),
|
|
430
|
-
port: Schema.number().default(3000)
|
|
431
|
-
}),
|
|
432
|
-
|
|
433
|
-
// 组合类型
|
|
434
|
-
mode: Schema.union([
|
|
435
|
-
Schema.const('auto'),
|
|
436
|
-
Schema.const('manual')
|
|
437
|
-
])
|
|
438
|
-
})
|
|
439
|
-
```
|
|
122
|
+
## 低级 API
|
|
440
123
|
|
|
441
|
-
|
|
124
|
+
- `fetchConsoleEntries(options?)` — 仅拉 JSON,不 import
|
|
125
|
+
- `registerConsolePluginsFromEntries(entries, hostApi, ...)` — 已知 entries 列表时注册
|
|
126
|
+
- `getRegisterFn(mod)` — 从动态模块解析 `register` 导出
|
|
442
127
|
|
|
443
|
-
|
|
444
|
-
- **智能分组**: 简单字段直接展示,复杂字段可折叠
|
|
445
|
-
- **嵌套支持**: 完整支持任意深度的嵌套结构
|
|
446
|
-
- **实时验证**: 输入时进行类型验证和格式检查
|
|
447
|
-
- **紧凑布局**: 使用 ScrollArea 和 Accordion 优化空间使用
|
|
128
|
+
## 构建
|
|
448
129
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
### PluginConfigForm 模块结构
|
|
452
|
-
|
|
453
|
-
```
|
|
454
|
-
PluginConfigForm/
|
|
455
|
-
├── types.ts - 类型定义
|
|
456
|
-
├── BasicFieldRenderers.tsx - 基础类型渲染器 (9个)
|
|
457
|
-
├── CollectionFieldRenderers.tsx - 集合类型渲染器 (5个)
|
|
458
|
-
├── CompositeFieldRenderers.tsx - 组合类型渲染器 (2个)
|
|
459
|
-
├── FieldRenderer.tsx - 字段渲染器主入口
|
|
460
|
-
├── NestedFieldRenderer.tsx - 嵌套字段渲染器
|
|
461
|
-
└── index.tsx - 主组件
|
|
130
|
+
```bash
|
|
131
|
+
pnpm --filter @zhin.js/client build
|
|
462
132
|
```
|
|
463
133
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
## 注意事项
|
|
467
|
-
|
|
468
|
-
1. **路由路径唯一性** - 确保路由路径的唯一性,避免冲突
|
|
469
|
-
2. **事件清理** - 记得清理事件监听器,避免内存泄漏
|
|
470
|
-
3. **性能考虑** - 大量路由时考虑使用懒加载
|
|
471
|
-
4. **类型安全** - 使用 TypeScript 确保类型安全
|
|
472
|
-
5. **Schema 定义** - 为插件配置定义清晰的 Schema,提供友好的描述信息
|
|
473
|
-
|
|
474
|
-
## 示例项目
|
|
134
|
+
## 相关文档
|
|
475
135
|
|
|
476
|
-
|
|
136
|
+
- [Remote Console 使用说明](../../../docs/console-remote.md)
|
|
137
|
+
- [Console 栈概览](../README.md)
|
|
138
|
+
- [@zhin.js/host-http(Console/HTTP Host 传输)](../../host/http/README.md)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AGENT_STREAM_MEDIA_TYPE,
|
|
3
|
+
ZHIN_AGENT_SESSION_API_PREFIX,
|
|
4
|
+
ZHIN_SESSION_ID_HEADER,
|
|
5
|
+
foldAgentStreamNdjson,
|
|
6
|
+
iterateAgentStreamNdjson,
|
|
7
|
+
type AgentStreamEvent,
|
|
8
|
+
type AgentStreamReduceState,
|
|
9
|
+
type ContinueAgentSessionResponse,
|
|
10
|
+
type FoldAgentStreamOptions,
|
|
11
|
+
type StartAgentSessionResponse,
|
|
12
|
+
} from "@zhin.js/contract";
|
|
13
|
+
import { getToken, resolveApiUrl } from "./console-utils/remoteApi.js";
|
|
14
|
+
|
|
15
|
+
export type AgentStreamClientOptions = {
|
|
16
|
+
fetch?: typeof fetch;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function resolveFetch(options?: AgentStreamClientOptions): typeof fetch {
|
|
20
|
+
const impl = options?.fetch ?? globalThis.fetch;
|
|
21
|
+
if (!impl) throw new Error("fetch is not available");
|
|
22
|
+
return impl;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function authedFetch(
|
|
26
|
+
path: string,
|
|
27
|
+
init: RequestInit,
|
|
28
|
+
fetchImpl: typeof fetch,
|
|
29
|
+
): Promise<Response> {
|
|
30
|
+
const token = getToken();
|
|
31
|
+
const headers = new Headers(init.headers);
|
|
32
|
+
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
33
|
+
const url = path.startsWith("/") ? resolveApiUrl(path) : path;
|
|
34
|
+
const res = await fetchImpl(url, { ...init, headers });
|
|
35
|
+
if (res.status === 401 && typeof localStorage !== "undefined") {
|
|
36
|
+
sessionStorage.removeItem("zhin_api_token");
|
|
37
|
+
localStorage.removeItem("zhin_api_token");
|
|
38
|
+
window.dispatchEvent(new CustomEvent("zhin:auth-required"));
|
|
39
|
+
}
|
|
40
|
+
return res;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readJson<T>(res: Response): Promise<T> {
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const err = await res.json().catch(() => ({})) as { error?: string };
|
|
46
|
+
throw new Error(err.error ?? `HTTP ${res.status}`);
|
|
47
|
+
}
|
|
48
|
+
return res.json() as Promise<T>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** POST /zhin/v1/session — start a Host agent session turn. */
|
|
52
|
+
export async function startAgentSession(
|
|
53
|
+
message: string,
|
|
54
|
+
options?: AgentStreamClientOptions,
|
|
55
|
+
): Promise<StartAgentSessionResponse> {
|
|
56
|
+
const fetchImpl = resolveFetch(options);
|
|
57
|
+
const res = await authedFetch(`${ZHIN_AGENT_SESSION_API_PREFIX}/session`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify({ message }),
|
|
61
|
+
}, fetchImpl);
|
|
62
|
+
return readJson<StartAgentSessionResponse>(res);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** POST /zhin/v1/session/:sessionId — continue with continuationToken + message. */
|
|
66
|
+
export async function continueAgentSession(
|
|
67
|
+
sessionId: string,
|
|
68
|
+
continuationToken: string,
|
|
69
|
+
message: string,
|
|
70
|
+
options?: AgentStreamClientOptions,
|
|
71
|
+
): Promise<ContinueAgentSessionResponse> {
|
|
72
|
+
const fetchImpl = resolveFetch(options);
|
|
73
|
+
const res = await authedFetch(
|
|
74
|
+
`${ZHIN_AGENT_SESSION_API_PREFIX}/session/${encodeURIComponent(sessionId)}`,
|
|
75
|
+
{
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "Content-Type": "application/json" },
|
|
78
|
+
body: JSON.stringify({ continuationToken, message }),
|
|
79
|
+
},
|
|
80
|
+
fetchImpl,
|
|
81
|
+
);
|
|
82
|
+
return readJson<ContinueAgentSessionResponse>(res);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type SubscribeAgentStreamOptions = FoldAgentStreamOptions & {
|
|
86
|
+
startIndex?: number;
|
|
87
|
+
fetch?: typeof fetch;
|
|
88
|
+
onEvent?: (event: AgentStreamEvent, state: AgentStreamReduceState) => void;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** GET /zhin/v1/session/:sessionId/stream — consume NDJSON until the stream closes. */
|
|
92
|
+
export async function subscribeAgentStream(
|
|
93
|
+
sessionId: string,
|
|
94
|
+
options?: SubscribeAgentStreamOptions,
|
|
95
|
+
): Promise<AgentStreamReduceState> {
|
|
96
|
+
const fetchImpl = resolveFetch(options);
|
|
97
|
+
const startIndex = options?.startIndex ?? 0;
|
|
98
|
+
const query = startIndex > 0 ? `?startIndex=${startIndex}` : "";
|
|
99
|
+
const res = await authedFetch(
|
|
100
|
+
`${ZHIN_AGENT_SESSION_API_PREFIX}/session/${encodeURIComponent(sessionId)}/stream${query}`,
|
|
101
|
+
{
|
|
102
|
+
method: "GET",
|
|
103
|
+
headers: { Accept: AGENT_STREAM_MEDIA_TYPE },
|
|
104
|
+
},
|
|
105
|
+
fetchImpl,
|
|
106
|
+
);
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
const err = await res.json().catch(() => ({})) as { error?: string };
|
|
109
|
+
throw new Error(err.error ?? `HTTP ${res.status}`);
|
|
110
|
+
}
|
|
111
|
+
const headerSessionId = res.headers.get(ZHIN_SESSION_ID_HEADER);
|
|
112
|
+
if (headerSessionId && headerSessionId !== sessionId) {
|
|
113
|
+
throw new Error(`stream session id mismatch: ${headerSessionId}`);
|
|
114
|
+
}
|
|
115
|
+
if (!res.body) {
|
|
116
|
+
throw new Error("stream response has no body");
|
|
117
|
+
}
|
|
118
|
+
return foldAgentStreamNdjson(res.body, options);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { iterateAgentStreamNdjson, foldAgentStreamNdjson };
|