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.
@@ -1866,5 +1866,25 @@
1866
1866
  "lastGoodRunId": null,
1867
1867
  "createdAt": "2026-09-02T04:20:58.370Z",
1868
1868
  "updatedAt": "2026-09-02T04:20:58.370Z"
1869
+ },
1870
+ {
1871
+ "id": "rc-21479838",
1872
+ "productId": "pd-d919d4bf",
1873
+ "lineId": "ln-f223008a",
1874
+ "name": "配方-mtjo000v",
1875
+ "description": "",
1876
+ "params": [
1877
+ {
1878
+ "nodeId": "dw-6ab87477",
1879
+ "templateRef": "temp-sp",
1880
+ "value": 120
1881
+ }
1882
+ ],
1883
+ "daqWindows": [],
1884
+ "version": 1,
1885
+ "paramsHistory": [],
1886
+ "lastGoodRunId": null,
1887
+ "createdAt": "2026-09-02T05:37:46.125Z",
1888
+ "updatedAt": "2026-09-02T05:37:46.125Z"
1869
1889
  }
1870
1890
  ]
@@ -3281,7 +3281,7 @@
3281
3281
  "channels": [],
3282
3282
  "degraded": true
3283
3283
  },
3284
- "evaluatedAt": "2026-09-02T04:34:39.231Z",
3284
+ "evaluatedAt": "2026-09-02T05:50:22.431Z",
3285
3285
  "windowAgg": {
3286
3286
  "at": "2026-08-31T16:25:17.398Z",
3287
3287
  "fromMs": 1788193396897,
@@ -1,22 +1,4 @@
1
1
  [
2
- {
3
- "id": "rr-127c6247",
4
- "recipeId": "rc-28c5aeb8",
5
- "recipeName": "终审配方A",
6
- "productId": "pd-54f429af",
7
- "lineId": "ln-c5145881",
8
- "startedAt": "2026-08-29T15:05:46.202Z",
9
- "endedAt": "2026-08-29T15:05:58.337Z",
10
- "results": [
11
- {
12
- "templateRef": "dcw-temp-sp",
13
- "nodeId": "dw-c5912d51",
14
- "ok": true,
15
- "message": "写入并回读一致:182 → raw 1280,回读 182(标定后物理值 182)",
16
- "value": 182
17
- }
18
- ]
19
- },
20
2
  {
21
3
  "id": "rr-a5f58b1a",
22
4
  "recipeId": "rc-7511c1bb",
@@ -4706,5 +4688,30 @@
4706
4688
  "value": 120
4707
4689
  }
4708
4690
  ]
4691
+ },
4692
+ {
4693
+ "id": "rr-06caf6f4",
4694
+ "recipeId": "rc-21479838",
4695
+ "recipeName": "配方-mtjo000v",
4696
+ "productId": "pd-d919d4bf",
4697
+ "lineId": "ln-f223008a",
4698
+ "startedAt": "2026-09-02T05:37:50.743Z",
4699
+ "endedAt": null,
4700
+ "results": [
4701
+ {
4702
+ "templateRef": "temp-sp",
4703
+ "nodeId": "dw-6ab87477",
4704
+ "ok": false,
4705
+ "message": "设定值 120℃ 越出工艺安全量程 [150, 200] ℃",
4706
+ "value": 120
4707
+ }
4708
+ ],
4709
+ "paramsSnapshot": [
4710
+ {
4711
+ "nodeId": "dw-6ab87477",
4712
+ "templateRef": "temp-sp",
4713
+ "value": 120
4714
+ }
4715
+ ]
4709
4716
  }
4710
4717
  ]
@@ -1963,5 +1963,26 @@
1963
1963
  "state": "idle",
1964
1964
  "lastError": null,
1965
1965
  "createdAt": "2026-09-02T04:20:58.320Z"
