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.
- 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 +81 -48
- 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 +19 -2
- package/scripts/_dbg-docs-site-shot.mjs +33 -0
- package/scripts/dev-guard.mjs +13 -0
- package/scripts/home-bootstrap.mjs +26 -1
- package/sdk/api.mjs +101 -0
- package/sdk/client.mjs +56 -8
- package/sdk/context.mjs +56 -15
- 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/sdk/index.d.mts +135 -0
- package/sdk/index.mjs +5 -18
- package/sdk/lifecycle.mjs +25 -0
- package/server/api/plugins/[name]/[...path].ts +5 -2
- 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 +260 -62
- package/server/data/dcw-lines.json +63 -0
- package/server/data/dcw-products.json +49 -0
- package/server/data/dcw-recipes.json +140 -0
- package/server/data/dcw-rollback.json +1 -1
- package/server/data/dcw-runs.json +175 -126
- package/server/data/dcws.json +147 -0
- package/server/data/line-runs.json +85 -15
- package/server/plugins/aw-plugins.ts +7 -1
- 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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
-
// 有效配置(
|
|
117
|
+
// 有效配置(只读面;引擎/模式解析从运行根动态加载)
|
|
85
118
|
let config = null
|
|
86
|
-
let
|
|
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
|
-
|
|
103
|
-
|
|
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:
|
|
236
|
+
hooks: scopedHooks,
|
|
137
237
|
logger: {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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}
|
|
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('
|
|
416
|
+
host.logger.info('插件清理完成,已发出 server:close')
|
|
223
417
|
}
|