agentworkshop 0.2.2 → 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.
Files changed (40) hide show
  1. package/LICENSE +91 -0
  2. package/README-zh.md +431 -424
  3. package/README.md +10 -3
  4. package/app/components/AppSidebar.vue +1 -0
  5. package/app/pages/plugins/index.vue +258 -0
  6. package/app/plugins/aw-plugins.client.ts +81 -48
  7. package/cli/commands/plugin.mjs +57 -7
  8. package/docs/plugins.md +133 -63
  9. package/docs/sdk.md +265 -0
  10. package/i18n/locales/en.ts +17 -0
  11. package/i18n/locales/zh-CN.ts +17 -0
  12. package/package.json +19 -2
  13. package/scripts/_dbg-docs-site-shot.mjs +33 -0
  14. package/scripts/dev-guard.mjs +13 -0
  15. package/scripts/home-bootstrap.mjs +26 -1
  16. package/sdk/api.mjs +101 -0
  17. package/sdk/client.mjs +56 -8
  18. package/sdk/context.mjs +56 -15
  19. package/sdk/examples/line-sentinel/client.mjs +31 -0
  20. package/sdk/examples/line-sentinel/index.mjs +102 -0
  21. package/sdk/examples/ops-notifier/client.mjs +23 -0
  22. package/sdk/examples/ops-notifier/index.mjs +34 -0
  23. package/sdk/index.d.mts +135 -0
  24. package/sdk/index.mjs +5 -18
  25. package/sdk/lifecycle.mjs +25 -0
  26. package/server/api/plugins/[name]/[...path].ts +5 -2
  27. package/server/api/workshop/plugins/[name]/disable.post.ts +19 -0
  28. package/server/api/workshop/plugins/[name]/enable.post.ts +19 -0
  29. package/server/api/workshop/plugins/[name]/index.get.ts +14 -0
  30. package/server/api/workshop/plugins/index.get.ts +17 -0
  31. package/server/data/daqs.json +260 -62
  32. package/server/data/dcw-lines.json +63 -0
  33. package/server/data/dcw-products.json +49 -0
  34. package/server/data/dcw-recipes.json +140 -0
  35. package/server/data/dcw-rollback.json +1 -1
  36. package/server/data/dcw-runs.json +175 -126
  37. package/server/data/dcws.json +147 -0
  38. package/server/data/line-runs.json +85 -15
  39. package/server/plugins/aw-plugins.ts +7 -1
  40. package/server/services/workshop/plugins/host.mjs +212 -18
@@ -7,7 +7,7 @@
7
7
  "productId": "pd-3ad54846",
8
8
  "productName": "光学膜 BOPP",
9
9
  "startedAt": "2026-08-31T16:20:13.707Z",
10
- "taggedSamples": 29498
10
+ "taggedSamples": 29766
11
11
  },
12
12
  {
13
13
  "lineId": "ln-c377a6bf",
@@ -17,7 +17,7 @@
17
17
  "productId": "pd-7e69e0ee",
18
18
  "productName": "警告验证产品-k2xh",
19
19
  "startedAt": "2026-09-01T03:23:18.233Z",
20
- "taggedSamples": 11559
20
+ "taggedSamples": 11693
21
21
  },
22
22
  {
23
23
  "lineId": "ln-9224d3d7",
@@ -27,7 +27,7 @@
27
27
  "productId": "pd-910c0fa8",
28
28
  "productName": "新协议产品-9xhn",
29
29
  "startedAt": "2026-09-01T05:59:24.062Z",
30
- "taggedSamples": 14533
30
+ "taggedSamples": 14666
31
31
  },
32
32
  {
33
33
  "lineId": "ln-386c53ed",
@@ -37,7 +37,7 @@
37
37
  "productId": "pd-2d4f9357",
38
38
  "productName": "新协议产品-6ofk",
39
39
  "startedAt": "2026-09-01T06:05:53.063Z",
40
- "taggedSamples": 14823
40
+ "taggedSamples": 14956
41
41
  },