1966
+ },
1967
+ {
1968
+ "id": "dw-6ab87477",
1969
+ "templateRef": "temp-sp",
1970
+ "name": "温控",
1971
+ "driver": "mock",
1972
+ "enabled": true,
1973
+ "holdIntervalMs": null,
1974
+ "unit": "℃",
1975
+ "decimals": 1,
1976
+ "min": 150,
1977
+ "max": 200,
1978
+ "deviceBindingId": null,
1979
+ "driverConfig": {},
1980
+ "lineId": "ln-f223008a",
1981
+ "value": null,
1982
+ "lastAckAt": null,
1983
+ "lastWriteAt": null,
1984
+ "state": "idle",
1985
+ "lastError": null,
1986
+ "createdAt": "2026-09-02T05:37:46.098Z"
1966
1987
  }
1967
1988
  ]
@@ -7,7 +7,7 @@
7
7
  "productId": "pd-3ad54846",
8
8
  "productName": "光学膜 BOPP",
9
9
  "startedAt": "2026-08-31T16:20:13.707Z",
10
- "taggedSamples": 29764
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": 11692
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": 14665
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": 14955
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": 14541
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": 14520
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": 14553
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": 14604
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": 14476
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": 13899
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": 12890
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": 12739
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": 12384
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": 10539
140
+ "taggedSamples": 10540
141
141
  },
142
142
  {
143
143
  "lineId": "ln-af002514",
@@ -147,7 +147,7 @@
147
147
  "productId": "pd-6348a595",
148
148
  "productName": "0.8mm 光学膜",
149
149
  "startedAt": "2026-09-01T14:06:28.766Z",
150
- "taggedSamples": 952
150
+ "taggedSamples": 956
151
151
  },
152
152
  {
153
153
  "lineId": "ln-0eccf6ba",
@@ -157,7 +157,7 @@
157
157
  "productId": "pd-4b71be77",
158
158
  "productName": "哨兵产品-mtjku36w",
159
159
  "startedAt": "2026-09-02T04:09:11.482Z",
160
- "taggedSamples": 5
160
+ "taggedSamples": 6
161
161
  },
162
162
  {
163
163
  "lineId": "ln-e984246e",
@@ -167,7 +167,7 @@
167
167
  "productId": "pd-bb1e0bed",
168
168
  "productName": "产品C-mtjkwyhu",
169
169
  "startedAt": "2026-09-02T04:11:26.583Z",
170
- "taggedSamples": 4
170
+ "taggedSamples": 5
171
171
  },
172
172
  {
173
173
  "lineId": "ln-1fe0b5bc",
@@ -177,7 +177,7 @@
177
177
  "productId": "pd-9f2d9507",
178
178
  "productName": "产品D-mtjkzcfr",
179
179
  "startedAt": "2026-09-02T04:13:17.902Z",
180
- "taggedSamples": 3
180
+ "taggedSamples": 4
181
181
  },
182
182
  {
183
183
  "lineId": "ln-d826473e",
@@ -187,7 +187,7 @@
187
187
  "productId": "pd-d230255e",
188
188
  "productName": "产品E-mtjl43pv",
189
189
  "startedAt": "2026-09-02T04:16:59.891Z",
190
- "taggedSamples": 2
190
+ "taggedSamples": 3
191
191
  },
192
192
  {
193
193
  "lineId": "ln-156abb71",
@@ -197,7 +197,7 @@
197
197
  "productId": "pd-6065b4ba",
198
198
  "productName": "产品F-mtjl60qf",
199
199
  "startedAt": "2026-09-02T04:18:29.340Z",
200
- "taggedSamples": 1
200
+ "taggedSamples": 2
201
201
  },
202
202
  {
203
203
  "lineId": "ln-bac43927",
@@ -207,6 +207,16 @@
207
207
  "productId": "pd-52f54e33",
208
208
  "productName": "产品G-mtjl98nl",
209
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",
210
220
  "taggedSamples": 0
211
221
  }
212
222
  ]
@@ -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, watch } 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,17 +104,20 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
75
104
  }),
76
105
  routes: createRouteTable(),
