agentworkshop 0.3.0 → 0.4.1
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/LICENSE +91 -0
- package/README-zh.md +431 -424
- package/README.md +10 -3
- package/app/components/AppSidebar.vue +1 -0
- package/app/pages/plugins/index.vue +258 -0
- package/app/plugins/aw-plugins.client.ts +87 -47
- package/cli/commands/plugin.mjs +57 -7
- package/docs/plugins.md +133 -63
- package/docs/sdk.md +265 -0
- package/i18n/locales/en.ts +17 -0
- package/i18n/locales/zh-CN.ts +17 -0
- package/package.json +4 -2
- package/scripts/_dbg-app-shot.mjs +50 -0
- package/scripts/_dbg-docs-site-shot.mjs +32 -0
- package/scripts/home-bootstrap.mjs +26 -1
- package/sdk/client.mjs +6 -2
- package/sdk/examples/line-sentinel/client.mjs +31 -0
- package/sdk/examples/line-sentinel/index.mjs +102 -0
- package/sdk/examples/ops-notifier/client.mjs +23 -0
- package/sdk/examples/ops-notifier/index.mjs +34 -0
- package/server/api/workshop/plugins/[name]/disable.post.ts +19 -0
- package/server/api/workshop/plugins/[name]/enable.post.ts +19 -0
- package/server/api/workshop/plugins/[name]/index.get.ts +14 -0
- package/server/api/workshop/plugins/index.get.ts +17 -0
- package/server/data/daqs.json +68 -46
- package/server/data/dcw-lines.json +7 -0
- package/server/data/dcw-products.json +7 -0
- package/server/data/dcw-recipes.json +20 -0
- package/server/data/dcw-rollback.json +1 -1
- package/server/data/dcw-runs.json +25 -18
- package/server/data/dcws.json +21 -0
- package/server/data/line-runs.json +30 -20
- package/server/services/workshop/plugins/host.mjs +177 -49
package/README.md
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
[](https://www.typescriptlang.org)
|
|
12
12
|
[](https://nodejs.org)
|
|
13
13
|
[](https://nodejs.org/api/sqlite.html)
|
|
14
|
+
[](./LICENSE)
|
|
14
15
|
|
|
15
|
-
**[中文文档 →](./README-zh.md)**
|
|
16
|
+
**[中文文档 →](./README-zh.md)** · **[Online Docs →](https://kingdol666.github.io/AgentWorkShop/)**
|
|
16
17
|
|
|
17
18
|
*A configuration-driven platform where **AI agent teams** and an **industrial digital twin** share one runtime — agents query real telemetry, issue supervisory setpoints through human-approved write control, and every event streams live to a 3D twin.*
|
|
18
19
|
|
|
@@ -403,13 +404,19 @@ node scripts/_dbg-full-feature-e2e.mjs # full-feature live E2E (server must b
|
|
|
403
404
|
| Edge deployment shape: standalone edge-agent + central broker | Planned |
|
|
404
405
|
| Alarm outbound delivery (email/webhook) + ack workflow | Planned |
|
|
405
406
|
| CI pipeline (typecheck + lint + e2e) | Planned |
|
|
406
|
-
| License
|
|
407
|
+
| License: PolyForm Noncommercial 1.0.0 (source-available, non-commercial) | Shipped |
|
|
407
408
|
|
|
408
409
|
## License
|
|
409
410
|
|
|
410
411
|
AgentWorkShop is an independent project and is **not an official product of Anthropic** or any LLM vendor. It integrates with agent harnesses (e.g. `omp`) through their public interfaces.
|
|
411
412
|
|
|
412
|
-
**
|
|
413
|
+
**AgentWorkShop is source-available software, licensed under the [PolyForm Noncommercial 1.0.0](./LICENSE).**
|
|
414
|
+
|
|
415
|
+
- ✅ **Permitted** — personal study, research, hobby projects, teaching, and use by noncommercial organizations (charities, education, public research, government).
|
|
416
|
+
- ❌ **Not permitted without prior written permission** — any **commercial use**: selling, paid services, integrating into commercial products, or production use serving a business. Commercial licenses are available from the copyright holder.
|
|
417
|
+
- 📌 When you redistribute the software, you must pass through the `Required Notice` line and these terms.
|
|
418
|
+
|
|
419
|
+
For commercial licensing, contact: [GitHub @kingdol666](https://github.com/kingdol666) · kingdol6080@gmail.com
|
|
413
420
|
|
|
414
421
|
<div align="center">
|
|
415
422
|
|
|
@@ -22,6 +22,7 @@ const menuItems = computed<MenuItem[]>(() => [
|
|
|
22
22
|
{ key: '/logs', icon: 'i-tabler-list-details', label: t('menu.logs'), motion: 'im-pop' },
|
|
23
23
|
{ key: '/users', icon: 'i-tabler-users-group', label: t('menu.users'), motion: 'im-pop' },
|
|
24
24
|
{ key: '/monitor', icon: 'i-tabler-cpu', label: t('menu.monitor'), motion: 'im-pulse' },
|
|
25
|
+
{ key: '/plugins', icon: 'i-tabler-puzzle', label: t('menu.plugins'), motion: 'im-pop' },
|
|
25
26
|
{ key: '/settings', icon: 'i-tabler-settings', label: t('menu.settings'), motion: 'im-rotate' },
|
|
26
27
|
])
|
|
27
28
|
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 插件管理 —— 查看配置根 plugins/ 下全部插件(双作用域)、详情、启停开关。
|
|
4
|
+
* 启停写 plugins-state.json,服务端 fs.watch 热重载(≈1s),浏览器侧 loader
|
|
5
|
+
* 经 WS plugins.reloaded 事件 + 轮询双通道热注入/卸载客户端增强。
|
|
6
|
+
*/
|
|
7
|
+
import { computed, onMounted, ref } from 'vue'
|
|
8
|
+
import { message } from 'ant-design-vue'
|
|
9
|
+
import { useUserStore } from '~/stores/workshop/user'
|
|
10
|
+
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
const userStore = useUserStore()
|
|
13
|
+
|
|
14
|
+
interface PluginRoute { method: string, path: string }
|
|
15
|
+
interface PluginInfo {
|
|
16
|
+
name: string
|
|
17
|
+
version: string
|
|
18
|
+
description: string
|
|
19
|
+
scope: 'project' | 'user'
|
|
20
|
+
enabled: boolean
|
|
21
|
+
hasClient: boolean
|
|
22
|
+
routes: PluginRoute[]
|
|
23
|
+
error?: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const plugins = ref<PluginInfo[]>([])
|
|
27
|
+
const failures = ref<Array<{ source: string, error: string }>>([])
|
|
28
|
+
const loading = ref(false)
|
|
29
|
+
const busyName = ref('')
|
|
30
|
+
const drawerOpen = ref(false)
|
|
31
|
+
const current = ref<PluginInfo | null>(null)
|
|
32
|
+
|
|
33
|
+
/** 平台 API 鉴权:Bearer token(与全局 $http 拦截器同源) */
|
|
34
|
+
function authHeaders(): Record<string, string> {
|
|
35
|
+
const token = (userStore as { token?: string }).token
|
|
36
|
+
return token ? { authorization: `Bearer ${token}` } : {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function load() {
|
|
40
|
+
loading.value = true
|
|
41
|
+
try {
|
|
42
|
+
const d = await $fetch<{ plugins: PluginInfo[], failures: Array<{ source: string, error: string }> }>('/api/workshop/plugins', { headers: authHeaders() })
|
|
43
|
+
plugins.value = d.plugins ?? []
|
|
44
|
+
failures.value = d.failures ?? []
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
message.error(t('plugins.enableFail'))
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
loading.value = false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function toggle(p: PluginInfo) {
|
|
55
|
+
busyName.value = p.name
|
|
56
|
+
try {
|
|
57
|
+
const d = await $fetch<{ enabled: boolean }>(`/api/workshop/plugins/${p.name}/${p.enabled ? 'disable' : 'enable'}`, { method: 'POST', headers: authHeaders() })
|
|
58
|
+
p.enabled = d.enabled
|
|
59
|
+
message.success(`${p.name} · ${d.enabled ? t('plugins.enabled') : t('plugins.disabled')}`)
|
|
60
|
+
setTimeout(load, 800) // 热重载完成后刷新路由/客户端状态
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
message.error(`${t('plugins.enableFail')}: ${(err as Error)?.message ?? ''}`)
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
busyName.value = ''
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const enabledCount = computed(() => plugins.value.filter(p => p.enabled).length)
|
|
71
|
+
const clientCount = computed(() => plugins.value.filter(p => p.hasClient).length)
|
|
72
|
+
|
|
73
|
+
function scopeLabel(scope: string) {
|
|
74
|
+
return scope === 'project' ? t('plugins.scopeProject') : t('plugins.scopeUser')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function openDetail(p: PluginInfo) {
|
|
78
|
+
current.value = p
|
|
79
|
+
drawerOpen.value = true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
onMounted(load)
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<template>
|
|
86
|
+
<div class="plugins-page">
|
|
87
|
+
<header class="pg-head">
|
|
88
|
+
<div>
|
|
89
|
+
<h1 class="pg-title">
|
|
90
|
+
{{ $t('plugins.title') }}
|
|
91
|
+
</h1>
|
|
92
|
+
<p class="pg-sub">
|
|
93
|
+
{{ $t('plugins.subtitle') }}
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="pg-stats">
|
|
97
|
+
<span class="stat">{{ $t('plugins.enabled') }} <b>{{ enabledCount }}</b></span>
|
|
98
|
+
<span class="stat">{{ $t('plugins.hasClient') }} <b>{{ clientCount }}</b></span>
|
|
99
|
+
<span class="stat">{{ $t('plugins.routes') }} <b>{{ plugins.reduce((s, p) => s + p.routes.length, 0) }}</b></span>
|
|
100
|
+
<a-button
|
|
101
|
+
size="small"
|
|
102
|
+
:loading="loading"
|
|
103
|
+
@click="load"
|
|
104
|
+
>
|
|
105
|
+
{{ $t('plugins.refresh') }}
|
|
106
|
+
</a-button>
|
|
107
|
+
</div>
|
|
108
|
+
</header>
|
|
109
|
+
|
|
110
|
+
<a-alert
|
|
111
|
+
v-if="failures.length"
|
|
112
|
+
type="error"
|
|
113
|
+
show-icon
|
|
114
|
+
class="pg-failures"
|
|
115
|
+
:message="$t('plugins.loadFailures', { n: failures.length })"
|
|
116
|
+
>
|
|
117
|
+
<template #description>
|
|
118
|
+
<div
|
|
119
|
+
v-for="f in failures"
|
|
120
|
+
:key="f.source"
|
|
121
|
+
class="fail-line"
|
|
122
|
+
>
|
|
123
|
+
{{ f.source }} — {{ f.error }}
|
|
124
|
+
</div>
|
|
125
|
+
</template>
|
|
126
|
+
</a-alert>
|
|
127
|
+
|
|
128
|
+
<div
|
|
129
|
+
v-if="!plugins.length && !loading"
|
|
130
|
+
class="pg-empty"
|
|
131
|
+
>
|
|
132
|
+
{{ $t('plugins.empty') }}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div class="pg-grid">
|
|
136
|
+
<article
|
|
137
|
+
v-for="p in plugins"
|
|
138
|
+
:key="`${p.scope}:${p.name}`"
|
|
139
|
+
class="pg-card"
|
|
140
|
+
:class="{ 'is-off': !p.enabled }"
|
|
141
|
+
@click="openDetail(p)"
|
|
142
|
+
>
|
|
143
|
+
<div class="card-head">
|
|
144
|
+
<span class="card-name">{{ p.name }}</span>
|
|
145
|
+
<span class="card-ver">{{ p.version }}</span>
|
|
146
|
+
<a-switch
|
|
147
|
+
:checked="p.enabled"
|
|
148
|
+
:loading="busyName === p.name"
|
|
149
|
+
size="small"
|
|
150
|
+
@click.stop
|
|
151
|
+
@change="toggle(p)"
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
<p class="card-desc">
|
|
155
|
+
{{ p.description || '—' }}
|
|
156
|
+
</p>
|
|
157
|
+
<div class="card-tags">
|
|
158
|
+
<span
|
|
159
|
+
class="tag"
|
|
160
|
+
:class="p.scope"
|
|
161
|
+
>{{ scopeLabel(p.scope) }}</span>
|
|
162
|
+
<span
|
|
163
|
+
class="tag"
|
|
164
|
+
:class="p.enabled ? 'on' : 'off'"
|
|
165
|
+
>{{ p.enabled ? $t('plugins.enabled') : $t('plugins.disabled') }}</span>
|
|
166
|
+
<span
|
|
167
|
+
v-if="p.hasClient"
|
|
168
|
+
class="tag client"
|
|
169
|
+
>{{ $t('plugins.hasClient') }}</span>
|
|
170
|
+
</div>
|
|
171
|
+
<div
|
|
172
|
+
v-if="p.routes.length"
|
|
173
|
+
class="card-routes"
|
|
174
|
+
>
|
|
175
|
+
<code
|
|
176
|
+
v-for="r in p.routes"
|
|
177
|
+
:key="r.method + r.path"
|
|
178
|
+
>{{ r.method }} {{ r.path }}</code>
|
|
179
|
+
</div>
|
|
180
|
+
</article>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<a-drawer
|
|
184
|
+
v-model:open="drawerOpen"
|
|
185
|
+
:title="current?.name"
|
|
186
|
+
width="460"
|
|
187
|
+
>
|
|
188
|
+
<template v-if="current">
|
|
189
|
+
<dl class="detail">
|
|
190
|
+
<dt>version</dt><dd>{{ current.version }}</dd>
|
|
191
|
+
<dt>scope</dt><dd>{{ scopeLabel(current.scope) }}</dd>
|
|
192
|
+
<dt>enabled</dt><dd>{{ current.enabled }}</dd>
|
|
193
|
+
<dt>client</dt><dd>{{ current.hasClient ? '✓' : '—' }}</dd>
|
|
194
|
+
<dt v-if="current.error">
|
|
195
|
+
error
|
|
196
|
+
</dt>
|
|
197
|
+
<dd
|
|
198
|
+
v-if="current.error"
|
|
199
|
+
style="color:#ff6b6b"
|
|
200
|
+
>
|
|
201
|
+
{{ current.error }}
|
|
202
|
+
</dd>
|
|
203
|
+
</dl>
|
|
204
|
+
<h4>{{ $t('plugins.routes') }}</h4>
|
|
205
|
+
<code
|
|
206
|
+
v-for="r in current.routes"
|
|
207
|
+
:key="r.method + r.path"
|
|
208
|
+
class="detail-route"
|
|
209
|
+
>
|
|
210
|
+
{{ r.method }} /api/plugins/{{ current.name }}{{ r.path }}
|
|
211
|
+
</code>
|
|
212
|
+
<p
|
|
213
|
+
v-if="!current.routes.length"
|
|
214
|
+
class="dim"
|
|
215
|
+
>
|
|
216
|
+
—
|
|
217
|
+
</p>
|
|
218
|
+
</template>
|
|
219
|
+
</a-drawer>
|
|
220
|
+
</div>
|
|
221
|
+
</template>
|
|
222
|
+
|
|
223
|
+
<style scoped lang="css">
|
|
224
|
+
.plugins-page { max-width: 1080px; margin: 0 auto; padding: 24px 20px 48px; }
|
|
225
|
+
.pg-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 18px; }
|
|
226
|
+
.pg-title { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: .5px; }
|
|
227
|
+
.pg-sub { margin: 4px 0 0; opacity: .6; font-size: 12px; }
|
|
228
|
+
.pg-stats { display: flex; align-items: center; gap: 14px; font-size: 12px; opacity: .85; }
|
|
229
|
+
.pg-stats b { font-size: 15px; }
|
|
230
|
+
.pg-failures { margin-bottom: 16px; }
|
|
231
|
+
.fail-line { font-family: ui-monospace, monospace; font-size: 11px; }
|
|
232
|
+
.pg-empty { padding: 48px 0; text-align: center; opacity: .55; }
|
|
233
|
+
.pg-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 14px; }
|
|
234
|
+
.pg-card { position: relative; padding: 14px 16px; border: 1px solid var(--aw-border, rgba(128, 152, 199, .25));
|
|
235
|
+
border-radius: 12px; background: var(--aw-card, rgba(255, 255, 255, .04)); cursor: pointer;
|
|
236
|
+
transition: border-color .2s, transform .2s; }
|
|
237
|
+
.pg-card:hover { border-color: var(--aw-accent, #35e0a0); transform: translateY(-1px); }
|
|
238
|
+
.pg-card.is-off { opacity: .55; }
|
|
239
|
+
.card-head { display: flex; align-items: center; gap: 10px; }
|
|
240
|
+
.card-name { font-weight: 700; font-size: 15px; }
|
|
241
|
+
.card-ver { color: inherit; opacity: .5; font-family: ui-monospace, monospace; font-size: 11px; }
|
|
242
|
+
.card-head .a-switch { margin-left: auto; }
|
|
243
|
+
.card-desc { margin: 8px 0; font-size: 12px; opacity: .75; min-height: 18px; }
|
|
244
|
+
.card-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
245
|
+
.tag { padding: 2px 8px; border-radius: 999px; font-size: 10px; border: 1px solid rgba(128, 152, 199, .3); }
|
|
246
|
+
.tag.project { border-color: rgba(53, 224, 160, .5); color: #35e0a0; }
|
|
247
|
+
.tag.user { border-color: rgba(65, 200, 244, .5); color: #41c8f4; }
|
|
248
|
+
.tag.on { border-color: rgba(53, 224, 160, .5); color: #35e0a0; }
|
|
249
|
+
.tag.off { border-color: rgba(255, 107, 107, .4); color: #ff6b6b; }
|
|
250
|
+
.tag.client { border-color: rgba(181, 140, 255, .5); color: #b58cff; }
|
|
251
|
+
.card-routes { display: flex; flex-direction: column; gap: 2px; }
|
|
252
|
+
.card-routes code { font-size: 10px; opacity: .65; }
|
|
253
|
+
.detail dt { margin-top: 10px; font-size: 11px; opacity: .5; }
|
|
254
|
+
.detail dd { margin: 2px 0 0; font-family: ui-monospace, monospace; font-size: 12px; }
|
|
255
|
+
.detail-route { display: block; font-size: 11px; opacity: .8; margin-bottom: 4px; }
|
|
256
|
+
h4 { margin: 16px 0 6px; }
|
|
257
|
+
.dim { opacity: .45; }
|
|
258
|
+
</style>
|
|
@@ -1,79 +1,119 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* aw 插件客户端装载器 ——
|
|
3
|
-
* - 启动期拉取 /api/plugins/manifest →
|
|
4
|
-
* -
|
|
5
|
-
*
|
|
2
|
+
* aw 插件客户端装载器 —— 前端增强入口(支持热注入/热卸载)。
|
|
3
|
+
* - 启动期拉取 /api/plugins/manifest → 对启用且含 client 的插件动态 import 装载
|
|
4
|
+
* - 热通道双保险:WS `plugins.reloaded` 事件(TownBus 桥) + 15s 轮询 diff
|
|
5
|
+
* → 新启用插件即时注入;停用插件即时 dispose 卸载
|
|
6
6
|
* - 错误隔离:单插件装载失败仅告警,不影响应用与其他插件
|
|
7
7
|
* 插件契约见 docs/plugins.md;信任模型与 aw commands 相同(仅装可信代码)。
|
|
8
8
|
*/
|
|
9
|
-
import { createClientContext } from '@/sdk/client.mjs'
|
|
9
|
+
import { createClientContext, type ClientContext } from '@/sdk/client.mjs'
|
|
10
10
|
import type { TownBus } from '~/composables/workshop/useTownBus'
|
|
11
11
|
|
|
12
|
+
interface ManifestEntry { name: string, enabled?: boolean, hasClient?: boolean }
|
|
13
|
+
|
|
12
14
|
export default defineNuxtPlugin(async (nuxtApp) => {
|
|
13
15
|
if (!import.meta.client) return
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
/** name → ctx(已装载客户端插件) */
|
|
18
|
+
const loaded = new Map<string, ClientContext>()
|
|
19
|
+
let bus: TownBus | null = null
|
|
20
|
+
try {
|
|
21
|
+
bus = useTownBus()
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
bus = null // WS 总线不可用(离线)时插件仍可装载,只是无实时事件流
|
|
25
|
+
}
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
function bridgeFactory(fn: (type: string, payload: unknown) => void) {
|
|
18
28
|
if (!bus) return () => {}
|
|
19
29
|
return bus.subscribe((e) => {
|
|
20
30
|
try {
|
|
21
31
|
fn(e.type, e.payload)
|
|
22
32
|
}
|
|
23
33
|
catch (err) {
|
|
24
|
-
console.warn(
|
|
34
|
+
console.warn('[aw-plugins] 事件分发异常:', err)
|
|
25
35
|
}
|
|
26
36
|
})
|
|
27
37
|
}
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
const res = await fetch('/api/plugins/manifest', { headers: { accept: 'application/json' } })
|
|
31
|
-
if (!res.ok) return
|
|
32
|
-
const body = await res.json().catch(() => null) as { plugins?: Array<{ name: string, hasClient?: boolean }> } | null
|
|
33
|
-
const plugins = (body?.plugins ?? []).filter(p => p.hasClient)
|
|
34
|
-
if (!plugins.length) return
|
|
35
|
-
|
|
36
|
-
let bus: TownBus | null = null
|
|
39
|
+
async function loadOne(name: string): Promise<boolean> {
|
|
37
40
|
try {
|
|
38
|
-
|
|
41
|
+
const mod = await import(/* @vite-ignore */ `/api/plugins/client/${encodeURIComponent(name)}`)
|
|
42
|
+
const setup = (mod as { setup?: unknown }).setup ?? (mod as { default?: { setup?: unknown } }).default?.setup
|
|
43
|
+
if (typeof setup !== 'function') {
|
|
44
|
+
console.warn(`[aw-plugins] ${name} 客户端入口缺少 setup(ctx)`)
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
const ctx = createClientContext({ name, eventBridge: bridgeFactory(bus) })
|
|
48
|
+
await (setup as (ctx: unknown) => void | Promise<void>)(ctx)
|
|
49
|
+
void ctx.hooks.emit('client:init', { name })
|
|
50
|
+
loaded.set(name, ctx)
|
|
51
|
+
console.info(`[aw-plugins] ✔ 客户端插件已注入: ${name}`)
|
|
52
|
+
return true
|
|
39
53
|
}
|
|
40
|
-
catch {
|
|
41
|
-
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.warn(`[aw-plugins] 客户端插件装载失败 ${name}:`, err)
|
|
56
|
+
return false
|
|
42
57
|
}
|
|
58
|
+
}
|
|
43
59
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
function unloadOne(name: string) {
|
|
61
|
+
const ctx = loaded.get(name)
|
|
62
|
+
if (!ctx) return
|
|
63
|
+
ctx.dispose()
|
|
64
|
+
loaded.delete(name)
|
|
65
|
+
console.info(`[aw-plugins] 客户端插件已卸载: ${name}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 全量同步:启用的新插件注入;停用/移除的插件卸载 */
|
|
69
|
+
async function syncPlugins(): Promise<void> {
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch('/api/plugins/manifest', { headers: { accept: 'application/json' } })
|
|
72
|
+
if (!res.ok) return
|
|
73
|
+
const body = await res.json().catch(() => null) as { plugins?: ManifestEntry[] } | null
|
|
74
|
+
const list = body?.plugins ?? []
|
|
75
|
+
for (const p of list) {
|
|
76
|
+
if (p.enabled !== false && p.hasClient && !loaded.has(p.name))
|
|
77
|
+
await loadOne(p.name)
|
|
60
78
|
}
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
for (const name of [...loaded.keys()]) {
|
|
80
|
+
const p = list.find(x => x.name === name)
|
|
81
|
+
if (!p || p.enabled === false || !p.hasClient)
|
|
82
|
+
unloadOne(name)
|
|
63
83
|
}
|
|
64
84
|
}
|
|
85
|
+
catch { /* 网络不可达:保留现状,下轮再试 */ }
|
|
86
|
+
}
|
|
65
87
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
// 首次装载
|
|
89
|
+
await syncPlugins()
|
|
90
|
+
|
|
91
|
+
// 页面切换广播(已装载插件均可感知;防御性隔离——单插件异常不影响路由)
|
|
92
|
+
if (loaded.size) {
|
|
93
|
+
nuxtApp.hooks.hook('page:finish', () => {
|
|
94
|
+
let path = ''
|
|
95
|
+
try {
|
|
96
|
+
path = useRoute().path ?? ''
|
|
97
|
+
}
|
|
98
|
+
catch { /* 路由上下文不可用时静默 */ }
|
|
99
|
+
for (const { ctx } of loaded.values()) {
|
|
100
|
+
try {
|
|
101
|
+
ctx?.hooks?.emit('page:change', { path })
|
|
72
102
|
}
|
|
73
|
-
|
|
74
|
-
|
|
103
|
+
catch { /* 单插件广播失败不阻断 */ }
|
|
104
|
+
}
|
|
105
|
+
})
|
|
75
106
|
}
|
|
76
|
-
|
|
77
|
-
|
|
107
|
+
|
|
108
|
+
// 热通道 1:WS plugins.reloaded(服务端热重载后广播)
|
|
109
|
+
if (bus) {
|
|
110
|
+
bus.subscribe((e) => {
|
|
111
|
+
if (e.type === 'plugins.reloaded')
|
|
112
|
+
void syncPlugins()
|
|
113
|
+
})
|
|
78
114
|
}
|
|
115
|
+
// 热通道 2:轮询兜底(无 WS 场景)
|
|
116
|
+
setInterval(() => {
|
|
117
|
+
void syncPlugins()
|
|
118
|
+
}, 15_000)
|
|
79
119
|
})
|
package/cli/commands/plugin.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// 插件契约:入口 index.mjs 导出 { name, version?, description?,
|
|
8
8
|
// setup(ctx)?, client?: './client.mjs', routes?: [...] } —— 零导入依赖。
|
|
9
9
|
// ============================================================
|
|
10
|
-
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
|
|
10
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
11
11
|
import { join } from 'node:path'
|
|
12
12
|
import { color } from '../core/logger.mjs'
|
|
13
13
|
import { CliError } from '../core/errors.mjs'
|
|
@@ -16,8 +16,8 @@ export const meta = {
|
|
|
16
16
|
name: 'plugin',
|
|
17
17
|
aliases: ['plugins', 'plug'],
|
|
18
18
|
group: '扩展',
|
|
19
|
-
summary: '
|
|
20
|
-
usage: 'aw plugin <list|create> [name] [--project|--global] [--force]',
|
|
19
|
+
summary: '插件管理:查看/启停(热重载)/脚手架',
|
|
20
|
+
usage: 'aw plugin <list|create|enable|disable> [name] [--project|--global] [--force]',
|
|
21
21
|
description: [
|
|
22
22
|
'插件 = 配置根 plugins/<name>/ 下的 node 项目:入口 index.mjs 导出',
|
|
23
23
|
'{ name, setup(ctx) } 即自动装载(服务端钩子/API 路由),client.mjs 可选(浏览器增强)。',
|
|
@@ -106,7 +106,54 @@ export async function run(argv, ctx) {
|
|
|
106
106
|
if (sub === 'create' || sub === 'new' || sub === 'add')
|
|
107
107
|
return create(ctx, positionals[1], flags)
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
if (sub === 'enable' || sub === 'disable')
|
|
110
|
+
return setEnabled(ctx, sub === 'enable', positionals[1])
|
|
111
|
+
|
|
112
|
+
throw new CliError('USAGE', '用法: aw plugin <list|create|enable|disable> [name]')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---- 启停状态机(单一事实源:<home>/plugins-state.json;服务 fs.watch 热重载) ----
|
|
116
|
+
function stateFile(ctx) {
|
|
117
|
+
return join(ctx.home, 'plugins-state.json')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readState(ctx) {
|
|
121
|
+
try {
|
|
122
|
+
const j = JSON.parse(readFileSync(stateFile(ctx), 'utf8'))
|
|
123
|
+
return new Set(Array.isArray(j.disabled) ? j.disabled : [])
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return new Set()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function writeState(ctx, disabledSet) {
|
|
131
|
+
const p = stateFile(ctx)
|
|
132
|
+
const tmp = `${p}.${process.pid}.tmp`
|
|
133
|
+
writeFileSync(tmp, `${JSON.stringify({ version: 1, updatedAt: new Date().toISOString(), disabled: [...disabledSet] }, null, 2)}\n`, 'utf8')
|
|
134
|
+
renameSync(tmp, p)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function setEnabled(ctx, enabled, name) {
|
|
138
|
+
if (!name) throw new CliError('USAGE', `用法: aw plugin ${enabled ? 'enable' : 'disable'} <name>`)
|
|
139
|
+
const dir = findPluginDir(ctx, name)
|
|
140
|
+
if (!dir) throw new CliError('NOT_FOUND', `未找到插件 "${name}"(aw plugin list 查看)`)
|
|
141
|
+
|
|
142
|
+
const disabled = readState(ctx)
|
|
143
|
+
if (enabled) disabled.delete(name)
|
|
144
|
+
else disabled.add(name)
|
|
145
|
+
writeState(ctx, disabled)
|
|
146
|
+
|
|
147
|
+
console.log(`${color.green('✔')} 插件 ${color.bold(name)} 已${enabled ? '启用' : '停用'}${color.dim(`(${dir})`)}`)
|
|
148
|
+
console.log(` › 运行中的服务会在 1s 内自动热重载;网页「插件管理」页状态同步`)
|
|
149
|
+
return 0
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function findPluginDir(ctx, name) {
|
|
153
|
+
for (const { dir } of scopes(ctx)) {
|
|
154
|
+
if (dir && existsSync(join(dir, name, 'index.mjs'))) return join(dir, name)
|
|
155
|
+
}
|
|
156
|
+
return null
|
|
110
157
|
}
|
|
111
158
|
|
|
112
159
|
function scopes(ctx) {
|
|
@@ -118,8 +165,9 @@ function scopes(ctx) {
|
|
|
118
165
|
|
|
119
166
|
function list(ctx) {
|
|
120
167
|
console.log('')
|
|
121
|
-
console.log(`${color.bold('AgentWorkShop 插件')} ${color.dim('— 配置根 plugins/
|
|
168
|
+
console.log(`${color.bold('AgentWorkShop 插件')} ${color.dim('— 配置根 plugins/ 目录,自动装载;aw plugin enable/disable 启停')}`)
|
|
122
169
|
let total = 0
|
|
170
|
+
const disabled = readState(ctx)
|
|
123
171
|
for (const { dir, label } of scopes(ctx)) {
|
|
124
172
|
console.log('')
|
|
125
173
|
console.log(color.bold(` ${label} ${color.dim(dir)}`))
|
|
@@ -135,11 +183,13 @@ function list(ctx) {
|
|
|
135
183
|
for (const name of dirs) {
|
|
136
184
|
total++
|
|
137
185
|
const hasClient = existsSync(join(dir, name, 'client.mjs'))
|
|
138
|
-
|
|
186
|
+
const off = disabled.has(name)
|
|
187
|
+
const stateText = off ? color.yellow('已停用') : color.green('已启用')
|
|
188
|
+
console.log(` ${off ? color.yellow('○') : color.green('●')} ${color.cyan(name.padEnd(24))}${stateText}${hasClient ? color.dim(' +client') : ''}`)
|
|
139
189
|
}
|
|
140
190
|
}
|
|
141
191
|
console.log('')
|
|
142
|
-
console.log(color.dim(`共 ${total} 个 ·
|
|
192
|
+
console.log(color.dim(`共 ${total} 个 · 启停: aw plugin enable|disable <name>(运行中服务热重载) · 文档: docs/plugins.md`))
|
|
143
193
|
console.log('')
|
|
144
194
|
return 0
|
|
145
195
|
}
|