agentworkshop 0.2.1 → 0.2.2
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/app/plugins/aw-plugins.client.ts +79 -0
- package/cli/commands/plugin.mjs +169 -0
- package/cli/commands/update.mjs +12 -4
- package/docs/plugins.md +127 -0
- package/package.json +4 -2
- package/scripts/home-bootstrap.mjs +25 -1
- package/sdk/client.mjs +76 -0
- package/sdk/context.mjs +178 -0
- package/sdk/examples/sample-insight/client.mjs +23 -0
- package/sdk/examples/sample-insight/index.mjs +39 -0
- package/sdk/hooks.mjs +103 -0
- package/sdk/index.mjs +54 -0
- package/server/api/plugins/[name]/[...path].ts +19 -0
- package/server/api/plugins/client/[name].get.ts +18 -0
- package/server/api/plugins/manifest.get.ts +10 -0
- package/server/api/workshop/dcw/lines/[id]/start.post.ts +2 -0
- package/server/api/workshop/dcw/lines/[id]/stop.post.ts +2 -0
- package/server/data/daqs.json +170 -60
- package/server/data/dcw-lines.json +35 -0
- package/server/data/dcw-products.json +35 -0
- package/server/data/dcw-recipes.json +100 -0
- package/server/data/dcw-rollback.json +1 -1
- package/server/data/dcw-runs.json +125 -90
- package/server/data/dcws.json +105 -0
- package/server/data/line-runs.json +15 -15
- package/server/plugins/aw-plugins.ts +18 -0
- package/server/services/workshop/daq/daq-controller.ts +6 -2
- package/server/services/workshop/dcw/dcw-controller.ts +3 -0
- package/server/services/workshop/plugins/host.mjs +223 -0
- package/server/services/workshop/scene-events.ts +3 -0
package/sdk/context.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// AgentWorkShop SDK — 服务端插件上下文工厂(宿主调用;插件经 setup(ctx) 获得)
|
|
3
|
+
// ------------------------------------------------------------
|
|
4
|
+
// ctx 形态:
|
|
5
|
+
// ctx.name / ctx.scope('home'|'project') / ctx.dir — 身份与目录
|
|
6
|
+
// ctx.hooks HookBus — 生命周期钩子(on/once/off/emit)
|
|
7
|
+
// ctx.logger 前缀日志(info/warn/error/debug) — 插件名自动前缀
|
|
8
|
+
// ctx.config { get(key), all() } — 有效配置只读(四层引擎)
|
|
9
|
+
// ctx.dataDir / ctx.kv { get,set,all,bump } — 插件私有持久化(json 落配置根 data/plugins/<name>)
|
|
10
|
+
// ctx.route(method, path, handler) — 注册插件 API → /api/plugins/<name><path>
|
|
11
|
+
// ctx.http { get,post } 带超时 fetch — 对外请求(仅 http/https)
|
|
12
|
+
// ctx.events { on(type,fn) } — scene 实时事件订阅(event:* 桥的糖)
|
|
13
|
+
// ctx.paths { home, configRoot, dataDir } — 配置根信息
|
|
14
|
+
// ============================================================
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs'
|
|
16
|
+
import { dirname, join, resolve } from 'node:path'
|
|
17
|
+
import { HookBus } from './hooks.mjs'
|
|
18
|
+
|
|
19
|
+
export const SDK_VERSION = '0.2.2'
|
|
20
|
+
|
|
21
|
+
/** 允许的对外请求协议守卫(拒绝 file:/data: 等;宿主/插件同守此规则) */
|
|
22
|
+
function safeUrl(raw, timeoutMs = 8000) {
|
|
23
|
+
const u = new URL(String(raw))
|
|
24
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:')
|
|
25
|
+
throw new Error(`协议不允许: ${u.protocol}(仅 http/https)`)
|
|
26
|
+
return { signal: AbortSignal.timeout(timeoutMs) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 创建服务端插件上下文。
|
|
31
|
+
* @param {{ name, scope, dir, hooks, logger, config, paths, emitter }} opts 宿主装配
|
|
32
|
+
*/
|
|
33
|
+
export function createPluginContext(opts) {
|
|
34
|
+
const { name, scope, dir, hooks, config, paths, emitter } = opts
|
|
35
|
+
// 规范化 logger:宿主实现缺级时兜底 no-op(插件可用全套 debug/info/warn/error)
|
|
36
|
+
const logger = {
|
|
37
|
+
debug: () => {},
|
|
38
|
+
info: () => {},
|
|
39
|
+
warn: () => {},
|
|
40
|
+
error: () => {},
|
|
41
|
+
...(opts.logger ?? {}),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---- 插件私有持久化:内存态为准 + 200ms 防抖落盘 —— 高频钩子(daq:sample)
|
|
45
|
+
// 与低频钩子(line:stop)并发时无 read-modify-write 竞态(JS 同步内存操作原子) ----
|
|
46
|
+
const kvDir = join(paths.dataDir, 'plugins', name)
|
|
47
|
+
const kvFile = join(kvDir, 'kv.json')
|
|
48
|
+
const kvState = (() => {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(readFileSync(kvFile, 'utf8'))
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return {}
|
|
54
|
+
}
|
|
55
|
+
})()
|
|
56
|
+
let kvFlushTimer = null
|
|
57
|
+
const kvFlush = () => {
|
|
58
|
+
clearTimeout(kvFlushTimer)
|
|
59
|
+
kvFlushTimer = setTimeout(() => {
|
|
60
|
+
try {
|
|
61
|
+
mkdirSync(kvDir, { recursive: true })
|
|
62
|
+
const tmp = `${kvFile}.${process.pid}.tmp`
|
|
63
|
+
writeFileSync(tmp, `${JSON.stringify(kvState, null, 2)}\n`, 'utf8')
|
|
64
|
+
renameSync(tmp, kvFile)
|
|
65
|
+
}
|
|
66
|
+
catch { /* 磁盘异常不阻断插件 */ }
|
|
67
|
+
}, 200)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ctx = {
|
|
71
|
+
name,
|
|
72
|
+
scope,
|
|
73
|
+
dir: resolve(dir),
|
|
74
|
+
sdkVersion: SDK_VERSION,
|
|
75
|
+
hooks, // 宿主全局总线(与事件桥同源)
|
|
76
|
+
logger,
|
|
77
|
+
config: {
|
|
78
|
+
get: key => config?.effective?.[key],
|
|
79
|
+
all: () => ({ ...config?.effective }),
|
|
80
|
+
},
|
|
81
|
+
paths: { ...paths },
|
|
82
|
+
dataDir: kvDir,
|
|
83
|
+
kv: {
|
|
84
|
+
get: key => kvState[key],
|
|
85
|
+
set: (key, value) => {
|
|
86
|
+
kvState[key] = value
|
|
87
|
+
kvFlush()
|
|
88
|
+
return value
|
|
89
|
+
},
|
|
90
|
+
all: () => ({ ...kvState }),
|
|
91
|
+
bump: (key, by = 1) => {
|
|
92
|
+
kvState[key] = (Number(kvState[key]) || 0) + by
|
|
93
|
+
kvFlush()
|
|
94
|
+
return kvState[key]
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
route: (method, path, handler) => emitter?.registerRoute(name, method, path, handler),
|
|
98
|
+
http: {
|
|
99
|
+
get: (url, opts2 = {}) => fetch(url, { ...safeUrl(url, opts2.timeoutMs), ...opts2 }),
|
|
100
|
+
post: (url, body, opts2 = {}) => fetch(url, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'content-type': 'application/json', ...(opts2.headers ?? {}) },
|
|
103
|
+
body: JSON.stringify(body ?? {}),
|
|
104
|
+
...safeUrl(url, opts2.timeoutMs),
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
events: {
|
|
108
|
+
on: (type, fn) => hooks.on(type === '*' ? '*' : `event:${type}`, fn),
|
|
109
|
+
off: (type, fn) => hooks.off(type === '*' ? '*' : `event:${type}`, fn),
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
return ctx
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 宿主路由表(exact-match;插件 API 挂 /api/plugins/<name><path>) */
|
|
116
|
+
export function createRouteTable() {
|
|
117
|
+
const table = new Map() // `${method} ${name} ${path}` → handler
|
|
118
|
+
return {
|
|
119
|
+
register(name, method, path, handler) {
|
|
120
|
+
if (typeof handler !== 'function') return false
|
|
121
|
+
const key = `${String(method).toUpperCase()} ${name} ${path.startsWith('/') ? path : `/${path}`}`
|
|
122
|
+
table.set(key, handler)
|
|
123
|
+
return true
|
|
124
|
+
},
|
|
125
|
+
resolve(name, method, path) {
|
|
126
|
+
return table.get(`${String(method).toUpperCase()} ${name} ${path.startsWith('/') ? path : `/${path}`}`) ?? null
|
|
127
|
+
},
|
|
128
|
+
byPlugin(name) {
|
|
129
|
+
const out = []
|
|
130
|
+
for (const key of table.keys()) {
|
|
131
|
+
const [m, n, ...rest] = key.split(' ')
|
|
132
|
+
if (n === name) out.push({ method: m, path: rest.join('/') })
|
|
133
|
+
}
|
|
134
|
+
return out
|
|
135
|
+
},
|
|
136
|
+
get size() {
|
|
137
|
+
return table.size
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 校验插件入口导出(宿主装载前的形态检查) */
|
|
143
|
+
export function validatePluginModule(mod, source) {
|
|
144
|
+
const def = mod?.default ?? mod
|
|
145
|
+
if (!def || typeof def !== 'object' || typeof def.name !== 'string' || !def.name.trim()) {
|
|
146
|
+
return { ok: false, error: `插件缺少 name 或不是对象: ${source}` }
|
|
147
|
+
}
|
|
148
|
+
if (def.setup && typeof def.setup !== 'function') {
|
|
149
|
+
return { ok: false, error: `插件 setup 不是函数: ${def.name}` }
|
|
150
|
+
}
|
|
151
|
+
if (def.client && typeof def.client !== 'string') {
|
|
152
|
+
return { ok: false, error: `插件 client 必须是相对路径字符串: ${def.name}` }
|
|
153
|
+
}
|
|
154
|
+
return { ok: true, def }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 便捷:SDK 侧 definePlugin(纯类型糖;宿主同样接受裸对象) */
|
|
158
|
+
export function definePlugin(def) {
|
|
159
|
+
const check = validatePluginModule({ default: def }, def?.name ?? '(anonymous)')
|
|
160
|
+
if (!check.ok) throw new Error(check.error)
|
|
161
|
+
return def
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 插件 KV 目录探测(存在性只读) */
|
|
165
|
+
export function pluginKvExists(dataDir, name) {
|
|
166
|
+
return existsSync(join(resolve(dataDir), 'plugins', name, 'kv.json'))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { dirname }
|
|
170
|
+
|
|
171
|
+
export default {
|
|
172
|
+
SDK_VERSION,
|
|
173
|
+
HookBus,
|
|
174
|
+
createPluginContext,
|
|
175
|
+
createRouteTable,
|
|
176
|
+
validatePluginModule,
|
|
177
|
+
definePlugin,
|
|
178
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// sample-insight — 客户端增强(自包含 ESM,无裸导入)
|
|
2
|
+
// 右下角徽标实时显示采样计数(事件与 WS 同源)
|
|
3
|
+
export function setup(ctx) {
|
|
4
|
+
const badge = ctx.el('div', {
|
|
5
|
+
style: 'padding:8px 12px;border:1px solid #35e0a0;border-radius:10px;color:#35e0a0;'
|
|
6
|
+
+ 'background:rgba(8,20,16,.82);font:600 12px/1 ui-monospace,monospace;'
|
|
7
|
+
+ 'box-shadow:0 4px 16px rgba(0,0,0,.35);letter-spacing:.4px',
|
|
8
|
+
}, ['⌁ sample-insight · 0'])
|
|
9
|
+
|
|
10
|
+
ctx.root().append(badge)
|
|
11
|
+
|
|
12
|
+
let n = 0
|
|
13
|
+
ctx.on('daq:sample', () => {
|
|
14
|
+
n++
|
|
15
|
+
badge.textContent = `⌁ sample-insight · ${n} 样本`
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
ctx.on('page:change', ({ path }) => {
|
|
19
|
+
ctx.log.info('page →', path)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
ctx.log.info('client ready — 右下角徽标已挂载')
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sample-insight — SDK 示例插件(服务端面)。
|
|
3
|
+
* 订阅下发级数采采样计数,并暴露插件 API:GET /api/plugins/sample-insight/stats
|
|
4
|
+
* 完整指南见 docs/plugins.md。
|
|
5
|
+
*/
|
|
6
|
+
export default {
|
|
7
|
+
name: 'sample-insight',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
description: 'SDK 示例:数采采样计数 + 统计 API + 客户端徽标',
|
|
10
|
+
|
|
11
|
+
async setup(ctx) {
|
|
12
|
+
ctx.logger.info('示例插件已装载 —— daq:sample 计数中')
|
|
13
|
+
|
|
14
|
+
ctx.hooks.on('daq:sample', (s) => {
|
|
15
|
+
ctx.kv.bump('samples')
|
|
16
|
+
if (s?.nodeId) ctx.kv.set(`last:${s.nodeId}`, { value: s.value, at: s.at })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
ctx.hooks.on('line:start', (p) => {
|
|
20
|
+
ctx.logger.info(`产线开跑: ${p?.lineId}`)
|
|
21
|
+
ctx.kv.set('lineRunning', true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
ctx.hooks.on('line:stop', () => ctx.kv.set('lineRunning', false))
|
|
25
|
+
|
|
26
|
+
ctx.route('GET', '/stats', () => ({
|
|
27
|
+
plugin: ctx.name,
|
|
28
|
+
version: ctx.sdkVersion,
|
|
29
|
+
samples: ctx.kv.get('samples') ?? 0,
|
|
30
|
+
lineRunning: ctx.kv.get('lineRunning') ?? false,
|
|
31
|
+
scope: ctx.scope,
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
ctx.route('POST', '/reset', () => {
|
|
35
|
+
ctx.kv.set('samples', 0)
|
|
36
|
+
return { ok: true }
|
|
37
|
+
})
|
|
38
|
+
},
|
|
39
|
+
}
|
package/sdk/hooks.mjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// AgentWorkShop SDK — HookBus(插件生命周期钩子总线)
|
|
3
|
+
// ------------------------------------------------------------
|
|
4
|
+
// 设计:
|
|
5
|
+
// · 异步串行触发(监听器按注册序 await,可返回值形成 waterfall)
|
|
6
|
+
// · 错误隔离:单监听器抛错不影响其他监听器,错误经 onError 上报
|
|
7
|
+
// · 支持通配符 '*'(接收 { type, payload })
|
|
8
|
+
// · 连续失败自动摘除(熔断):同一监听器连续失败超阈值自动停用,
|
|
9
|
+
// 防止病态插件拖垮事件桥
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
const FAILLIMIT = 8
|
|
13
|
+
|
|
14
|
+
export class HookBus {
|
|
15
|
+
constructor({ name = 'hooks', onError } = {}) {
|
|
16
|
+
this.name = name
|
|
17
|
+
this.onError = onError ?? (() => {})
|
|
18
|
+
/** type → Array<{ fn, fails }> */
|
|
19
|
+
this.listeners = new Map()
|
|
20
|
+
this.count = 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 注册监听器;type='*' 接收全部事件({type,payload})。返回解绑函数 */
|
|
24
|
+
on(type, fn) {
|
|
25
|
+
if (typeof type !== 'string' || typeof fn !== 'function') return () => {}
|
|
26
|
+
const list = this.listeners.get(type) ?? []
|
|
27
|
+
list.push({ fn, fails: 0 })
|
|
28
|
+
this.listeners.set(type, list)
|
|
29
|
+
this.count++
|
|
30
|
+
return () => this.off(type, fn)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 一次性监听(触发后自动解绑) */
|
|
34
|
+
once(type, fn) {
|
|
35
|
+
const off = this.on(type, async (arg) => {
|
|
36
|
+
off()
|
|
37
|
+
return fn(arg)
|
|
38
|
+
})
|
|
39
|
+
return off
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
off(type, fn) {
|
|
43
|
+
const list = this.listeners.get(type)
|
|
44
|
+
if (!list) return
|
|
45
|
+
const idx = list.findIndex(l => l.fn === fn)
|
|
46
|
+
if (idx >= 0) {
|
|
47
|
+
list.splice(idx, 1)
|
|
48
|
+
this.count--
|
|
49
|
+
}
|
|
50
|
+
if (!list.length) this.listeners.delete(type)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 触发事件:同名监听器按注册序串行 await;返回最后一个非 undefined 返回值。
|
|
55
|
+
* emit 永不抛错——监听器错误被隔离并计数,连续超限自动摘除(熔断)。
|
|
56
|
+
*/
|
|
57
|
+
async emit(type, payload) {
|
|
58
|
+
const same = this.listeners.get(type)
|
|
59
|
+
const wild = this.listeners.get('*')
|
|
60
|
+
let result
|
|
61
|
+
if (same) {
|
|
62
|
+
for (const l of [...same]) {
|
|
63
|
+
const r = await this.#invoke(type, l, payload)
|
|
64
|
+
if (r !== undefined) result = r
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (wild) {
|
|
68
|
+
for (const l of [...wild]) {
|
|
69
|
+
await this.#invoke('*', l, { type, payload })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async #invoke(type, l, payload) {
|
|
76
|
+
try {
|
|
77
|
+
return await l.fn(payload)
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
l.fails++
|
|
81
|
+
try {
|
|
82
|
+
this.onError(err, { bus: this.name, type, fails: l.fails })
|
|
83
|
+
}
|
|
84
|
+
catch { /* 上报器自身错误忽略 */ }
|
|
85
|
+
if (l.fails >= FAILLIMIT) {
|
|
86
|
+
const list = this.listeners.get(type)
|
|
87
|
+
const idx = list?.indexOf(l) ?? -1
|
|
88
|
+
if (idx >= 0) {
|
|
89
|
+
list.splice(idx, 1)
|
|
90
|
+
this.count--
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 已注册监听器总数(诊断用) */
|
|
98
|
+
get size() {
|
|
99
|
+
return this.count
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default HookBus
|
package/sdk/index.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// AgentWorkShop SDK — 门面
|
|
3
|
+
// ------------------------------------------------------------
|
|
4
|
+
// 插件作者两种写法:
|
|
5
|
+
// 1) 零依赖(推荐,全局安装零解析链约束下的标准形态):
|
|
6
|
+
// export default { name, version, setup(ctx), client: './client.mjs', routes: [...] }
|
|
7
|
+
// —— ctx 由宿主注入,本 SDK 对插件运行时非必需。
|
|
8
|
+
// 2) 显式糖(本地开发/单体使用时):
|
|
9
|
+
// import { definePlugin } from 'agentworkshop/sdk'
|
|
10
|
+
// export default definePlugin({ ... })
|
|
11
|
+
// 生命周期(宿主触发):
|
|
12
|
+
// 服务端: setup(ctx) → plugin:host:init → event:*/daq:sample/dcw:write/line:start|stop
|
|
13
|
+
// → server:close
|
|
14
|
+
// 客户端: client.mjs setup(ctx) → client:init → event:*/page:change
|
|
15
|
+
// ============================================================
|
|
16
|
+
import { SDK_VERSION, definePlugin, createPluginContext, createRouteTable, validatePluginModule, pluginKvExists } from './context.mjs'
|
|
17
|
+
import { HookBus } from './hooks.mjs'
|
|
18
|
+
import { CLIENT_SDK_VERSION, createClientContext } from './client.mjs'
|
|
19
|
+
|
|
20
|
+
export { SDK_VERSION, definePlugin, createPluginContext, createRouteTable, validatePluginModule, pluginKvExists } from './context.mjs'
|
|
21
|
+
export { HookBus } from './hooks.mjs'
|
|
22
|
+
export { CLIENT_SDK_VERSION, createClientContext } from './client.mjs'
|
|
23
|
+
|
|
24
|
+
/** 服务端生命周期事件清单(宿主触发;文档见 docs/plugins.md) */
|
|
25
|
+
export const LIFECYCLE_EVENTS = Object.freeze([
|
|
26
|
+
'plugin:host:init',
|
|
27
|
+
'event:*',
|
|
28
|
+
'daq:sample',
|
|
29
|
+
'dcw:write',
|
|
30
|
+
'line:start',
|
|
31
|
+
'line:stop',
|
|
32
|
+
'server:close',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
/** 客户端生命周期事件清单 */
|
|
36
|
+
export const CLIENT_EVENTS = Object.freeze([
|
|
37
|
+
'client:init',
|
|
38
|
+
'event:*',
|
|
39
|
+
'page:change',
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
export default {
|
|
43
|
+
SDK_VERSION,
|
|
44
|
+
definePlugin,
|
|
45
|
+
HookBus,
|
|
46
|
+
createPluginContext,
|
|
47
|
+
createRouteTable,
|
|
48
|
+
validatePluginModule,
|
|
49
|
+
pluginKvExists,
|
|
50
|
+
CLIENT_SDK_VERSION,
|
|
51
|
+
createClientContext,
|
|
52
|
+
LIFECYCLE_EVENTS,
|
|
53
|
+
CLIENT_EVENTS,
|
|
54
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /api/plugins/:name/** —— 插件自注册 API 的转发层(exact-match)。
|
|
3
|
+
* 插件经 ctx.route(method, path, handler) 注册;handler(event) 返回值由 nitro 序列化。
|
|
4
|
+
* 鉴权由插件自行处理(v1 不强制;可经 resolveUser 复用业务鉴权)。
|
|
5
|
+
*/
|
|
6
|
+
import { defineEventHandler, createError } from 'h3'
|
|
7
|
+
import { getPluginHost } from '@/server/services/workshop/plugins/host.mjs'
|
|
8
|
+
|
|
9
|
+
export default defineEventHandler((event) => {
|
|
10
|
+
const host = getPluginHost()
|
|
11
|
+
if (!host) throw createError({ statusCode: 503, statusMessage: 'plugin host not ready' })
|
|
12
|
+
const name = String(event.context.params?.name ?? '')
|
|
13
|
+
const path = '/' + (event.context.params?.path ?? '').replace(/^\/+/, '')
|
|
14
|
+
const handler = host.routes.resolve(name, event.method, path)
|
|
15
|
+
if (!handler) {
|
|
16
|
+
throw createError({ statusCode: 404, statusMessage: `plugin route not found: ${event.method} /api/plugins/${name}${path}` })
|
|
17
|
+
}
|
|
18
|
+
return handler(event)
|
|
19
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/plugins/client/:name —— 插件客户端脚本(text/javascript)。
|
|
3
|
+
* 自包含 ESM(无裸导入);客户端 loader 动态 import 后以 setup(ctx) 装载。
|
|
4
|
+
* 信任模型与 aw commands 相同:仅装载自己放入插件目录的可信代码。
|
|
5
|
+
*/
|
|
6
|
+
import { defineEventHandler, getRouterParam, setHeader, createError } from 'h3'
|
|
7
|
+
import { readClientScript } from '@/server/services/workshop/plugins/host.mjs'
|
|
8
|
+
|
|
9
|
+
export default defineEventHandler((event) => {
|
|
10
|
+
const name = String(getRouterParam(event, 'name') ?? '').replace(/\.mjs$/, '')
|
|
11
|
+
const r = readClientScript(name)
|
|
12
|
+
if (r.status !== 200) {
|
|
13
|
+
throw createError({ statusCode: r.status, statusMessage: r.status === 404 ? 'plugin client not found' : 'bad request' })
|
|
14
|
+
}
|
|
15
|
+
setHeader(event, 'content-type', r.contentType ?? 'text/javascript; charset=utf-8')
|
|
16
|
+
setHeader(event, 'cache-control', 'no-cache')
|
|
17
|
+
return r.code
|
|
18
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/plugins/manifest —— 已装载插件清单(免鉴权只读:名称/版本/作用域/路由/是否有客户端)。
|
|
3
|
+
* 客户端 loader 启动期拉取;未登录/服务未装载时返回空数组(前端静默)。
|
|
4
|
+
*/
|
|
5
|
+
import { pluginManifest } from '@/server/services/workshop/plugins/host.mjs'
|
|
6
|
+
import { defineEventHandler } from 'h3'
|
|
7
|
+
|
|
8
|
+
export default defineEventHandler(() => {
|
|
9
|
+
return { plugins: pluginManifest() }
|
|
10
|
+
})
|
|
@@ -9,6 +9,7 @@ import { bindDcwBroadcast, getDcwController } from '@/server/services/workshop/d
|
|
|
9
9
|
import { broadcastSceneEvent } from '@/server/services/workshop/scene-events'
|
|
10
10
|
import { getActiveLineRun } from '@/server/services/workshop/dcw/line-run'
|
|
11
11
|
import { recordOps } from '@/server/services/workshop/ops/ops'
|
|
12
|
+
import { emitLineLifecycle } from '@/server/services/workshop/plugins/host.mjs'
|
|
12
13
|
|
|
13
14
|
export default defineApiHandler(async (event) => {
|
|
14
15
|
const user = resolveUser(event)
|
|
@@ -31,5 +32,6 @@ export default defineApiHandler(async (event) => {
|
|
|
31
32
|
recipeId: active?.recipeId ?? String(body.recipeId ?? ''),
|
|
32
33
|
detail: { runId: run.id, recipeId: body.recipeId ?? '' },
|
|
33
34
|
})
|
|
35
|
+
emitLineLifecycle('line:start', { lineId: id, runId: run.id, recipeId: active?.recipeId ?? String(body.recipeId ?? ''), productName: active?.productName ?? '' })
|
|
34
36
|
return { run, line: getDcwController().lineState(id) }
|
|
35
37
|
})
|
|
@@ -9,6 +9,7 @@ import { bindDcwBroadcast, getDcwController } from '@/server/services/workshop/d
|
|
|
9
9
|
import { broadcastSceneEvent } from '@/server/services/workshop/scene-events'
|
|
10
10
|
import { getActiveLineRun } from '@/server/services/workshop/dcw/line-run'
|
|
11
11
|
import { recordOps } from '@/server/services/workshop/ops/ops'
|
|
12
|
+
import { emitLineLifecycle } from '@/server/services/workshop/plugins/host.mjs'
|
|
12
13
|
|
|
13
14
|
export default defineApiHandler(async (event) => {
|
|
14
15
|
const user = resolveUser(event)
|
|
@@ -30,5 +31,6 @@ export default defineApiHandler(async (event) => {
|
|
|
30
31
|
recipeId: active?.recipeId ?? '',
|
|
31
32
|
detail: { runId: active?.runId ?? '' },
|
|
32
33
|
})
|
|
34
|
+
emitLineLifecycle('line:stop', { lineId: id, runId: active?.runId ?? run?.id ?? '' })
|
|
33
35
|
return { run, line: getDcwController().lineState(id) }
|
|
34
36
|
})
|