77
106
  plugins: new Map(),
78
- disposables: new Map(), // name → fn[](setup 内 ctx.onDispose 登记;server:close 时回收)
107
+ disposables: new Map(), // name → fn[](ctx.onDispose 登记)
108
+ hookOffs: new Map(), // name → off[](ctx.hooks.on 登记;热重载时解绑)
79
109
  failures: [],
80
110
  initedAt: null,
111
+ cwd,
112
+ packageRoot,
81
113
  logger,
82
114
  }
83
115
  g.__awPluginHost = host
84
116
 
85
- // 有效配置(只读面;引擎/模式解析从运行根动态加载,js-yaml 由对应 node_modules 解析)
117
+ // 有效配置(只读面;引擎/模式解析从运行根动态加载)
86
118
  let config = null
87
119
  let settingsPath = null
88
- let paths = { home: defaultHome(), configRoot: join(cwd, '.AgentWorkShop'), dataDir: join(cwd, '.AgentWorkShop', 'data') }
120
+ let paths = { home: homeDir, configRoot: join(cwd, '.AgentWorkShop'), dataDir: join(cwd, '.AgentWorkShop', 'data') }
89
121
  try {
90
122
  const homeMod = await import(pathToUrl(join(cwd, 'shared', 'config', 'home.mjs')))
91
123
  const rm = homeMod.resolveRunMode({ cwd, packageRoot, env: process.env })
@@ -102,8 +134,9 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
102
134
  host.logger.warn('配置引擎加载降级(插件 ctx.config 将为空):', err?.message)
103
135
  }
104
136
  host.config = config
105
- // 自环 origin:PORT env(prod) > argv --port(dev:dev-guard 转发链天然携带) > 配置 dev 端口;
106
- // nitro listen 钩子(setSelfOrigin)仍保留为最终兜底
137
+
138
+ // 自环 origin:PORT env(prod:start.mjs 注入 / dev:dev-guard 注入 CLI 显式值)权威;
139
+ // nitro listen 钩子兜底回填
107
140
  const argPort = (() => {
108
141
  const argv = process.argv
109
142
  const i = argv.indexOf('--port')
@@ -117,38 +150,21 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
117
150
  host.logger.info(`自环 origin 就绪: ${selfOrigin}`)
118
151
  }
119
152
  host.selfOrigin = () => selfOrigin
120
- host.logger.info(`自环 origin: ${selfOrigin} (PORT=${process.env.PORT ?? '∅'} NITRO_PORT=${process.env.NITRO_PORT ?? '∅'} argPort=${argPort ?? '∅'})`)
153
+ host.logger.info(`自环 origin: ${selfOrigin}`)
121
154
 
122
- // 配置变更监听:runtime-settings.json 变化 原地刷新 effective + config:changed 钩子
123
- if (settingsPath && existsSync(settingsPath)) {
124
- try {
125
- let debounce = null
126
- watch(settingsPath, () => {
127
- clearTimeout(debounce)
128
- debounce = setTimeout(async () => {
129
- try {
130
- if (host.config && existsSync(settingsPath)) {
131
- const homeMod = await import(pathToUrl(join(cwd, 'shared', 'config', 'home.mjs')))
132
- const rm = homeMod.resolveRunMode({ cwd, packageRoot, env: process.env })
133
- const engine = await import(pathToUrl(join((rm.mode === 'repo' ? rm.root : packageRoot ?? rm.root) ?? cwd, 'shared', 'config', 'engine.mjs')))
134
- const fresh = engine.loadEffective({ configPath: rm.configPath, settingsPath: rm.settingsPath, env: process.env })
135
- Object.assign(host.config.effective, fresh.effective)
136
- host.config.sources = fresh.sources
137
- }
138
- await host.bus.emit('config:changed', { at: new Date().toISOString() })
139
- host.logger.info('配置变更已广播(config:changed)')
140
- }
141
- catch (err) {
142
- host.logger.warn('config:changed 处理失败:', err?.message)
143
- }
144
- }, 300)
145
- })
146
- }
147
- catch { /* fs.watch 不可用时插件可经轮询自行感知 */ }
148
- }
155
+ await loadAllPlugins(host, { config, settingsPath, paths })
156
+ ensureStateWatcher(host)
157
+ return host
158
+ }
149
159
 