42
42
  {
43
43
  "lineId": "ln-5957f6db",
@@ -47,7 +47,7 @@
47
47
  "productId": "pd-a3ed5047",
48
48
  "productName": "新协议产品-x4hd",
49
49
  "startedAt": "2026-09-01T06:09:02.148Z",
50
- "taggedSamples": 14409
50
+ "taggedSamples": 14542
51
51
  },
52
52
  {
53
53
  "lineId": "ln-9ec42f08",
@@ -57,7 +57,7 @@
57
57
  "productId": "pd-5de729c0",
58
58
  "productName": "新协议产品-teti",
59
59
  "startedAt": "2026-09-01T06:13:44.315Z",
60
- "taggedSamples": 14388
60
+ "taggedSamples": 14521
61
61
  },
62
62
  {
63
63
  "lineId": "ln-69e37924",
@@ -67,7 +67,7 @@
67
67
  "productId": "pd-45eb8d43",
68
68
  "productName": "新协议产品-fg8t",
69
69
  "startedAt": "2026-09-01T06:16:47.469Z",
70
- "taggedSamples": 14421
70
+ "taggedSamples": 14554
71
71
  },
72
72
  {
73
73
  "lineId": "ln-c269e692",
@@ -77,7 +77,7 @@
77
77
  "productId": "pd-b3bd0191",
78
78
  "productName": "新协议产品-w2sp",
79
79
  "startedAt": "2026-09-01T06:56:05.893Z",
80
- "taggedSamples": 14472
80
+ "taggedSamples": 14605
81
81
  },
82
82
  {
83
83
  "lineId": "ln-229d1637",
@@ -87,7 +87,7 @@
87
87
  "productId": "pd-06b896f9",
88
88
  "productName": "新协议产品-lp4h",
89
89
  "startedAt": "2026-09-01T06:57:29.288Z",
90
- "taggedSamples": 14344
90
+ "taggedSamples": 14477
91
91
  },
92
92
  {
93
93
  "lineId": "ln-9546f91f",
@@ -97,7 +97,7 @@
97
97
  "productId": "pd-53e55078",
98
98
  "productName": "新协议产品-jri6",
99
99
  "startedAt": "2026-09-01T07:00:25.059Z",
100
- "taggedSamples": 13767
100
+ "taggedSamples": 13900
101
101
  },
102
102
  {
103
103
  "lineId": "ln-087aad5b",
@@ -107,7 +107,7 @@
107
107
  "productId": "pd-e7e969af",
108
108
  "productName": "新协议产品-4c2d",
109
109
  "startedAt": "2026-09-01T07:09:07.307Z",
110
- "taggedSamples": 12758
110
+ "taggedSamples": 12891
111
111
  },
112
112
  {
113
113
  "lineId": "ln-97d44f8a",
@@ -117,7 +117,7 @@
117
117
  "productId": "pd-28a3ab2c",
118
118
  "productName": "新协议产品-b2ni",
119
119
  "startedAt": "2026-09-01T07:13:44.912Z",
120
- "taggedSamples": 12607
120
+ "taggedSamples": 12740
121
121
  },
122
122
  {
123
123
  "lineId": "ln-a9551770",
@@ -127,7 +127,7 @@
127
127
  "productId": "pd-0c6a64c8",
128
128
  "productName": "新协议产品-uwkf",
129
129
  "startedAt": "2026-09-01T07:15:40.113Z",
130
- "taggedSamples": 12252
130
+ "taggedSamples": 12385
131
131
  },
132
132
  {
133
133
  "lineId": "ln-844ad82f",
@@ -137,7 +137,7 @@
137
137
  "productId": "pd-6932b24f",
138
138
  "productName": "新协议产品-kk6l",
139
139
  "startedAt": "2026-09-01T09:06:55.728Z",
140
- "taggedSamples": 10407
140
+ "taggedSamples": 10540
141
141
  },
