@zenk-agent/plugin-preview 0.0.24
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 +145 -0
- package/app/index.html +12 -0
- package/app/postcss.config.js +6 -0
- package/app/src/App.tsx +101 -0
- package/app/src/main.tsx +16 -0
- package/app/src/preview-host.ts +339 -0
- package/app/src/styles.css +60 -0
- package/app/tailwind.config.js +8 -0
- package/bin/zenk-plugin-preview.mjs +33 -0
- package/dist/cli.mjs +386 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# @zenk-agent/plugin-preview
|
|
2
|
+
|
|
3
|
+
单插件预览服务。运行后会启动一个 Vite dev server,并把当前目录当作插件根目录自动加载。
|
|
4
|
+
|
|
5
|
+
## 用法
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
zenk-plugin-preview dev
|
|
9
|
+
zenk-plugin-preview dev /path/to/plugin --port 4176 --host 127.0.0.1
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 约定
|
|
13
|
+
|
|
14
|
+
- 当前目录需要有 `package.json`
|
|
15
|
+
- `package.json` 中需要声明 `zenk.publicPlugin` 或 `zenk.publicPlugins`
|
|
16
|
+
- 对应的 `exports` 里需要能解析出 config 和 worker 入口
|
|
17
|
+
|
|
18
|
+
## 代理配置
|
|
19
|
+
|
|
20
|
+
预览模式下,插件里的 HTTP 请求应当直接写相对 URL,例如:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { usePluginSdk } from '@zenk-agent/plugin-sdk'
|
|
24
|
+
|
|
25
|
+
const sdk = usePluginSdk()
|
|
26
|
+
|
|
27
|
+
await sdk.http.request({
|
|
28
|
+
url: '/api/ontology/schemas/EQ/instances?limit=10&offset=0',
|
|
29
|
+
method: 'GET',
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
不要在插件 worker 里写死后端完整地址。预览服务会根据 `preview.config.json` 的 `proxy` 把这些前缀转发到真实后端。
|
|
34
|
+
|
|
35
|
+
配置优先级:
|
|
36
|
+
|
|
37
|
+
1. 当前插件目录 `preview.config.json`
|
|
38
|
+
2. workspace 根目录 `preview.config.json`
|
|
39
|
+
|
|
40
|
+
注意:`preview.config.json` 只在 `zenk-plugin-preview dev` 启动时读取一次。修改后需要重启预览服务,不能依赖 Vite 热更新刷新代理或 ontology 相关配置。
|
|
41
|
+
|
|
42
|
+
推荐优先使用 `preview.config.json`:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"proxy": {
|
|
47
|
+
"/v1": {
|
|
48
|
+
"target": "http://127.0.0.1:8999",
|
|
49
|
+
"changeOrigin": true,
|
|
50
|
+
"secure": true
|
|
51
|
+
},
|
|
52
|
+
"/agent-api": {
|
|
53
|
+
"target": "http://127.0.0.1:8999",
|
|
54
|
+
"changeOrigin": true,
|
|
55
|
+
"secure": true
|
|
56
|
+
},
|
|
57
|
+
"/api": {
|
|
58
|
+
"target": "http://127.0.0.1:10000",
|
|
59
|
+
"changeOrigin": true,
|
|
60
|
+
"secure": false
|
|
61
|
+
},
|
|
62
|
+
"/tools-api": {
|
|
63
|
+
"target": "http://127.0.0.1:10000",
|
|
64
|
+
"changeOrigin": true,
|
|
65
|
+
"secure": false
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"ontology": {
|
|
69
|
+
"mode": "remote",
|
|
70
|
+
"baseUrl": "http://127.0.0.1:9998",
|
|
71
|
+
"proxyPrefix": "/__plugin-preview-ontology",
|
|
72
|
+
"schemasPath": "/api/ontology/schemas"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
其中:
|
|
78
|
+
|
|
79
|
+
- `proxy` 结构直接复用 Vite `server.proxy`
|
|
80
|
+
- 请求发哪个前缀,由插件代码自己决定
|
|
81
|
+
- 预览层不再维护“默认 API 前缀”
|
|
82
|
+
|
|
83
|
+
## Ontology 调试数据源
|
|
84
|
+
|
|
85
|
+
`sdk.ontology.query()` / `sdk.ontology.get()` / `sdk.ontology.executeCypher()` 在预览环境支持两种模式:
|
|
86
|
+
|
|
87
|
+
1. `mock`
|
|
88
|
+
2. `remote`
|
|
89
|
+
|
|
90
|
+
默认是 `mock`,会返回预览内置假数据。
|
|
91
|
+
|
|
92
|
+
如果要联调真实后端,推荐在 `preview.config.json` 里配置:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"proxy": {
|
|
97
|
+
"/v1": {
|
|
98
|
+
"target": "http://192.168.10.61:8999",
|
|
99
|
+
"changeOrigin": true,
|
|
100
|
+
"secure": true
|
|
101
|
+
},
|
|
102
|
+
"/agent-api": {
|
|
103
|
+
"target": "http://192.168.10.61:8999",
|
|
104
|
+
"changeOrigin": true,
|
|
105
|
+
"secure": true
|
|
106
|
+
},
|
|
107
|
+
"/api": {
|
|
108
|
+
"target": "http://192.168.10.61:10000",
|
|
109
|
+
"changeOrigin": true,
|
|
110
|
+
"secure": false
|
|
111
|
+
},
|
|
112
|
+
"/tools-api": {
|
|
113
|
+
"target": "http://192.168.10.61:10000",
|
|
114
|
+
"changeOrigin": true,
|
|
115
|
+
"secure": false
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"ontology": {
|
|
119
|
+
"mode": "remote",
|
|
120
|
+
"baseUrl": "http://192.168.10.61:9998"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
此时 ontology capability 会请求:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
GET /api/ontology/schemas/:schemaKey/instances?limit=20&offset=0
|
|
128
|
+
GET /api/ontology/schemas/:schemaKey/instances/:entityId
|
|
129
|
+
GET /api/ontology/schemas/:schemaKey/instances/by-name/:entityName
|
|
130
|
+
POST /api/ontology/schemas/:schemaKey/cypher (executeCypher,body: { cypher })
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
例如:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
http://localhost:4175/api/ontology/schemas/SUP/instances?limit=20&offset=0
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
插件侧调用不需要改,仍然保持:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
sdk.ontology.query({ schemaKey: 'SUP', query: { limit: 20, offset: 0 } })
|
|
143
|
+
sdk.ontology.get({ schemaKey: 'SUP', entityId: '123' })
|
|
144
|
+
sdk.ontology.executeCypher({ cypher: 'MATCH (n:Equipment) RETURN n LIMIT 10' })
|
|
145
|
+
```
|
package/app/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Zenk Plugin Preview</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body class="bg-slate-950">
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/app/src/App.tsx
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { HostedPluginCard, RemotePluginCardHost, renderDefaultPluginModal } from '@zenk-agent/plugin-host'
|
|
3
|
+
import { decision, demoTheme, fetchFn, navigate, ontology } from './preview-host'
|
|
4
|
+
import { previewPluginDir, previewPlugins } from 'virtual:tool-plugin-preview'
|
|
5
|
+
|
|
6
|
+
function App() {
|
|
7
|
+
const [collapsed, setCollapsed] = useState(false)
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<main className="relative min-h-screen bg-[radial-gradient(circle_at_top,rgba(251,191,36,0.18),transparent_28%),linear-gradient(180deg,#111827_0%,#020617_100%)] text-slate-50">
|
|
11
|
+
{/* Left content area */}
|
|
12
|
+
<div className={`mx-auto flex w-full max-w-5xl flex-col gap-8 px-6 py-10 transition-[padding-right] duration-200 lg:px-10 ${collapsed ? 'pr-[72px]' : 'pr-[416px]'}`}>
|
|
13
|
+
<header className="max-w-3xl">
|
|
14
|
+
<p className="text-sm font-medium uppercase tracking-[0.24em] text-amber-300/80">Plugin Preview Service</p>
|
|
15
|
+
<h1 className="mt-3 text-4xl font-semibold tracking-tight text-white sm:text-5xl">Tool plugin preview</h1>
|
|
16
|
+
<p className="mt-4 text-base leading-7 text-slate-300">
|
|
17
|
+
这个页面由 `@zenk-agent/plugin-preview` 启动,只预览当前插件目录。
|
|
18
|
+
</p>
|
|
19
|
+
<p className="mt-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-300">
|
|
20
|
+
<span className="font-semibold text-white">Plugin dir</span>: {previewPluginDir}
|
|
21
|
+
</p>
|
|
22
|
+
</header>
|
|
23
|
+
|
|
24
|
+
{previewPlugins.length === 0 ? (
|
|
25
|
+
<section className="rounded-[28px] border border-amber-300/20 bg-white/[0.04] px-6 py-6 shadow-[0_24px_60px_rgba(2,8,23,0.32)]">
|
|
26
|
+
<p className="text-lg font-semibold text-white">没有匹配到可预览的插件</p>
|
|
27
|
+
<p className="mt-2 text-sm leading-6 text-slate-300">
|
|
28
|
+
当前目录需要包含 `package.json`,并在 `zenk.publicPlugin` 或 `zenk.publicPlugins` 中声明 config 和 worker 导出。
|
|
29
|
+
</p>
|
|
30
|
+
</section>
|
|
31
|
+
) : null}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{/* Right toolbar */}
|
|
35
|
+
<aside
|
|
36
|
+
className={`absolute right-0 top-0 z-20 h-full border-l border-white/10 bg-white/[0.05] backdrop-blur-sm transition-[width] duration-200 ${collapsed ? 'w-14 overflow-hidden' : 'w-[400px]'}`}
|
|
37
|
+
>
|
|
38
|
+
{/* Toolbar header */}
|
|
39
|
+
<div
|
|
40
|
+
className={`flex h-[54px] items-center border-b border-white/10 bg-white/[0.02] ${collapsed ? 'justify-center px-2' : 'justify-between gap-2 px-6'}`}
|
|
41
|
+
>
|
|
42
|
+
<div className="flex items-center gap-2">
|
|
43
|
+
<svg className="h-4 w-4 text-amber-400" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
44
|
+
<path
|
|
45
|
+
d="M3 2.667h10v4.666H3V2.667Zm2 2H4.333V5.33H5V4.667Zm0 3.333h6.667v5.333H5V8Zm-2 0h1v5.333H3V8Zm10 0h-1v5.333h1V8Z"
|
|
46
|
+
fill="currentColor"
|
|
47
|
+
/>
|
|
48
|
+
</svg>
|
|
49
|
+
{!collapsed ? <span className="text-[16px] font-medium text-white">插件预览</span> : null}
|
|
50
|
+
</div>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={() => setCollapsed((prev) => !prev)}
|
|
54
|
+
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-[#d7dbe4] transition hover:bg-white/10 hover:text-white"
|
|
55
|
+
aria-label={collapsed ? '展开工具栏' : '收起工具栏'}
|
|
56
|
+
title={collapsed ? '展开工具栏' : '收起工具栏'}
|
|
57
|
+
>
|
|
58
|
+
<svg className="h-4 w-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
59
|
+
<path
|
|
60
|
+
d={collapsed ? 'M6 3.5L10 8L6 12.5' : 'M10 3.5L6 8L10 12.5'}
|
|
61
|
+
stroke="currentColor"
|
|
62
|
+
strokeWidth="1.4"
|
|
63
|
+
strokeLinecap="round"
|
|
64
|
+
strokeLinejoin="round"
|
|
65
|
+
/>
|
|
66
|
+
</svg>
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Toolbar content */}
|
|
71
|
+
{!collapsed ? (
|
|
72
|
+
<div className="h-[calc(100%-54px)] overflow-y-auto px-4 py-4 pb-6 scrollbar-thin">
|
|
73
|
+
<div className="space-y-4">
|
|
74
|
+
{previewPlugins.map((plugin) => (
|
|
75
|
+
<HostedPluginCard
|
|
76
|
+
key={plugin.id}
|
|
77
|
+
plugin={plugin}
|
|
78
|
+
renderPluginContent={() => (
|
|
79
|
+
<RemotePluginCardHost
|
|
80
|
+
plugin={plugin}
|
|
81
|
+
navigate={navigate}
|
|
82
|
+
fetchFn={fetchFn}
|
|
83
|
+
getTheme={() => demoTheme}
|
|
84
|
+
storage={window.localStorage}
|
|
85
|
+
decision={decision}
|
|
86
|
+
ontology={ontology}
|
|
87
|
+
renderModal={renderDefaultPluginModal}
|
|
88
|
+
styleIsolation="shadow"
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
/>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
) : null}
|
|
96
|
+
</aside>
|
|
97
|
+
</main>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default App
|
package/app/src/main.tsx
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import App from './App'
|
|
4
|
+
import './styles.css'
|
|
5
|
+
|
|
6
|
+
const container = document.getElementById('root')
|
|
7
|
+
|
|
8
|
+
if (!container) {
|
|
9
|
+
throw new Error('Root container #root was not found.')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
createRoot(container).render(
|
|
13
|
+
<StrictMode>
|
|
14
|
+
<App />
|
|
15
|
+
</StrictMode>
|
|
16
|
+
)
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
OntologyPermission,
|
|
3
|
+
PluginDecisionPayload,
|
|
4
|
+
PluginHttpPayload,
|
|
5
|
+
PluginListItem,
|
|
6
|
+
PluginOntologyPayload,
|
|
7
|
+
PluginTheme,
|
|
8
|
+
} from '@zenk-agent/plugin-types'
|
|
9
|
+
|
|
10
|
+
export const demoTheme: PluginTheme = {
|
|
11
|
+
mode: 'dark',
|
|
12
|
+
tokens: {
|
|
13
|
+
accent: '#fbbf24',
|
|
14
|
+
surface: '#111827',
|
|
15
|
+
text: '#f8fafc',
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const navigate = (to: string, options?: { replace?: boolean }) => {
|
|
20
|
+
const mode = options?.replace ? 'replace' : 'push'
|
|
21
|
+
console.info(`[tool-plugins-preview:navigate] ${mode}`, to)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type OntologyPreviewMode = 'mock' | 'remote'
|
|
25
|
+
type DecisionPreviewMode = 'mock' | 'remote'
|
|
26
|
+
|
|
27
|
+
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, '')
|
|
28
|
+
const normalizePath = (path: string) => (path.startsWith('/') ? path : `/${path}`)
|
|
29
|
+
|
|
30
|
+
const ontologyBaseUrl = trimTrailingSlash(String(import.meta.env.VITE_PLUGIN_ONTOLOGY_BASE_URL ?? ''))
|
|
31
|
+
const ontologyProxyPrefix = trimTrailingSlash(
|
|
32
|
+
String(import.meta.env.VITE_PLUGIN_ONTOLOGY_PROXY_PREFIX ?? '/__plugin-preview-ontology')
|
|
33
|
+
)
|
|
34
|
+
const ontologySchemasPath = String(import.meta.env.VITE_SERVER_ONTOLOGY_SCHEMAS_PATH ?? '/api/ontology/schemas')
|
|
35
|
+
const ontologyCypherQueryPath = String(import.meta.env.VITE_SERVER_ONTOLOGY_CYPHER_QUERY_PATH ?? '/api/ontology/cypher/query')
|
|
36
|
+
const decisionBaseUrl = trimTrailingSlash(String(import.meta.env.VITE_PLUGIN_DECISION_BASE_URL ?? ''))
|
|
37
|
+
const decisionProxyPrefix = trimTrailingSlash(
|
|
38
|
+
String(import.meta.env.VITE_PLUGIN_DECISION_PROXY_PREFIX ?? '/__plugin-preview-decision')
|
|
39
|
+
)
|
|
40
|
+
const decisionInvokePath = String(import.meta.env.VITE_SERVER_DECISION_INVOKE_PATH ?? '/api/decision/invoke')
|
|
41
|
+
|
|
42
|
+
const ontologyMode = ((): OntologyPreviewMode => {
|
|
43
|
+
const raw = String(import.meta.env.VITE_PLUGIN_ONTOLOGY_MODE ?? 'mock').trim().toLowerCase()
|
|
44
|
+
return raw === 'remote' ? 'remote' : 'mock'
|
|
45
|
+
})()
|
|
46
|
+
|
|
47
|
+
const decisionMode = ((): DecisionPreviewMode => {
|
|
48
|
+
const raw = String(import.meta.env.VITE_PLUGIN_DECISION_MODE ?? 'mock').trim().toLowerCase()
|
|
49
|
+
return raw === 'remote' ? 'remote' : 'mock'
|
|
50
|
+
})()
|
|
51
|
+
|
|
52
|
+
const readHttpResponse = async (response: Response) => {
|
|
53
|
+
if (response.status === 204) return null
|
|
54
|
+
|
|
55
|
+
const contentType = response.headers.get('content-type') ?? ''
|
|
56
|
+
if (contentType.includes('application/json')) {
|
|
57
|
+
return response.json()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return response.text()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const isApiEnvelope = <T>(value: unknown): value is { code: number; message: string; data: T } => {
|
|
64
|
+
if (!value || typeof value !== 'object') return false
|
|
65
|
+
const candidate = value as Partial<{ code: number; message: string; data: T }>
|
|
66
|
+
return typeof candidate.code === 'number' && typeof candidate.message === 'string' && 'data' in candidate
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const unwrapApiResponse = <T>(payload: unknown): T => {
|
|
70
|
+
if (!isApiEnvelope<T>(payload)) {
|
|
71
|
+
throw new Error('Unexpected server response format')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (payload.code !== 200) {
|
|
75
|
+
throw new Error(payload.message || `Request failed with code ${payload.code}`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return payload.data
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const fetchJson = async (input: string) => {
|
|
82
|
+
const response = await fetch(input, {
|
|
83
|
+
method: 'GET',
|
|
84
|
+
credentials: 'include',
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
const detail = await readHttpResponse(response)
|
|
89
|
+
throw new Error(
|
|
90
|
+
typeof detail === 'string' && detail.trim()
|
|
91
|
+
? detail
|
|
92
|
+
: `Ontology request failed with status ${response.status}`
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
return unwrapApiResponse(await readHttpResponse(response))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const postJson = async (input: string, body: unknown) => {
|
|
99
|
+
const response = await fetch(input, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
credentials: 'include',
|
|
102
|
+
headers: {
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify(body),
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const detail = await readHttpResponse(response)
|
|
110
|
+
throw new Error(
|
|
111
|
+
typeof detail === 'string' && detail.trim()
|
|
112
|
+
? detail
|
|
113
|
+
: `Ontology request failed with status ${response.status}`
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
return unwrapApiResponse(await readHttpResponse(response))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const toFiniteNumber = (value: unknown, fallback: number, validator: (value: number) => boolean) => {
|
|
120
|
+
const parsed =
|
|
121
|
+
typeof value === 'number' && Number.isFinite(value)
|
|
122
|
+
? value
|
|
123
|
+
: Number(value)
|
|
124
|
+
|
|
125
|
+
return Number.isFinite(parsed) && validator(parsed) ? parsed : fallback
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const buildOntologyInstancesPath = (schemaKey: string) =>
|
|
129
|
+
`${normalizePath(ontologySchemasPath)}/${encodeURIComponent(schemaKey)}/instances`
|
|
130
|
+
|
|
131
|
+
const buildOntologyInstanceDetailPath = (
|
|
132
|
+
schemaKey: string,
|
|
133
|
+
{ entityId, entityName }: { entityId?: string; entityName?: string }
|
|
134
|
+
) => {
|
|
135
|
+
const normalizedEntityId = trimToUndefined(entityId)
|
|
136
|
+
if (normalizedEntityId) {
|
|
137
|
+
return `${buildOntologyInstancesPath(schemaKey)}/${encodeURIComponent(normalizedEntityId)}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const normalizedEntityName = trimToUndefined(entityName)
|
|
141
|
+
if (normalizedEntityName) {
|
|
142
|
+
return `${buildOntologyInstancesPath(schemaKey)}/by-name/${encodeURIComponent(normalizedEntityName)}`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw new Error('entityId or entityName is required')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const buildOntologyApiUrl = (path: string, query?: Record<string, unknown>) => {
|
|
149
|
+
const basePath = ontologyBaseUrl
|
|
150
|
+
? `${normalizePath(ontologyProxyPrefix)}${normalizePath(path)}`
|
|
151
|
+
: normalizePath(path)
|
|
152
|
+
const url = new URL(basePath, window.location.origin)
|
|
153
|
+
|
|
154
|
+
if (query) {
|
|
155
|
+
for (const [key, value] of Object.entries(query)) {
|
|
156
|
+
if (value == null || value === '') continue
|
|
157
|
+
|
|
158
|
+
if (Array.isArray(value)) {
|
|
159
|
+
for (const item of value) {
|
|
160
|
+
if (item != null && item !== '') {
|
|
161
|
+
url.searchParams.append(key, String(item))
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
url.searchParams.set(key, String(value))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return url.toString()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const buildDecisionApiUrl = (path: string) => {
|
|
175
|
+
const basePath = decisionBaseUrl
|
|
176
|
+
? `${normalizePath(decisionProxyPrefix)}${normalizePath(path)}`
|
|
177
|
+
: normalizePath(path)
|
|
178
|
+
return new URL(basePath, window.location.origin).toString()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const trimToUndefined = (value?: string) => {
|
|
182
|
+
const nextValue = value?.trim()
|
|
183
|
+
return nextValue ? nextValue : undefined
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const queryParamValue = (query: Record<string, unknown> | undefined, keys: string[]) => {
|
|
187
|
+
if (!query) return undefined
|
|
188
|
+
|
|
189
|
+
for (const key of keys) {
|
|
190
|
+
const value = query[key]
|
|
191
|
+
if (value != null && value !== '') return value
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return undefined
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const previewOntologyConfig = {
|
|
198
|
+
mode: ontologyMode,
|
|
199
|
+
baseUrl: ontologyBaseUrl,
|
|
200
|
+
proxyPrefix: ontologyProxyPrefix,
|
|
201
|
+
schemasPath: ontologySchemasPath,
|
|
202
|
+
cypherQueryPath: ontologyCypherQueryPath,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const previewDecisionConfig = {
|
|
206
|
+
mode: decisionMode,
|
|
207
|
+
baseUrl: decisionBaseUrl,
|
|
208
|
+
proxyPrefix: decisionProxyPrefix,
|
|
209
|
+
invokePath: decisionInvokePath,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const fetchFn = async (request: PluginHttpPayload, context: { plugin: PluginListItem; token?: string | null }) => {
|
|
213
|
+
const headers = new Headers(request.headers ?? {})
|
|
214
|
+
if (context.token) headers.set('Authorization', `Bearer ${context.token}`)
|
|
215
|
+
|
|
216
|
+
console.info('[tool-plugins-preview:http]', context.plugin.id, request.url, {
|
|
217
|
+
method: request.method ?? 'GET',
|
|
218
|
+
token: context.token ?? null,
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const response = await fetch(request.url, {
|
|
222
|
+
...request,
|
|
223
|
+
headers,
|
|
224
|
+
credentials: request.credentials ?? 'include',
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
const detail = await readHttpResponse(response)
|
|
229
|
+
throw new Error(
|
|
230
|
+
typeof detail === 'string' && detail.trim()
|
|
231
|
+
? detail
|
|
232
|
+
: `HTTP request failed with status ${response.status}`
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return readHttpResponse(response)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export const decision = async (request: PluginDecisionPayload, context: { plugin: PluginListItem }) => {
|
|
240
|
+
console.info('[tool-plugins-preview:decision]', context.plugin.id, request)
|
|
241
|
+
|
|
242
|
+
if (decisionMode === 'remote') {
|
|
243
|
+
return postJson(buildDecisionApiUrl(decisionInvokePath), request)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (request.method === 'optimizer.pareto') {
|
|
247
|
+
return [
|
|
248
|
+
{ v1: 0.982, v2: 0.914, v3: 0.043, v4: 0.876, v5: 0.903, v6: 16, v7: 82.5, v8: 138.4, v9: 4, v10: 46.2, score: 0.926 },
|
|
249
|
+
{ v1: 0.967, v2: 0.943, v3: 0.051, v4: 0.891, v5: 0.881, v6: 15, v7: 79.8, v8: 142.6, v9: 5, v10: 44.7, score: 0.918 },
|
|
250
|
+
{ v1: 0.951, v2: 0.927, v3: 0.038, v4: 0.862, v5: 0.914, v6: 17, v7: 84.1, v8: 135.2, v9: 4, v10: 47.3, score: 0.923 },
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
method: request.method,
|
|
256
|
+
params: request.params ?? {},
|
|
257
|
+
summary: 'mock decision result',
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export const ontology = async (
|
|
262
|
+
request: PluginOntologyPayload,
|
|
263
|
+
context: { plugin: PluginListItem; permission: OntologyPermission }
|
|
264
|
+
) => {
|
|
265
|
+
console.info('[tool-plugins-preview:ontology]', context.plugin.id, request, context.permission)
|
|
266
|
+
|
|
267
|
+
if (ontologyMode === 'remote') {
|
|
268
|
+
if (request.action === 'executeCypher') {
|
|
269
|
+
return postJson(buildOntologyApiUrl(ontologyCypherQueryPath), {
|
|
270
|
+
cypher: request.cypher,
|
|
271
|
+
params: request.params,
|
|
272
|
+
limit: request.limit,
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (request.action === 'listConcepts') {
|
|
277
|
+
const query = request.query ?? {}
|
|
278
|
+
const limit = toFiniteNumber(query.limit, 8, (value) => value > 0)
|
|
279
|
+
const offset = toFiniteNumber(request.query?.offset, 0, (value) => value >= 0)
|
|
280
|
+
const rawEntityIds = query.entity_ids ?? query.entityIds
|
|
281
|
+
const entityIds = Array.isArray(rawEntityIds)
|
|
282
|
+
? rawEntityIds.map((value) => String(value).trim()).filter(Boolean)
|
|
283
|
+
: typeof rawEntityIds === 'string'
|
|
284
|
+
? rawEntityIds
|
|
285
|
+
.split(',')
|
|
286
|
+
.map((value) => value.trim())
|
|
287
|
+
.filter(Boolean)
|
|
288
|
+
: undefined
|
|
289
|
+
|
|
290
|
+
return fetchJson(
|
|
291
|
+
buildOntologyApiUrl(buildOntologyInstancesPath(request.conceptType), {
|
|
292
|
+
limit,
|
|
293
|
+
offset,
|
|
294
|
+
search: typeof query.search === 'string' ? query.search : undefined,
|
|
295
|
+
entity_ids: entityIds?.length ? entityIds.join(',') : undefined,
|
|
296
|
+
})
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return fetchJson(
|
|
301
|
+
buildOntologyApiUrl(buildOntologyInstanceDetailPath(request.conceptType, {
|
|
302
|
+
entityId: request.entityId ?? request.nodeId ?? undefined,
|
|
303
|
+
entityName: request.entityName,
|
|
304
|
+
}))
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (request.action === 'executeCypher') {
|
|
309
|
+
return {
|
|
310
|
+
columns: ['value'],
|
|
311
|
+
rows: [{ value: 'mock cypher result' }],
|
|
312
|
+
rowCount: 1,
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (request.action === 'listConcepts') {
|
|
317
|
+
const pageSize = toFiniteNumber(request.query?.limit, 8, (value) => value > 0)
|
|
318
|
+
const offset = toFiniteNumber(request.query?.offset, 0, (value) => value >= 0)
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
list: [
|
|
322
|
+
{
|
|
323
|
+
id: `${request.conceptType}:demo-1`,
|
|
324
|
+
label: `${request.conceptType} Demo Node`,
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
total: 1,
|
|
328
|
+
page: Math.floor(offset / pageSize) + 1,
|
|
329
|
+
pageSize,
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return request.nodeId
|
|
334
|
+
? {
|
|
335
|
+
id: request.nodeId,
|
|
336
|
+
label: `${request.conceptType} Demo Detail`,
|
|
337
|
+
}
|
|
338
|
+
: null
|
|
339
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
color: #e2e8f0;
|
|
7
|
+
background-color: #020617;
|
|
8
|
+
font-family:
|
|
9
|
+
"IBM Plex Sans",
|
|
10
|
+
"Segoe UI",
|
|
11
|
+
sans-serif;
|
|
12
|
+
font-synthesis: none;
|
|
13
|
+
text-rendering: optimizeLegibility;
|
|
14
|
+
-webkit-font-smoothing: antialiased;
|
|
15
|
+
-moz-osx-font-smoothing: grayscale;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
* {
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
html,
|
|
23
|
+
body,
|
|
24
|
+
#root {
|
|
25
|
+
min-height: 100%;
|
|
26
|
+
margin: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
min-width: 320px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
button,
|
|
34
|
+
input,
|
|
35
|
+
textarea,
|
|
36
|
+
select {
|
|
37
|
+
font: inherit;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.scrollbar-thin {
|
|
41
|
+
scrollbar-width: thin;
|
|
42
|
+
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.scrollbar-thin::-webkit-scrollbar {
|
|
46
|
+
width: 6px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.scrollbar-thin::-webkit-scrollbar-track {
|
|
50
|
+
background: transparent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.scrollbar-thin::-webkit-scrollbar-thumb {
|
|
54
|
+
background-color: rgba(255, 255, 255, 0.15);
|
|
55
|
+
border-radius: 3px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
|
59
|
+
background-color: rgba(255, 255, 255, 0.25);
|
|
60
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createPreviewServer, formatPreviewUrl, parsePreviewCliArgs } from '../dist/cli.mjs'
|
|
3
|
+
|
|
4
|
+
const argv = process.argv.slice(2)
|
|
5
|
+
const { command, host, pluginDir, port } = parsePreviewCliArgs(argv)
|
|
6
|
+
|
|
7
|
+
if (command !== 'dev') {
|
|
8
|
+
console.error(`Unsupported command: ${command}`)
|
|
9
|
+
console.error('Usage: zenk-plugin-preview dev [plugin-dir] [--host 0.0.0.0] [--port 4175]')
|
|
10
|
+
process.exit(1)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const server = await createPreviewServer({
|
|
14
|
+
host,
|
|
15
|
+
pluginDir,
|
|
16
|
+
port,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
await server.listen()
|
|
20
|
+
|
|
21
|
+
const resolvedUrls = server.resolvedUrls?.local ?? server.resolvedUrls?.network ?? []
|
|
22
|
+
const url = resolvedUrls[0] ?? formatPreviewUrl(host, port)
|
|
23
|
+
|
|
24
|
+
console.log(`[zenk-plugin-preview] plugin: ${pluginDir}`)
|
|
25
|
+
console.log(`[zenk-plugin-preview] ready: ${url}`)
|
|
26
|
+
|
|
27
|
+
const closeServer = async () => {
|
|
28
|
+
await server.close()
|
|
29
|
+
process.exit(0)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
process.on('SIGINT', closeServer)
|
|
33
|
+
process.on('SIGTERM', closeServer)
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { dirname as N, resolve as i, basename as L, extname as H } from "node:path";
|
|
2
|
+
import { fileURLToPath as B } from "node:url";
|
|
3
|
+
import { realpathSync as z, readFileSync as R, existsSync as x } from "node:fs";
|
|
4
|
+
import { createRequire as S } from "node:module";
|
|
5
|
+
import { createServer as A, normalizePath as g } from "vite";
|
|
6
|
+
import J from "@vitejs/plugin-react";
|
|
7
|
+
import F from "autoprefixer";
|
|
8
|
+
import X from "tailwindcss";
|
|
9
|
+
const D = "preview.config.json", j = "virtual:tool-plugin-preview", k = `\0${j}`, W = "internal.plugin-preview-placeholder", Y = "0.0.0-placeholder", c = (e) => typeof e == "object" && e !== null, v = (e) => typeof e == "string" && e.trim().length > 0, V = (e) => v(e) ? e.trim() : void 0, q = (e) => {
|
|
10
|
+
if (!x(e)) return null;
|
|
11
|
+
const t = JSON.parse(R(e, "utf8"));
|
|
12
|
+
if (!c(t))
|
|
13
|
+
throw new Error(`Expected a JSON object in ${e}`);
|
|
14
|
+
return t;
|
|
15
|
+
}, b = (e) => q(i(e, D)), u = (e, t) => {
|
|
16
|
+
let r = e;
|
|
17
|
+
for (const n of t) {
|
|
18
|
+
if (!c(r) || !(n in r)) return;
|
|
19
|
+
r = r[n];
|
|
20
|
+
}
|
|
21
|
+
return r;
|
|
22
|
+
}, $ = (e) => {
|
|
23
|
+
if (!v(e))
|
|
24
|
+
throw new Error(`Invalid proxy prefix "${String(e)}": expected a non-empty string`);
|
|
25
|
+
const t = e.trim();
|
|
26
|
+
return (t.startsWith("/") ? t : `/${t}`).replace(/\/+$/, "") || "/";
|
|
27
|
+
}, K = (e, t) => {
|
|
28
|
+
const r = $(e);
|
|
29
|
+
if (v(t))
|
|
30
|
+
return [r, { target: t.trim(), changeOrigin: !0, secure: !1 }];
|
|
31
|
+
if (!c(t) || !v(t.target))
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Invalid proxy config for "${r}": expected a target string or an object with a "target" field`
|
|
34
|
+
);
|
|
35
|
+
return [
|
|
36
|
+
r,
|
|
37
|
+
{
|
|
38
|
+
target: t.target.trim(),
|
|
39
|
+
changeOrigin: typeof t.changeOrigin == "boolean" ? t.changeOrigin : !0,
|
|
40
|
+
secure: typeof t.secure == "boolean" ? t.secure : !1
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
}, Q = (e) => {
|
|
44
|
+
if (!c(e)) return [];
|
|
45
|
+
const t = u(e, ["proxy"]);
|
|
46
|
+
return c(t) ? Object.entries(t).map(([r, n]) => K(r, n)) : [];
|
|
47
|
+
}, Z = (e, t) => {
|
|
48
|
+
if (!c(e) && !c(t)) return null;
|
|
49
|
+
const r = c(e) ? e : {}, n = c(t) ? t : {};
|
|
50
|
+
return {
|
|
51
|
+
...r,
|
|
52
|
+
...n,
|
|
53
|
+
proxy: { ...c(r.proxy) ? r.proxy : {}, ...c(n.proxy) ? n.proxy : {} },
|
|
54
|
+
ontology: {
|
|
55
|
+
...c(r.ontology) ? r.ontology : {},
|
|
56
|
+
...c(n.ontology) ? n.ontology : {}
|
|
57
|
+
},
|
|
58
|
+
decision: {
|
|
59
|
+
...c(r.decision) ? r.decision : {},
|
|
60
|
+
...c(n.decision) ? n.decision : {}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}, ee = (e, t) => {
|
|
64
|
+
const r = e.zenk?.publicPlugins, n = e.zenk?.publicPlugin;
|
|
65
|
+
return c(r) ? Object.entries(r).map(([o, s]) => {
|
|
66
|
+
if (!c(s))
|
|
67
|
+
throw new Error(`Invalid zenk.publicPlugins.${o} in ${t}`);
|
|
68
|
+
return { entryName: o, configExport: s.configExport ?? s.manifestExport, workerExport: s.workerExport };
|
|
69
|
+
}) : c(n) ? [{ entryName: L(String(n.workerExport ?? "worker"), H(String(n.workerExport ?? "worker"))), configExport: n.configExport ?? n.manifestExport, workerExport: n.workerExport }] : [];
|
|
70
|
+
}, te = (e) => e.replace(/<parent>[\s\S]*?<\/parent>/, ""), w = (e, t) => {
|
|
71
|
+
const r = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), n = e.match(new RegExp(`<${r}>([\\s\\S]*?)</${r}>`));
|
|
72
|
+
return n ? n[1].trim() : null;
|
|
73
|
+
}, re = (e) => {
|
|
74
|
+
const t = i(e, "../pom.xml");
|
|
75
|
+
if (!x(t)) return null;
|
|
76
|
+
const r = te(R(t, "utf8")), n = w(r, "properties") ?? "", o = w(n, "plugin.id") || w(r, "artifactId"), s = w(r, "version");
|
|
77
|
+
return o && s ? { pluginId: o, pluginVersion: s } : null;
|
|
78
|
+
}, ne = (e) => {
|
|
79
|
+
const t = [];
|
|
80
|
+
for (const r of e) {
|
|
81
|
+
const n = i(r, "package.json"), o = JSON.parse(R(n, "utf8")), s = ee(o, n);
|
|
82
|
+
for (const { entryName: l, configExport: a, workerExport: d } of s) {
|
|
83
|
+
if (typeof a != "string" || typeof d != "string")
|
|
84
|
+
throw new Error(`Invalid public plugin config in ${n}`);
|
|
85
|
+
const m = o.exports?.[a], f = o.exports?.[d];
|
|
86
|
+
if (typeof m != "string" || typeof f != "string")
|
|
87
|
+
throw new Error(`Missing exports for public plugin in ${n}`);
|
|
88
|
+
t.push({
|
|
89
|
+
entryName: l,
|
|
90
|
+
configExport: a,
|
|
91
|
+
workerExport: d,
|
|
92
|
+
configModulePath: i(r, m),
|
|
93
|
+
workerModulePath: i(r, f),
|
|
94
|
+
packageDir: r,
|
|
95
|
+
packageJson: o,
|
|
96
|
+
packageJsonPath: n,
|
|
97
|
+
packageName: L(r),
|
|
98
|
+
pomIdentity: re(r)
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return t;
|
|
103
|
+
}, oe = N(z(B(import.meta.url))), U = i(oe, ".."), _ = i(U, "app"), h = i(U, ".."), ie = i(h, ".."), se = i(h, "plugin-host"), ce = i(h, "plugin-types"), M = (e, t, r) => {
|
|
104
|
+
const n = i(e, t);
|
|
105
|
+
return x(n) ? n : i(e, r);
|
|
106
|
+
}, y = (e, t) => M(se, e, t), le = (e, t) => M(ce, e, t), ae = S(import.meta.url), T = (e, t, r) => ae.resolve(e).replace(t, r), ue = T("@remote-dom/react/host", "/build/cjs/host.cjs", "/build/esm/host.mjs"), pe = T("@remote-dom/react", "/build/cjs/index.cjs", "/build/esm/index.mjs"), de = (e, t) => S(i(e, "package.json")).resolve(t), fe = (e) => {
|
|
107
|
+
const t = b(ie), r = b(e), n = Z(t, r);
|
|
108
|
+
return { workspaceConfig: t, pluginConfig: r, resolvedConfig: n };
|
|
109
|
+
}, ge = (e) => {
|
|
110
|
+
const t = Q(e);
|
|
111
|
+
if (t.length === 0)
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Invalid preview config: "${D}" must contain at least one entry under "proxy"`
|
|
114
|
+
);
|
|
115
|
+
return Object.fromEntries(t);
|
|
116
|
+
}, me = (e) => (V(u(e, ["ontology", "baseUrl"])) ?? "").replace(/\/+$/, ""), Pe = (e) => $(u(e, ["ontology", "proxyPrefix"]) ?? "/__plugin-preview-ontology"), ye = (e) => (V(u(e, ["decision", "baseUrl"])) ?? "").replace(/\/+$/, ""), he = (e) => $(u(e, ["decision", "proxyPrefix"]) ?? "/__plugin-preview-decision"), Ee = (e) => {
|
|
117
|
+
const t = {
|
|
118
|
+
VITE_PLUGIN_ONTOLOGY_MODE: u(e, ["ontology", "mode"]) ?? "mock",
|
|
119
|
+
VITE_PLUGIN_ONTOLOGY_BASE_URL: u(e, ["ontology", "baseUrl"]),
|
|
120
|
+
VITE_PLUGIN_ONTOLOGY_PROXY_PREFIX: u(e, ["ontology", "proxyPrefix"]),
|
|
121
|
+
VITE_SERVER_ONTOLOGY_SCHEMAS_PATH: u(e, ["ontology", "schemasPath"]),
|
|
122
|
+
VITE_PLUGIN_DECISION_MODE: u(e, ["decision", "mode"]) ?? "mock",
|
|
123
|
+
VITE_PLUGIN_DECISION_BASE_URL: u(e, ["decision", "baseUrl"]),
|
|
124
|
+
VITE_PLUGIN_DECISION_PROXY_PREFIX: u(e, ["decision", "proxyPrefix"]),
|
|
125
|
+
VITE_SERVER_DECISION_INVOKE_PATH: u(e, ["decision", "invokePath"])
|
|
126
|
+
};
|
|
127
|
+
return Object.fromEntries(
|
|
128
|
+
Object.entries(t).filter(([r, n]) => r.startsWith("VITE_") && typeof n == "string").map(([r, n]) => [`import.meta.env.${r}`, JSON.stringify(n)])
|
|
129
|
+
);
|
|
130
|
+
}, we = (e, t) => {
|
|
131
|
+
const r = [], n = [];
|
|
132
|
+
return e.forEach((o, s) => {
|
|
133
|
+
t(o.packageJsonPath), t(o.configModulePath), t(o.workerModulePath);
|
|
134
|
+
const l = `configModule${s}`, a = `workerUrl${s}`, d = `styleUrl${s}`, m = `/@fs${g(o.configModulePath)}`, f = `/@fs${g(o.workerModulePath)}`, I = `/@fs${g(i(o.packageDir, "resources"))}`, P = i(N(o.workerModulePath), "styles.css"), E = x(P);
|
|
135
|
+
r.push(`import * as ${l} from '${m}'`), r.push(`import ${a} from '${f}?worker&url'`), E && (t(P), r.push(`import ${d} from '/@fs${g(P)}?url'`));
|
|
136
|
+
const O = E ? `[${d}]` : "plugin.manifest.styleEntryUrls ?? []";
|
|
137
|
+
n.push(`
|
|
138
|
+
...resolvePluginModule(${l}, ${JSON.stringify(o.pomIdentity)}).map((plugin) => {
|
|
139
|
+
const previewPlugin = withPreviewResourceUrls(plugin, ${JSON.stringify(I)})
|
|
140
|
+
return {
|
|
141
|
+
...previewPlugin,
|
|
142
|
+
manifest: {
|
|
143
|
+
...previewPlugin.manifest,
|
|
144
|
+
workerEntryUrl: ${a},
|
|
145
|
+
styleEntryUrls: ${O},
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
})`);
|
|
149
|
+
}), `
|
|
150
|
+
${r.join(`
|
|
151
|
+
`)}
|
|
152
|
+
|
|
153
|
+
const PLACEHOLDER_PLUGIN_ID = ${JSON.stringify(W)}
|
|
154
|
+
const PLACEHOLDER_PLUGIN_VERSION = ${JSON.stringify(Y)}
|
|
155
|
+
|
|
156
|
+
const isRecord = (value) => typeof value === 'object' && value !== null
|
|
157
|
+
|
|
158
|
+
const isPluginListItem = (value) =>
|
|
159
|
+
isRecord(value) &&
|
|
160
|
+
typeof value.id === 'string' &&
|
|
161
|
+
isRecord(value.manifest) &&
|
|
162
|
+
typeof value.manifest.id === 'string' &&
|
|
163
|
+
typeof value.manifest.runtime === 'string'
|
|
164
|
+
|
|
165
|
+
const isPluginSourceDefinition = (value) =>
|
|
166
|
+
isRecord(value) &&
|
|
167
|
+
isRecord(value.manifest) &&
|
|
168
|
+
typeof value.manifest.name === 'string' &&
|
|
169
|
+
typeof value.manifest.runtime === 'string'
|
|
170
|
+
|
|
171
|
+
const assertSourceManifestDoesNotOwnGeneratedFields = (definition) => {
|
|
172
|
+
if ('id' in definition) {
|
|
173
|
+
throw new Error('Plugin source definition must not define generated field "id".')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const field of ['id', 'version', 'workerEntryUrl', 'styleEntryUrls']) {
|
|
177
|
+
if (field in definition.manifest) {
|
|
178
|
+
throw new Error(\`Plugin source manifest must not define generated field "\${field}".\`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const composePluginDefinition = (definition, identity) => {
|
|
184
|
+
if (isPluginListItem(definition)) {
|
|
185
|
+
const pluginId = identity?.pluginId ?? definition.id
|
|
186
|
+
const pluginVersion = identity?.pluginVersion ?? definition.manifest.version
|
|
187
|
+
return {
|
|
188
|
+
...definition,
|
|
189
|
+
id: pluginId,
|
|
190
|
+
manifest: {
|
|
191
|
+
...definition.manifest,
|
|
192
|
+
id: pluginId,
|
|
193
|
+
version: pluginVersion,
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!isPluginSourceDefinition(definition)) return null
|
|
199
|
+
const pluginId = identity?.pluginId ?? PLACEHOLDER_PLUGIN_ID
|
|
200
|
+
const pluginVersion = identity?.pluginVersion ?? PLACEHOLDER_PLUGIN_VERSION
|
|
201
|
+
|
|
202
|
+
assertSourceManifestDoesNotOwnGeneratedFields(definition)
|
|
203
|
+
return {
|
|
204
|
+
id: pluginId,
|
|
205
|
+
enabled: definition.enabled,
|
|
206
|
+
config: definition.config,
|
|
207
|
+
manifest: {
|
|
208
|
+
...definition.manifest,
|
|
209
|
+
id: pluginId,
|
|
210
|
+
version: pluginVersion,
|
|
211
|
+
workerEntryUrl: '',
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const resolvePluginModule = (moduleExports, identity) =>
|
|
217
|
+
Object.values(moduleExports)
|
|
218
|
+
.map((definition) => composePluginDefinition(definition, identity))
|
|
219
|
+
.filter(Boolean)
|
|
220
|
+
|
|
221
|
+
const resolvePreviewResourceUrl = (value, resourceBaseUrl) => {
|
|
222
|
+
if (typeof value !== 'string') return value
|
|
223
|
+
if (value.startsWith('./resources/')) return resourceBaseUrl + '/' + value.slice('./resources/'.length)
|
|
224
|
+
if (value.startsWith('resources/')) return resourceBaseUrl + '/' + value.slice('resources/'.length)
|
|
225
|
+
return value
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const rewritePreviewResourceItem = (item, resourceBaseUrl) =>
|
|
229
|
+
isRecord(item)
|
|
230
|
+
? {
|
|
231
|
+
...item,
|
|
232
|
+
href: resolvePreviewResourceUrl(item.href, resourceBaseUrl),
|
|
233
|
+
}
|
|
234
|
+
: item
|
|
235
|
+
|
|
236
|
+
const rewritePreviewResources = (resources, resourceBaseUrl) =>
|
|
237
|
+
isRecord(resources)
|
|
238
|
+
? Object.fromEntries(Object.entries(resources).map(([key, value]) => [key, rewritePreviewResourceItem(value, resourceBaseUrl)]))
|
|
239
|
+
: resources
|
|
240
|
+
|
|
241
|
+
const withPreviewResourceUrls = (plugin, resourceBaseUrl) => {
|
|
242
|
+
const display = plugin.manifest.display
|
|
243
|
+
if (!isRecord(display)) return plugin
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
...plugin,
|
|
247
|
+
manifest: {
|
|
248
|
+
...plugin.manifest,
|
|
249
|
+
display: {
|
|
250
|
+
...display,
|
|
251
|
+
resources: rewritePreviewResources(display.resources, resourceBaseUrl),
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export const previewPluginDir = ${JSON.stringify(g(e[0]?.packageDir ?? ""))}
|
|
258
|
+
export const previewPlugins = [
|
|
259
|
+
${n.join(`,
|
|
260
|
+
`)}
|
|
261
|
+
]
|
|
262
|
+
.filter((plugin) => plugin.enabled !== false)
|
|
263
|
+
.sort((a, b) => a.manifest.order - b.manifest.order)
|
|
264
|
+
`;
|
|
265
|
+
}, ve = (e) => ({
|
|
266
|
+
name: "tool-plugin-preview-module",
|
|
267
|
+
resolveId(t) {
|
|
268
|
+
if (t === j) return k;
|
|
269
|
+
},
|
|
270
|
+
load(t) {
|
|
271
|
+
if (t !== k) return null;
|
|
272
|
+
const r = ne([e]);
|
|
273
|
+
return we(r, this.addWatchFile.bind(this));
|
|
274
|
+
},
|
|
275
|
+
handleHotUpdate(t) {
|
|
276
|
+
const r = g(e);
|
|
277
|
+
if (!g(t.file).startsWith(r)) return;
|
|
278
|
+
const n = t.server.moduleGraph.getModuleById(k);
|
|
279
|
+
n && t.server.moduleGraph.invalidateModule(n), t.server.ws.send({ type: "full-reload" });
|
|
280
|
+
}
|
|
281
|
+
}), be = (e) => {
|
|
282
|
+
const t = [...e], r = t.shift() ?? "dev";
|
|
283
|
+
let n = "0.0.0.0", o = process.cwd(), s = 4175;
|
|
284
|
+
for (; t.length > 0; ) {
|
|
285
|
+
const l = t.shift();
|
|
286
|
+
if (!l) break;
|
|
287
|
+
if (l === "--host") {
|
|
288
|
+
n = t.shift() ?? n;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (l === "--port") {
|
|
292
|
+
const a = Number(t.shift());
|
|
293
|
+
Number.isFinite(a) && a > 0 && (s = a);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
l.startsWith("-") || (o = i(l));
|
|
297
|
+
}
|
|
298
|
+
return { command: r, host: n, pluginDir: o, port: s };
|
|
299
|
+
}, Ne = (e, t) => `http://${e === "0.0.0.0" ? "localhost" : e}:${t}/`, Le = async ({ host: e, pluginDir: t, port: r }) => {
|
|
300
|
+
const { resolvedConfig: n } = fe(t), o = ge(n), s = me(n), l = Pe(n), a = ye(n), d = he(n), m = Ee(n), f = (p) => de(t, p), I = f("preact"), P = f("preact/hooks"), E = f("preact/jsx-runtime"), O = f("preact/jsx-dev-runtime"), G = s ? {
|
|
301
|
+
[l]: {
|
|
302
|
+
target: s,
|
|
303
|
+
changeOrigin: !0,
|
|
304
|
+
secure: !1,
|
|
305
|
+
rewrite: (p) => p.replace(new RegExp(`^${l}`), "")
|
|
306
|
+
}
|
|
307
|
+
} : {}, C = a ? {
|
|
308
|
+
[d]: {
|
|
309
|
+
target: a,
|
|
310
|
+
changeOrigin: !0,
|
|
311
|
+
secure: !1,
|
|
312
|
+
rewrite: (p) => p.replace(new RegExp(`^${d}`), "")
|
|
313
|
+
}
|
|
314
|
+
} : {};
|
|
315
|
+
return A({
|
|
316
|
+
appType: "spa",
|
|
317
|
+
configFile: !1,
|
|
318
|
+
root: _,
|
|
319
|
+
plugins: [
|
|
320
|
+
ve(t),
|
|
321
|
+
J({
|
|
322
|
+
include: [/plugin-preview\/app\/src\/.*\.[jt]sx?$/, /plugin-host\/src\/.*\.[jt]sx?$/],
|
|
323
|
+
exclude: [/plugin-sdk\/src\/.*\.[jt]sx?$/]
|
|
324
|
+
})
|
|
325
|
+
],
|
|
326
|
+
resolve: {
|
|
327
|
+
alias: [
|
|
328
|
+
{ find: /^preact\/jsx-dev-runtime$/, replacement: O },
|
|
329
|
+
{ find: /^preact\/jsx-runtime$/, replacement: E },
|
|
330
|
+
{ find: /^preact\/hooks$/, replacement: P },
|
|
331
|
+
{ find: /^preact$/, replacement: I },
|
|
332
|
+
{ find: "@remote-dom/react/host", replacement: ue },
|
|
333
|
+
{ find: "@remote-dom/react", replacement: pe },
|
|
334
|
+
{ find: "@zenk-agent/plugin-host/RemotePluginCardHost", replacement: y("src/RemotePluginCardHost.tsx", "dist/RemotePluginCardHost.js") },
|
|
335
|
+
{ find: "@zenk-agent/plugin-host/hostCapabilities", replacement: y("src/hostCapabilities.ts", "dist/hostCapabilities.js") },
|
|
336
|
+
{ find: "@zenk-agent/plugin-host/loadPlugins", replacement: y("src/loadPlugins.ts", "dist/loadPlugins.js") },
|
|
337
|
+
{ find: "@zenk-agent/plugin-host/types", replacement: y("src/types.ts", "dist/types.js") },
|
|
338
|
+
{ find: "@zenk-agent/plugin-host", replacement: y("src/index.ts", "dist/index.js") },
|
|
339
|
+
{ find: "@zenk-agent/plugin-types", replacement: le("src/index.ts", "dist/index.js") }
|
|
340
|
+
],
|
|
341
|
+
dedupe: ["react", "react-dom", "preact"]
|
|
342
|
+
},
|
|
343
|
+
optimizeDeps: {
|
|
344
|
+
include: ["preact", "preact/hooks", "preact/jsx-runtime", "preact/jsx-dev-runtime", "react", "react/jsx-runtime", "react-dom", "@remote-dom/react/host"]
|
|
345
|
+
},
|
|
346
|
+
define: m,
|
|
347
|
+
css: {
|
|
348
|
+
postcss: {
|
|
349
|
+
plugins: [
|
|
350
|
+
X({
|
|
351
|
+
content: [
|
|
352
|
+
i(_, "index.html"),
|
|
353
|
+
`${i(_, "src")}/**/*.{ts,tsx}`,
|
|
354
|
+
`${i(h, "plugin-host/src")}/**/*.{ts,tsx}`,
|
|
355
|
+
`${i(t, "src")}/**/*.{ts,tsx}`
|
|
356
|
+
]
|
|
357
|
+
}),
|
|
358
|
+
F()
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
server: {
|
|
363
|
+
host: e,
|
|
364
|
+
port: r,
|
|
365
|
+
proxy: { ...o, ...G, ...C },
|
|
366
|
+
fs: { allow: [t, U, h] }
|
|
367
|
+
},
|
|
368
|
+
preview: { host: e, port: r },
|
|
369
|
+
build: {
|
|
370
|
+
rollupOptions: {
|
|
371
|
+
output: {
|
|
372
|
+
manualChunks(p) {
|
|
373
|
+
if (p.includes("node_modules/echarts")) return "echarts";
|
|
374
|
+
if (p.includes("node_modules/react") || p.includes("node_modules/react-dom") || p.includes("node_modules/@remote-dom")) return "runtime";
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
export {
|
|
382
|
+
Le as createPreviewServer,
|
|
383
|
+
ne as discoverPublicPluginEntriesFromPackageDirs,
|
|
384
|
+
Ne as formatPreviewUrl,
|
|
385
|
+
be as parsePreviewCliArgs
|
|
386
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zenk-agent/plugin-preview",
|
|
3
|
+
"version": "0.0.24",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/cli.mjs",
|
|
7
|
+
"module": "./dist/cli.mjs",
|
|
8
|
+
"bin": {
|
|
9
|
+
"zenk-plugin-preview": "./bin/zenk-plugin-preview.mjs"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/cli.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"dist",
|
|
17
|
+
"app"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"registry": "https://registry.npmjs.org/",
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@remote-dom/react": "^1.2.2",
|
|
25
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
26
|
+
"autoprefixer": "^10.4.21",
|
|
27
|
+
"postcss": "^8.5.6",
|
|
28
|
+
"react": "19.2.5",
|
|
29
|
+
"react-dom": "19.2.5",
|
|
30
|
+
"tailwindcss": "^3.4.17",
|
|
31
|
+
"vite": "^7.2.0",
|
|
32
|
+
"@zenk-agent/plugin-host": "0.0.24",
|
|
33
|
+
"@zenk-agent/plugin-sdk": "0.0.24",
|
|
34
|
+
"@zenk-agent/plugin-types": "0.0.24"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^24.10.1",
|
|
38
|
+
"@types/react": "^19.2.5",
|
|
39
|
+
"@types/react-dom": "^19.2.3",
|
|
40
|
+
"typescript": "~5.9.3"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"dev": "node ./bin/zenk-plugin-preview.mjs dev",
|
|
44
|
+
"build": "vite build",
|
|
45
|
+
"lint": "echo \"plugin-preview: no lint configured yet\"",
|
|
46
|
+
"typecheck": "echo \"plugin-preview: no typecheck configured yet\""
|
|
47
|
+
}
|
|
48
|
+
}
|