150
- const entries = discoverPluginDirs(cwd)
151
- if (entries.length) host.logger.info(`发现 ${entries.length} 个插件,开始装载 ...`)
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}),开始装载 ...`)
152
168
 
153
169
  for (const { dir, scope } of entries) {
154
170
  const entry = join(dir, 'index.mjs')
@@ -159,6 +175,22 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
159
175
  const def = check.def
160
176
 
161
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
+ }
162
194
 
163
195
  const rec = {
164
196
  name: def.name,
@@ -169,6 +201,8 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
169
201
  entry,
170
202
  clientPath: def.client ? resolve(dir, def.client) : null,
171
203
  routes: [],
204
+ enabled: true,
205
+ error: null,
172
206
  }
173
207
 
174
208
  const emitter = {
@@ -178,17 +212,33 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
178
212
  },
179
213
  }
180
214
  const perPluginDisposables = []
215
+ const hookOffs = []
181
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
+ }
182
232
  const ctx = createPluginContext({
183
233
  name: def.name,
184
234
  scope,
185
235
  dir,
186
- hooks: host.bus,
236
+ hooks: scopedHooks,
187
237
  logger: {
188
- debug: (...a) => logger.info(`[${def.name}][debug]`, ...a),
189
- info: (...a) => logger.info(`[${def.name}]`, ...a),
190
- warn: (...a) => logger.warn(`[${def.name}]`, ...a),
191
- 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),
192
242
  },
193
243
  config,
194
244
  paths,
@@ -197,10 +247,9 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
197
247
  perPluginDisposables.push(fn)
198
248
  return fn
199
249
  },
200
- selfOrigin: host.selfOrigin,
250
+ selfOrigin: () => selfOriginRef(host),
201
251
  })
202
252
 
203
- // 声明式 routes + setup 内 ctx.route() 两种形态都支持
204
253
  for (const r of (Array.isArray(def.routes) ? def.routes : [])) {
205
254
  emitter.registerRoute(def.name, r.method ?? 'GET', r.path, r.handler)
206
255
  }
@@ -217,10 +266,87 @@ export async function initPluginHost({ cwd = process.cwd(), packageRoot } = {})
217
266
 
218
267
  host.initedAt = new Date().toISOString()
219
268
  await host.bus.emit('plugin:host:init', { plugins: [...host.plugins.keys()], failures: host.failures.length })
220
- 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 { /* 广播失败不影响重载 */ }
221
317
  return host
222
318
  }
223
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
+
224
350
  /** 单例访问(未初始化返回 null —— 桥接点据此快速 no-op) */
225
351
  export function getPluginHost() {
226
352
  return g.__awPluginHost ?? null
@@ -256,7 +382,7 @@ export function readClientScript(name) {
256
382
  return { status: 200, code: readFileSync(rec.clientPath, 'utf8'), contentType: 'text/javascript; charset=utf-8' }
257
383
  }
258
384
 
259
- /** 清单(非敏感只读:名称/版本/作用域/描述/路由/是否有客户端) */
385
+ /** 清单(非敏感只读;含启停状态与路由) */
260
386
  export function pluginManifest() {
261
387
  const host = getPluginHost()
262
388
  if (!host) return []
@@ -265,8 +391,10 @@ export function pluginManifest() {
265
391
  version: r.version,
266
392
  description: r.description,
267
393
  scope: r.scope,
394
+ enabled: r.enabled !== false,
268
395
  hasClient: Boolean(r.clientPath),
269
396
  routes: host.routes.byPlugin(r.name),
397
+ error: r.error,
270
398
  }))
271
399
  }
272
400