142
142
  {
143
143
  "lineId": "ln-af002514",
@@ -147,6 +147,76 @@
147
147
  "productId": "pd-6348a595",
148
148
  "productName": "0.8mm 光学膜",
149
149
  "startedAt": "2026-09-01T14:06:28.766Z",
150
- "taggedSamples": 676
150
+ "taggedSamples": 956
151
+ },
152
+ {
153
+ "lineId": "ln-0eccf6ba",
154
+ "runId": "rr-4e0a115f",
155
+ "recipeId": "rc-8a60518e",
156
+ "recipeName": "哨兵配方-mtjku36w",
157
+ "productId": "pd-4b71be77",
158
+ "productName": "哨兵产品-mtjku36w",
159
+ "startedAt": "2026-09-02T04:09:11.482Z",
160
+ "taggedSamples": 6
161
+ },
162
+ {
163
+ "lineId": "ln-e984246e",
164
+ "runId": "rr-39b3edda",
165
+ "recipeId": "rc-70f39a03",
166
+ "recipeName": "配方C-mtjkwyhu",
167
+ "productId": "pd-bb1e0bed",
168
+ "productName": "产品C-mtjkwyhu",
169
+ "startedAt": "2026-09-02T04:11:26.583Z",
170
+ "taggedSamples": 5
171
+ },
172
+ {
173
+ "lineId": "ln-1fe0b5bc",
174
+ "runId": "rr-2baf5f34",
175
+ "recipeId": "rc-25081a45",
176
+ "recipeName": "配方D-mtjkzcfr",
177
+ "productId": "pd-9f2d9507",
178
+ "productName": "产品D-mtjkzcfr",
179
+ "startedAt": "2026-09-02T04:13:17.902Z",
180
+ "taggedSamples": 4
181
+ },
182
+ {
183
+ "lineId": "ln-d826473e",
184
+ "runId": "rr-7585cef0",
185
+ "recipeId": "rc-3ebd1a97",
186
+ "recipeName": "配方E-mtjl43pv",
187
+ "productId": "pd-d230255e",
188
+ "productName": "产品E-mtjl43pv",
189
+ "startedAt": "2026-09-02T04:16:59.891Z",
190
+ "taggedSamples": 3
191
+ },
192
+ {
193
+ "lineId": "ln-156abb71",
194
+ "runId": "rr-d8002452",
195
+ "recipeId": "rc-fce9474f",
196
+ "recipeName": "配方F-mtjl60qf",
197
+ "productId": "pd-6065b4ba",
198
+ "productName": "产品F-mtjl60qf",
199
+ "startedAt": "2026-09-02T04:18:29.340Z",
200
+ "taggedSamples": 2
201
+ },
202
+ {
203
+ "lineId": "ln-bac43927",
204
+ "runId": "rr-7f4a6ac0",
205
+ "recipeId": "rc-e27a0673",
206
+ "recipeName": "配方G-mtjl98nl",
207
+ "productId": "pd-52f54e33",
208
+ "productName": "产品G-mtjl98nl",
209
+ "startedAt": "2026-09-02T04:20:59.682Z",
210
+ "taggedSamples": 1
211
+ },
212
+ {
213
+ "lineId": "ln-f223008a",
214
+ "runId": "rr-06caf6f4",
215
+ "recipeId": "rc-21479838",
216
+ "recipeName": "配方-mtjo000v",
217
+ "productId": "pd-d919d4bf",
218
+ "productName": "产品-mtjo000v",
219
+ "startedAt": "2026-09-02T05:37:50.743Z",
220
+ "taggedSamples": 0
151
221
  }
152
222
  ]
@@ -11,7 +11,13 @@ import { dirname, resolve } from 'node:path'
11
11
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
12
12
 
