agentworkshop 0.3.0 → 0.4.0

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 CHANGED
@@ -11,8 +11,9 @@
11
11
  [![TypeScript 5.7](https://img.shields.io/badge/TypeScript-5.7-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
12
12
  [![Node ≥ 23.4](https://img.shields.io/badge/Node.js-%E2%89%A5%2023.4-3C873A?logo=nodedotjs&logoColor=white)](https://nodejs.org)
13
13
  [![SQLite node:sqlite](https://img.shields.io/badge/SQLite-node:sqlite-003B57?logo=sqlite&logoColor=white)](https://nodejs.org/api/sqlite.html)
14
+ [![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm_NC_1.0-8A2BE2?logo=openaccess&logoColor=white)](./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 file | Pending |
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
- **License is TBD** the license file will be added before the `v1.0` release.
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,112 @@
1
1
  /**
2
- * aw 插件客户端装载器 —— 前端增强入口。
3
- * - 启动期拉取 /api/plugins/manifest → 对含 client 的插件动态 import 脚本
4
- * - 每个插件获得独立 ctx(sdk/client.mjs):事件订阅/Hooks/DOM 助手/私有挂载点
5
- * - 事件桥:useTownBus(AEP 信封,与 WS 同源) ctx.hooks(event:<type> / '*')
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
- const loaded: Array<{ name: string, ctx: ReturnType<typeof createClientContext> }> = []
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
- const bridgeFactory = (bus: TownBus | null) => (fn: (type: string, payload: unknown) => void) => {
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(`[aw-plugins] 事件分发异常:`, err)
34
+ console.warn('[aw-plugins] 事件分发异常:', err)
25
35
  }
26
36
  })
27
37
  }
28
38
 
29
- try {
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
- bus = useTownBus()
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
- bus = null // WS 总线不可用(离线)时插件仍可装载,只是无事件流
54
+ catch (err) {
55
+ console.warn(`[aw-plugins] 客户端插件装载失败 ${name}:`, err)
56
+ return false
42
57
  }
58
+ }
43
59
 
44
- for (const p of plugins) {
45
- try {
46
- const mod = await import(/* @vite-ignore */ `/api/plugins/client/${encodeURIComponent(p.name)}`)
47
- const setup = (mod as { setup?: unknown }).setup ?? (mod as { default?: { setup?: unknown } }).default?.setup
48
- if (typeof setup !== 'function') {
49
- console.warn(`[aw-plugins] ${p.name} 客户端入口缺少 setup(ctx)`)
50
- continue
51
- }
52
- const ctx = createClientContext({
53
- name: p.name,
54
- eventBridge: bridgeFactory(bus),
55
- })
56
- await (setup as (ctx: unknown) => void | Promise<void>)(ctx)
57
- void ctx.hooks.emit('client:init', { name: p.name })
58
- loaded.push({ name: p.name, ctx })
59
- console.info(`[aw-plugins] 客户端插件已装载: ${p.name}`)
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
- catch (err) {
62
- console.warn(`[aw-plugins] 客户端插件装载失败 ${p.name}:`, err)
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
- // 页面切换广播(page:change)
67
- if (loaded.length) {
68
- nuxtApp.hooks.hook('page:finish', () => {
69
- const route = useRoute()
70
- for (const { ctx } of loaded) {
71
- void ctx.hooks.emit('page:change', { path: route.path })
72
- }
73
- })
74
- }
88
+ // 首次装载
89
+ await syncPlugins()
90
+
91
+ // 页面切换广播(已装载插件均可感知)
92
+ if (loaded.size) {
93
+ nuxtApp.hooks.hook('page:finish', () => {
94
+ const route = useRoute()
95
+ for (const { ctx } of loaded.values()) {
96
+ void ctx.hooks.emit('page:change', { path: route.path })
97
+ }
98
+ })
75
99
  }
76
- catch {
77
- // 网络不可达/服务未就绪:静默(插件是增强层,绝不阻断应用)
100
+
101
+ // 热通道 1:WS plugins.reloaded(服务端热重载后广播)
102
+ if (bus) {
103
+ bus.subscribe((e) => {
104
+ if (e.type === 'plugins.reloaded')
105
+ void syncPlugins()
106
+ })
78
107
  }
108
+ // 热通道 2:轮询兜底(无 WS 场景)
109
+ setInterval(() => {
110
+ void syncPlugins()
111
+ }, 15_000)
79
112
  })
@@ -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
- throw new CliError('USAGE', '用法: aw plugin <list|create> [name] [--project|--global]')
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
- console.log(` ${color.green('●')} ${color.cyan(name.padEnd(24))}${hasClient ? `${color.dim(' +client')}` : ''}`)
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} 个 · 契约: index.mjs 导出 { name, setup(ctx) } · 文档: docs/plugins.md`))
192
+ console.log(color.dim(`共 ${total} 个 · 启停: aw plugin enable|disable <name>(运行中服务热重载) · 文档: docs/plugins.md`))
143
193
  console.log('')
144
194
  return 0
145
195
  }