13
13
  export default defineNitroPlugin((nitroApp) => {
14
- void initPluginHost({ cwd: process.cwd(), packageRoot })
14
+ void initPluginHost({ cwd: process.cwd(), packageRoot }).then((host) => {
15
+ // 实际监听端口回填(nitro listen 后 ctx.api 自环 origin 才准确)
16
+ nitroApp.hooks.hookOnce('listen', (listener: { port?: number }) => {
17
+ if (listener?.port)
18
+ host.setSelfOrigin(listener.port)
19
+ })
20
+ })
15
21
  nitroApp.hooks.hookOnce('close', () => {
16
22
  void shutdownPluginHost()
17
23
  })
@@ -1,5 +1,5 @@
1
1
  // ============================================================
2
- // AgentWorkShop 插件宿主 —— 发现 / 装载 / 生命周期 / 路由表
2
+ // AgentWorkShop 插件宿主 —— 发现 / 装载 / 启停状态 / 热重载 / 路由表
3
3
  // ------------------------------------------------------------
4
4
  // 目录(与 aw commands 同哲学):
5
5
  // project: <repo>/.AgentWorkShop/plugins/<name>/index.mjs
@@ -7,10 +7,15 @@
7
7
  // 契约:入口导出普通对象 { name, version?, description?, setup(ctx)?,
8
8
  // client?: './client.mjs', routes?: [{method,path,handler}] }
9
9
  // —— ctx 由宿主注入,插件运行时零导入依赖(sdk/ 供类型与显式糖)。
10
+ //
11
+ // 启停状态机:配置根 plugins-state.json { version, updatedAt, disabled: string[] }
12
+ // · 装载时跳过 disabled 插件(manifest 仍可见,enabled:false)
13
+ // · 状态文件变化(fs.watch)→ 热重载:全部 dispose/解绑 → 重新装载
14
+ // · CLI(aw plugin enable/disable) 与 Web 设置页均只写状态文件,服务自感知
10
15
  // 错误隔离:单插件装载/执行失败记入 failures,绝不拖垮主服务。
11
16
  // ============================================================
12
- import { existsSync, readdirSync, readFileSync } from 'node:fs'
13
- import { join, resolve } from 'node:path'
17
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, watch, writeFileSync } from 'node:fs'
18
+ import { dirname, join, resolve } from 'node:path'
14
19
  import { pathToFileURL } from 'node:url'
15
20
  import { HookBus, createPluginContext, createRouteTable, validatePluginModule } from '@/sdk/index.mjs'
16
21
 
@@ -18,6 +23,7 @@ const g = globalThis
18
23
 
19
24
  function log() {
20
25
  return {
26
+ debug: (...a) => console.log('[aw-plugins][debug]', ...a),
21
27
  info: (...a) => console.log('[aw-plugins]', ...a),
22
28
  warn: (...a) => console.warn('[aw-plugins]', ...a),
23
29
  error: (...a) => console.error('[aw-plugins]', ...a),
@@ -35,6 +41,7 @@ function modePaths(cwd) {
35
41
  return {
36
42
  projectDir: isRepo ? join(cwd, '.AgentWorkShop', 'plugins') : null,
37
43
  userDir: join(process.env.AW_HOME && String(process.env.AW_HOME).trim() ? String(process.env.AW_HOME).trim() : defaultHome(), 'plugins'),
44
+ homeDir: process.env.AW_HOME && String(process.env.AW_HOME).trim() ? String(process.env.AW_HOME).trim() : defaultHome(),
38
45
  }
39
46
  }
40
47
 
@@ -42,6 +49,29 @@ function pathToUrl(p) {
42
49
  return pathToFileURL(resolve(p)).href
43
50
  }
44
51
 
52
+ // ---- 启停状态(单一事实源:<配置根>/plugins-state.json;CLI/Web/宿主三方读写) ----
53
+ export function statePathFor(homeDir) {
54
+ return join(homeDir, 'plugins-state.json')
55
+ }
56
+
57
+ export function readDisabledSet(homeDir) {
58
+ try {
59
+ const j = JSON.parse(readFileSync(statePathFor(homeDir), 'utf8'))
60
+ return new Set(Array.isArray(j.disabled) ? j.disabled : [])
61
+ }
62
+ catch {
63
+ return new Set()
64
+ }
65
+ }
66
+
67
+ export function writeDisabledSet(homeDir, disabled) {
68
+ const p = statePathFor(homeDir)
69
+ mkdirSync(dirname(p), { recursive: true })
70
+ const tmp = `${p}.${process.pid}.tmp`
71
+ writeFileSync(tmp, `${JSON.stringify({ version: 1, updatedAt: new Date().toISOString(), disabled: [...disabled] }, null, 2)}\n`, 'utf8')
72
+ renameSync(tmp, p)
73
+ }
74
+
45
75
  /** 发现两个作用域下的插件入口(project 同名覆盖 user) */
46
76
  export function discoverPluginDirs(cwd = process.cwd()) {
47
77
  const { projectDir, userDir } = modePaths(cwd)
@@ -62,12 +92,11 @@ export function discoverPluginDirs(cwd = process.cwd()) {
62
92
 
63
93
  /**
64
94
  * 装载插件宿主(idempotent;nitro 启动期调用一次)。
65
- * 装载 = 动态 import 入口 → 形态校验 → createPluginContext → setup(ctx)
66
- * → 收集 routes/client → emit plugin:host:init
67
95
  */
68
96
  export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {}) {
69
97
  if (g.__awPluginHost) return g.__awPluginHost
70
98
  const logger = log()
99
+ const homeDir = modePaths(cwd).homeDir
71
100
  const host = {
72
101
  bus: new HookBus({
73
102
  name: 'aw-plugins',
@@ -75,15 +104,20 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
75
104
  }),
76
105
  routes: createRouteTable(),
77
106
  plugins: new Map(),
107
+ disposables: new Map(), // name → fn[](ctx.onDispose 登记)
108
+ hookOffs: new Map(), // name → off[](ctx.hooks.on 登记;热重载时解绑)
78
109
  failures: [],
79
110
  initedAt: null,
111
+ cwd,
112
+ packageRoot,
80
113
  logger,
81
114
  }
82
115
  g.__awPluginHost = host
83
116
 
84
- // 有效配置(只读面;引擎/模式解析从运行根动态加载,js-yaml 由对应 node_modules 解析)
117
+ // 有效配置(只读面;引擎/模式解析从运行根动态加载)
85
118
  let config = null
86
- let paths = { home: defaultHome(), configRoot: join(cwd, '.AgentWorkShop'), dataDir: join(cwd, '.AgentWorkShop', 'data') }
119
+ let settingsPath = null
120
+ let paths = { home: homeDir, configRoot: join(cwd, '.AgentWorkShop'), dataDir: join(cwd, '.AgentWorkShop', 'data') }
87
121
  try {
88
122
  const homeMod = await import(pathToUrl(join(cwd, 'shared', 'config', 'home.mjs')))
89
123
  const rm = homeMod.resolveRunMode({ cwd, packageRoot, env: process.env })
@@ -91,6 +125,7 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
91
125
  if (engineRoot && existsSync(join(engineRoot, 'shared', 'config', 'engine.mjs'))) {
92
126
  const engine = await import(pathToUrl(join(engineRoot, 'shared', 'config', 'engine.mjs')))
93
127
  config = engine.loadEffective({ configPath: rm.configPath, settingsPath: rm.settingsPath, env: process.env })
128
+ settingsPath = rm.settingsPath
94
129
  }
95
130
  paths = { home: rm.home, configRoot: rm.configRoot, dataDir: rm.dataDir }
96
131
  host.logger.info(`配置根: ${rm.configRoot} (${rm.mode} 模式)`)
@@ -98,9 +133,38 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
98
133
  catch (err) {
99
134
  host.logger.warn('配置引擎加载降级(插件 ctx.config 将为空):', err?.message)
100
135
  }
136
+ host.config = config
137
+
138
+ // 自环 origin:PORT env(prod:start.mjs 注入 / dev:dev-guard 注入 CLI 显式值)权威;
139
+ // nitro listen 钩子兜底回填
140
+ const argPort = (() => {
141
+ const argv = process.argv
142
+ const i = argv.indexOf('--port')
143
+ if (i >= 0 && argv[i + 1]) return argv[i + 1]
144
+ const eq = argv.find(a => a.startsWith('--port='))
145
+ return eq ? eq.slice(7) : null
146
+ })()
147
+ let selfOrigin = `http://127.0.0.1:${process.env.PORT ?? process.env.NITRO_PORT ?? argPort ?? config?.effective?.['server.dev.port'] ?? 3000}`
148
+ host.setSelfOrigin = (port) => {
149
+ selfOrigin = `http://127.0.0.1:${port}`
150
+ host.logger.info(`自环 origin 就绪: ${selfOrigin}`)
151
+ }
152
+ host.selfOrigin = () => selfOrigin
153
+ host.logger.info(`自环 origin: ${selfOrigin}`)
101
154
 
102
- const entries = discoverPluginDirs(cwd)
103
- if (entries.length) host.logger.info(`发现 ${entries.length} 个插件,开始装载 ...`)
155
+ await loadAllPlugins(host, { config, settingsPath, paths })
156
+ ensureStateWatcher(host)
157
+ return host
158
+ }
159
+
160
+ /** 装载/重载全部插件(跳过 disabled;manifest 仍可见) */
161
+ async function loadAllPlugins(host, { config, paths }) {
162
+ const homeDir = modePaths(host.cwd).homeDir
163
+ const disabled = readDisabledSet(homeDir)
164
+ host.disabledSet = disabled
165
+
166
+ const entries = discoverPluginDirs(host.cwd)
167
+ if (entries.length) host.logger.info(`发现 ${entries.length} 个插件(停用 ${disabled.size}),开始装载 ...`)
104
168
 
105
169
  for (const { dir, scope } of entries) {
106
170
  const entry = join(dir, 'index.mjs')
@@ -111,6 +175,22 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
111
175
  const def = check.def
112
176
 
113
177
  if (host.plugins.has(def.name)) throw new Error(`插件重名(后装载者跳过): ${def.name}`)
178
+ if (disabled.has(def.name)) {
179
+ host.plugins.set(def.name, {
180
+ name: def.name,
181
+ version: String(def.version ?? '0.0.0'),
182
+ description: String(def.description ?? ''),
183
+ scope,
184
+ dir,
185
+ entry,
186
+ clientPath: def.client ? resolve(dir, def.client) : null,
187
+ routes: [],
188
+ enabled: false,
189
+ error: null,
190
+ })
191
+ host.logger.info(`⊘ 跳过(已停用) [${scope}] ${def.name}`)
192
+ continue
193
+ }
114
194
 
115
195
  const rec = {
116
196
  name: def.name,
@@ -121,6 +201,8 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
121
201
  entry,
122
202
  clientPath: def.client ? resolve(dir, def.client) : null,
123
203
  routes: [],
204
+ enabled: true,
205
+ error: null,
124
206
  }
125
207
 
126
208
  const emitter = {
@@ -129,22 +211,45 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
129
211
  rec.routes.push({ method: String(m).toUpperCase(), path: p })
130
212
  },
131
213
  }
214
+ const perPluginDisposables = []
215
+ const hookOffs = []
216
+ host.disposables.set(def.name, perPluginDisposables)
217
+ host.hookOffs.set(def.name, hookOffs)
218
+ const scopedHooks = {
219
+ on: (t, fn) => {
220
+ const off = host.bus.on(t, fn)
221
+ hookOffs.push(off)
222
+ return off
223
+ },
224
+ once: (t, fn) => {
225
+ const off = host.bus.once(t, fn)
226
+ hookOffs.push(off)
227
+ return off
228
+ },
229
+ off: (t, fn) => host.bus.off(t, fn),
230
+ emit: (t, p) => host.bus.emit(t, p),
231
+ }
132
232
  const ctx = createPluginContext({
133
233
  name: def.name,
134
234
  scope,
135
235
  dir,
136
- hooks: host.bus,
236
+ hooks: scopedHooks,
137
237
  logger: {
138
- info: (...a) => logger.info(`[${def.name}]`, ...a),
139
- warn: (...a) => logger.warn(`[${def.name}]`, ...a),
140
- error: (...a) => logger.error(`[${def.name}]`, ...a),
238
+ debug: (...a) => host.logger.info(`[${def.name}][debug]`, ...a),
239
+ info: (...a) => host.logger.info(`[${def.name}]`, ...a),
240
+ warn: (...a) => host.logger.warn(`[${def.name}]`, ...a),
241
+ error: (...a) => host.logger.error(`[${def.name}]`, ...a),
141
242
  },
142
243
  config,
143
244
  paths,
144
245
  emitter,
246
+ onDispose: (fn) => {
247
+ perPluginDisposables.push(fn)
248
+ return fn
249
+ },
250
+ selfOrigin: () => selfOriginRef(host),
145
251
  })
146
252
 
147
- // 声明式 routes + setup 内 ctx.route() 两种形态都支持
148
253
  for (const r of (Array.isArray(def.routes) ? def.routes : [])) {
149
254
  emitter.registerRoute(def.name, r.method ?? 'GET', r.path, r.handler)
150
255
  }
@@ -161,10 +266,87 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
161
266
 
162
267
  host.initedAt = new Date().toISOString()
163
268
  await host.bus.emit('plugin:host:init', { plugins: [...host.plugins.keys()], failures: host.failures.length })
164
- host.logger.info(`装载完成: ${host.plugins.size} 成功 / ${host.failures.length} 失败 / 路由 ${host.routes.size} 条`)
269
+ host.logger.info(`装载完成: ${host.plugins.size} 个(含停用)/ ${host.failures.length} 失败 / 活跃路由 ${host.routes.size} 条`)
270
+ }
271
+
272
+ function selfOriginRef(host) {
273
+ return host.selfOrigin()
274
+ }
275
+
276
+ /** 热重载:全部 dispose/解绑 → 重新装载(跳过停用)→ 广播 plugins.reloaded(并发合并) */
277
+ export async function reloadPluginHost() {
278
+ const host = getPluginHost()
279
+ if (!host) return null
280
+ if (host.reloadInFlight) return host.reloadInFlight
281
+ host.reloadInFlight = doReload(host).finally(() => {
282
+ host.reloadInFlight = null
283
+ })
284
+ return host.reloadInFlight
285
+ }
286
+
287
+ async function doReload(host) {
288
+ host.logger.info('热重载插件宿主 ...')
289
+ for (const [name, list] of host.disposables ?? []) {
290
+ for (const fn of list.splice(0)) {
291
+ try {
292
+ await fn()
293
+ }
294
+ catch (err) {
295
+ host.logger.warn(`[${name}] onDispose 失败:`, err?.message)
296
+ }
297
+ }
298
+ }
299
+ for (const offs of (host.hookOffs ?? new Map()).values()) {
300
+ for (const off of offs.splice(0)) {
301
+ try {
302
+ off()
303
+ }
304
+ catch { /* 解绑失败忽略 */ }
305
+ }
306
+ }
307
+ host.plugins.clear()
308
+ host.routes = createRouteTable()
309
+ host.failures = []
310
+ await loadAllPlugins(host, { config: host.config, settingsPath: null, paths: { home: modePaths(host.cwd).homeDir, configRoot: join(host.cwd, '.AgentWorkShop'), dataDir: join(host.cwd, '.AgentWorkShop', 'data') } })
311
+ await host.bus.emit('plugins:reloaded', { plugins: pluginManifest() })
312
+ try {
313
+ const m = await import('@/server/services/workshop/scene-events')
314
+ m.broadcastSceneEvent('plugins.reloaded', { plugins: pluginManifest() })
315
+ }
316
+ catch { /* 广播失败不影响重载 */ }
165
317
  return host
166
318
  }
167
319
 
320
+ /** 启停单插件(写状态文件;调用方随后 reloadPluginHost 或由 state watcher 触发) */
321
+ export function setPluginEnabled(name, enabled) {
322
+ const host = getPluginHost()
323
+ const homeDir = modePaths(host?.cwd ?? process.cwd()).homeDir
324
+ const set = readDisabledSet(homeDir)
325
+ if (enabled) set.delete(name)
326
+ else set.add(name)
327
+ writeDisabledSet(homeDir, set)
328
+ return [...set]
329
+ }
330
+
331
+ /** 状态文件监视 → 热重载(CLI/Web 只写文件,服务自感知) */
332
+ function ensureStateWatcher(host) {
333
+ const statePath = statePathFor(modePaths(host.cwd).homeDir)
334
+ try {
335
+ mkdirSync(dirname(statePath), { recursive: true })
336
+ if (!existsSync(statePath)) writeDisabledSet(modePaths(host.cwd).homeDir, new Set())
337
+ let debounce = null
338
+ watch(statePath, () => {
339
+ clearTimeout(debounce)
340
+ debounce = setTimeout(() => {
341
+ void reloadPluginHost()
342
+ }, 400)
343
+ })
344
+ }
345
+ catch (err) {
346
+ host.logger.warn('状态文件监视不可用(启停需手动重启):', err?.message)
347
+ }
348
+ }
349
+
168
350
  /** 单例访问(未初始化返回 null —— 桥接点据此快速 no-op) */
169
351
  export function getPluginHost() {
170
352
  return g.__awPluginHost ?? null
@@ -200,7 +382,7 @@ export function readClientScript(name) {
200
382
  return { status: 200, code: readFileSync(rec.clientPath, 'utf8'), contentType: 'text/javascript; charset=utf-8' }
201
383
  }
202
384
 
203
- /** 清单(非敏感只读:名称/版本/作用域/描述/路由/是否有客户端) */
385
+ /** 清单(非敏感只读;含启停状态与路由) */
204
386
  export function pluginManifest() {
205
387
  const host = getPluginHost()
206
388
  if (!host) return []
@@ -209,15 +391,27 @@ export function pluginManifest() {
209
391
  version: r.version,
210
392
  description: r.description,
211
393
  scope: r.scope,
394
+ enabled: r.enabled !== false,
212
395
  hasClient: Boolean(r.clientPath),
213
396
  routes: host.routes.byPlugin(r.name),
397
+ error: r.error,
214
398
  }))
215
399
  }
216
400
 
217
- /** 关机钩子(nitro close 时调用) */
401
+ /** 关机钩子(nitro close 时调用):先逐插件回收订阅/定时器,再广播 server:close */
218
402
  export async function shutdownPluginHost() {
219
403
  const host = getPluginHost()
220
404
  if (!host) return
405
+ for (const [name, list] of host.disposables ?? []) {
406
+ for (const fn of list.splice(0)) {
407
+ try {
408
+ await fn()
409
+ }
410
+ catch (err) {
411
+ host.logger.warn(`[${name}] onDispose 失败:`, err?.message)
412
+ }
413
+ }
414
+ }
221
415
  await host.bus.emit('server:close', { at: new Date().toISOString() })
222
- host.logger.info('已发出 server:close,插件宿主关闭')
416
+ host.logger.info('插件清理完成,已发出 server:close')
223
417
  }