agentworkshop 0.2.1 → 0.3.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/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 +19 -2
- package/scripts/dev-guard.mjs +13 -0
- package/scripts/home-bootstrap.mjs +25 -1
- package/sdk/api.mjs +101 -0
- package/sdk/client.mjs +120 -0
- package/sdk/context.mjs +219 -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.d.mts +135 -0
- package/sdk/index.mjs +41 -0
- package/sdk/lifecycle.mjs +25 -0
- package/server/api/plugins/[name]/[...path].ts +22 -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 +347 -61
- package/server/data/dcw-lines.json +91 -0
- package/server/data/dcw-products.json +77 -0
- package/server/data/dcw-recipes.json +220 -0
- package/server/data/dcw-rollback.json +1 -1
- package/server/data/dcw-runs.json +275 -198
- package/server/data/dcws.json +231 -0
- package/server/data/line-runs.json +74 -14
- package/server/plugins/aw-plugins.ts +24 -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 +289 -0
- package/server/services/workshop/scene-events.ts +3 -0
package/sdk/client.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// AgentWorkShop SDK — 客户端插件上下文工厂(客户端 loader 调用)
|
|
3
|
+
// ------------------------------------------------------------
|
|
4
|
+
// 插件 client.mjs 导出:
|
|
5
|
+
// export function setup(ctx) { ctx.on('daq.reading', d => { ... }) }
|
|
6
|
+
// ctx 形态(浏览器侧,自包含、零框架依赖):
|
|
7
|
+
// ctx.name / ctx.sdkVersion
|
|
8
|
+
// ctx.hooks 客户端本地 HookBus(client:init / event:* / page:change / client:destroy)
|
|
9
|
+
// ctx.on(type, fn) scene 实时事件订阅(与 WS 同源;'*' 通配)——自动登记 pagehide 回收
|
|
10
|
+
// ctx.fetch(path, …) 同源平台 API 助手(JSON;自动解信封 data)
|
|
11
|
+
// ctx.el(tag, attrs, children) DOM 助手(挂到任意面板/宿主节点)
|
|
12
|
+
// ctx.mount(selector|el, node) 挂载节点(缺失时挂 body 角落)
|
|
13
|
+
// ctx.root 插件 UI 挂载点(懒创建,自动附加到 body,#aw-plugin-<name>)
|
|
14
|
+
// ctx.log 前缀 console
|
|
15
|
+
// ctx.dispose() 卸载:清空挂载点 + 回收订阅 + 广播 client:destroy(pagehide 自动触发)
|
|
16
|
+
// ============================================================
|
|
17
|
+
import { HookBus } from './hooks.mjs'
|
|
18
|
+
|
|
19
|
+
export const CLIENT_SDK_VERSION = '0.3.0'
|
|
20
|
+
|
|
21
|
+
function el(tag, attrs = {}, children = []) {
|
|
22
|
+
const node = document.createElement(tag)
|
|
23
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
24
|
+
if (k === 'style') node.style.cssText = v
|
|
25
|
+
else if (k === 'class') node.className = v
|
|
26
|
+
else if (k.startsWith('on') && typeof v === 'function') node.addEventListener(k.slice(2), v)
|
|
27
|
+
else node.setAttribute(k, String(v))
|
|
28
|
+
}
|
|
29
|
+
for (const c of [].concat(children)) {
|
|
30
|
+
node.append(typeof c === 'string' ? document.createTextNode(c) : c)
|
|
31
|
+
}
|
|
32
|
+
return node
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createClientContext({ name, eventBridge, baseUrl = '' }) {
|
|
36
|
+
const hooks = new HookBus({ name: `client:${name}`, onError: err => console.warn(`[aw-plugin:${name}]`, err) })
|
|
37
|
+
const disposables = []
|
|
38
|
+
let rootEl = null
|
|
39
|
+
let disposed = false
|
|
40
|
+
|
|
41
|
+
const ctx = {
|
|
42
|
+
name,
|
|
43
|
+
sdkVersion: CLIENT_SDK_VERSION,
|
|
44
|
+
hooks,
|
|
45
|
+
on: (type, fn) => {
|
|
46
|
+
const off = type === 'event:*' ? hooks.on('*', fn) : hooks.on(`event:${type}`, fn)
|
|
47
|
+
disposables.push(off)
|
|
48
|
+
return off
|
|
49
|
+
},
|
|
50
|
+
/** 同源平台 API(JSON;自动解 {data} 信封;非 2xx 抛错) */
|
|
51
|
+
fetch: async (path, opt = {}) => {
|
|
52
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
53
|
+
headers: { accept: 'application/json', ...(opt.body !== undefined ? { 'content-type': 'application/json' } : {}), ...(opt.headers ?? {}) },
|
|
54
|
+
method: opt.method ?? (opt.body !== undefined ? 'POST' : 'GET'),
|
|
55
|
+
body: opt.body !== undefined ? JSON.stringify(opt.body) : undefined,
|
|
56
|
+
})
|
|
57
|
+
const json = await res.json().catch(() => null)
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
const err = new Error(json?.message ?? `HTTP ${res.status} ${path}`)
|
|
60
|
+
err.status = res.status
|
|
61
|
+
throw err
|
|
62
|
+
}
|
|
63
|
+
return json && typeof json === 'object' && 'data' in json ? json.data : json
|
|
64
|
+
},
|
|
65
|
+
el,
|
|
66
|
+
log: {
|
|
67
|
+
info: (...a) => console.info(`[aw-plugin:${name}]`, ...a),
|
|
68
|
+
warn: (...a) => console.warn(`[aw-plugin:${name}]`, ...a),
|
|
69
|
+
error: (...a) => console.error(`[aw-plugin:${name}]`, ...a),
|
|
70
|
+
},
|
|
71
|
+
root: () => {
|
|
72
|
+
rootEl ??= (() => {
|
|
73
|
+
const found = document.getElementById(`aw-plugin-${name}`)
|
|
74
|
+
if (found) return found
|
|
75
|
+
const box = el('div', { id: `aw-plugin-${name}`, style: 'position:fixed;right:16px;bottom:16px;z-index:2147483000;font:12px/1.6 ui-monospace,monospace' })
|
|
76
|
+
document.body.append(box)
|
|
77
|
+
return box
|
|
78
|
+
})()
|
|
79
|
+
return rootEl
|
|
80
|
+
},
|
|
81
|
+
mount: (target, node) => {
|
|
82
|
+
const host = typeof target === 'string' ? document.querySelector(target) : target
|
|
83
|
+
;(host ?? ctx.root()).append(node)
|
|
84
|
+
return node
|
|
85
|
+
},
|
|
86
|
+
/** 卸载:回收订阅 + 清空挂载点 + 广播 client:destroy(幂等) */
|
|
87
|
+
dispose: () => {
|
|
88
|
+
if (disposed) return
|
|
89
|
+
disposed = true
|
|
90
|
+
void hooks.emit('client:destroy', { name })
|
|
91
|
+
for (const d of disposables.splice(0)) {
|
|
92
|
+
try {
|
|
93
|
+
d()
|
|
94
|
+
}
|
|
95
|
+
catch { /* 单个回收失败不阻断 */ }
|
|
96
|
+
}
|
|
97
|
+
rootEl?.remove()
|
|
98
|
+
rootEl = null
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// scene 事件桥 → ctx.hooks(event:<type> 与 '*' 均可订阅)
|
|
103
|
+
if (eventBridge) {
|
|
104
|
+
const offBridge = eventBridge((type, payload) => {
|
|
105
|
+
void hooks.emit(`event:${type}`, payload)
|
|
106
|
+
})
|
|
107
|
+
disposables.push(offBridge)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 页面卸载自动回收(pagehide 覆盖 bfcache 场景)
|
|
111
|
+
if (typeof document !== 'undefined') {
|
|
112
|
+
document.addEventListener('visibilitychange', () => {
|
|
113
|
+
if (document.visibilityState === 'hidden') ctx.dispose()
|
|
114
|
+
}, { once: true })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return ctx
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default { CLIENT_SDK_VERSION, createClientContext, el }
|
package/sdk/context.mjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// AgentWorkShop SDK — 服务端插件上下文工厂(宿主调用;插件经 setup(ctx) 获得)
|
|
3
|
+
// ------------------------------------------------------------
|
|
4
|
+
// ctx 形态(运行时完整变量面):
|
|
5
|
+
// 身份 ctx.name / ctx.scope('home'|'project') / ctx.dir / ctx.sdkVersion
|
|
6
|
+
// 钩子 ctx.hooks HookBus(生命周期 + event:*) [SDK]
|
|
7
|
+
// 日志 ctx.logger { debug, info, warn, error } 插件名前缀 [SDK]
|
|
8
|
+
// 配置 ctx.config { get(key), all(), onChange(fn) } [SDK]
|
|
9
|
+
// 存储 ctx.kv { get,set,all,bump } 内存态+防抖落盘 [SDK]
|
|
10
|
+
// 定时 ctx.timer { setInterval, setTimeout } 服务关闭自动回收 [SDK]
|
|
11
|
+
// 清理 ctx.onDispose(fn) / ctx.subscriptions [SDK]
|
|
12
|
+
// 路由 ctx.route(method, path, handler) → /api/plugins/<name>… [SDK]
|
|
13
|
+
// 平台 ctx.api 平台 REST 客户端(lines/daq/dcw/twins/teams…) [SDK]
|
|
14
|
+
// 网络 ctx.http { get, post } 带超时 fetch(仅 http/https) [SDK]
|
|
15
|
+
// 事件 ctx.events { on(type,fn), off } scene 实时事件 [SDK]
|
|
16
|
+
// 路径 ctx.paths { home, configRoot, dataDir } [SDK]
|
|
17
|
+
// ============================================================
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs'
|
|
19
|
+
import { join, resolve } from 'node:path'
|
|
20
|
+
import { HookBus } from './hooks.mjs'
|
|
21
|
+
import { createPlatformClient } from './api.mjs'
|
|
22
|
+
|
|
23
|
+
export const SDK_VERSION = '0.3.0'
|
|
24
|
+
|
|
25
|
+
/** 允许的对外请求协议守卫(拒绝 file:/data: 等;宿主/插件同守此规则) */
|
|
26
|
+
function safeUrl(raw, timeoutMs = 8000) {
|
|
27
|
+
const u = new URL(String(raw))
|
|
28
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:')
|
|
29
|
+
throw new Error(`协议不允许: ${u.protocol}(仅 http/https)`)
|
|
30
|
+
return { signal: AbortSignal.timeout(timeoutMs) }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 创建服务端插件上下文。
|
|
35
|
+
* @param {{ name, scope, dir, hooks, logger, config, paths, emitter, onDispose, selfOrigin }} opts 宿主装配
|
|
36
|
+
*/
|
|
37
|
+
export function createPluginContext(opts) {
|
|
38
|
+
const { name, scope, dir, hooks, config, paths, emitter } = opts
|
|
39
|
+
// 规范化 logger:宿主实现缺级时兜底 no-op(插件可用全套 debug/info/warn/error)
|
|
40
|
+
const logger = {
|
|
41
|
+
debug: () => {},
|
|
42
|
+
info: () => {},
|
|
43
|
+
warn: () => {},
|
|
44
|
+
error: () => {},
|
|
45
|
+
...(opts.logger ?? {}),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---- 订阅回收(VSCode subscriptions 范式):服务关闭时宿主逐个调用 ----
|
|
49
|
+
const disposables = []
|
|
50
|
+
const onDispose = (fn) => {
|
|
51
|
+
if (typeof fn === 'function') disposables.push(fn)
|
|
52
|
+
return fn
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---- 定时器:自动登记回收,杜绝插件定时器泄漏 ----
|
|
56
|
+
const timer = {
|
|
57
|
+
setInterval: (fn, ms, ...rest) => {
|
|
58
|
+
const id = setInterval(fn, ms, ...rest)
|
|
59
|
+
if (typeof id === 'object' && id !== null && 'unref' in id) id.unref?.()
|
|
60
|
+
onDispose(() => clearInterval(id))
|
|
61
|
+
return id
|
|
62
|
+
},
|
|
63
|
+
setTimeout: (fn, ms, ...rest) => {
|
|
64
|
+
const id = setTimeout(fn, ms, ...rest)
|
|
65
|
+
if (typeof id === 'object' && id !== null && 'unref' in id) id.unref?.()
|
|
66
|
+
onDispose(() => clearTimeout(id))
|
|
67
|
+
return id
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---- 插件私有持久化:内存态为准 + 200ms 防抖落盘 —— 高频钩子(daq:sample)
|
|
72
|
+
// 与低频钩子(line:stop)并发时无 read-modify-write 竞态(JS 同步内存操作原子) ----
|
|
73
|
+
const kvDir = join(paths.dataDir, 'plugins', name)
|
|
74
|
+
const kvFile = join(kvDir, 'kv.json')
|
|
75
|
+
const kvState = (() => {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(readFileSync(kvFile, 'utf8'))
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return {}
|
|
81
|
+
}
|
|
82
|
+
})()
|
|
83
|
+
let kvFlushTimer = null
|
|
84
|
+
const kvFlush = () => {
|
|
85
|
+
clearTimeout(kvFlushTimer)
|
|
86
|
+
kvFlushTimer = setTimeout(() => {
|
|
87
|
+
try {
|
|
88
|
+
mkdirSync(kvDir, { recursive: true })
|
|
89
|
+
const tmp = `${kvFile}.${process.pid}.tmp`
|
|
90
|
+
writeFileSync(tmp, `${JSON.stringify(kvState, null, 2)}\n`, 'utf8')
|
|
91
|
+
renameSync(tmp, kvFile)
|
|
92
|
+
}
|
|
93
|
+
catch { /* 磁盘异常不阻断插件 */ }
|
|
94
|
+
}, 200)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const ctx = {
|
|
98
|
+
name,
|
|
99
|
+
scope,
|
|
100
|
+
dir: resolve(dir),
|
|
101
|
+
sdkVersion: SDK_VERSION,
|
|
102
|
+
hooks, // 宿主全局总线(与事件桥同源)
|
|
103
|
+
logger,
|
|
104
|
+
config: {
|
|
105
|
+
get: key => config?.effective?.[key],
|
|
106
|
+
all: () => ({ ...config?.effective }),
|
|
107
|
+
/** 运行时覆盖变更订阅(runtime-settings.json 变化;宿主 fs.watch 驱动) */
|
|
108
|
+
onChange: fn => hooks.on('config:changed', fn),
|
|
109
|
+
},
|
|
110
|
+
paths: { ...paths },
|
|
111
|
+
dataDir: kvDir,
|
|
112
|
+
kv: {
|
|
113
|
+
get: key => kvState[key],
|
|
114
|
+
set: (key, value) => {
|
|
115
|
+
kvState[key] = value
|
|
116
|
+
kvFlush()
|
|
117
|
+
return value
|
|
118
|
+
},
|
|
119
|
+
all: () => ({ ...kvState }),
|
|
120
|
+
bump: (key, by = 1) => {
|
|
121
|
+
kvState[key] = (Number(kvState[key]) || 0) + by
|
|
122
|
+
kvFlush()
|
|
123
|
+
return kvState[key]
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
timer,
|
|
127
|
+
onDispose,
|
|
128
|
+
/** 订阅式清理对象({ dispose(){} })集中登记 */
|
|
129
|
+
subscriptions: {
|
|
130
|
+
add: (d) => {
|
|
131
|
+
disposables.push(typeof d === 'function' ? d : (...a) => d.dispose?.(...a))
|
|
132
|
+
return d
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
route: (method, path, handler) => emitter?.registerRoute(name, method, path, handler),
|
|
136
|
+
/** 平台 REST 客户端(自环 origin 延迟解析;鉴权端点请 ctx.api.setToken(token)) */
|
|
137
|
+
api: createPlatformClient({
|
|
138
|
+
baseUrl: typeof opts.selfOrigin === 'function' ? opts.selfOrigin : () => opts.selfOrigin,
|
|
139
|
+
logger,
|
|
140
|
+
}),
|
|
141
|
+
http: {
|
|
142
|
+
get: (url, opts2 = {}) => fetch(url, { ...safeUrl(url, opts2.timeoutMs), ...opts2 }),
|
|
143
|
+
post: (url, body, opts2 = {}) => fetch(url, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: { 'content-type': 'application/json', ...(opts2.headers ?? {}) },
|
|
146
|
+
body: JSON.stringify(body ?? {}),
|
|
147
|
+
...safeUrl(url, opts2.timeoutMs),
|
|
148
|
+
}),
|
|
149
|
+
},
|
|
150
|
+
events: {
|
|
151
|
+
on: (type, fn) => hooks.on(type === '*' ? '*' : `event:${type}`, fn),
|
|
152
|
+
off: (type, fn) => hooks.off(type === '*' ? '*' : `event:${type}`, fn),
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
return ctx
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 宿主路由表(exact-match;插件 API 挂 /api/plugins/<name><path>) */
|
|
159
|
+
export function createRouteTable() {
|
|
160
|
+
const table = new Map() // `${method} ${name} ${path}` → handler
|
|
161
|
+
return {
|
|
162
|
+
register(name, method, path, handler) {
|
|
163
|
+
if (typeof handler !== 'function') return false
|
|
164
|
+
const key = `${String(method).toUpperCase()} ${name} ${path.startsWith('/') ? path : `/${path}`}`
|
|
165
|
+
table.set(key, handler)
|
|
166
|
+
return true
|
|
167
|
+
},
|
|
168
|
+
resolve(name, method, path) {
|
|
169
|
+
return table.get(`${String(method).toUpperCase()} ${name} ${path.startsWith('/') ? path : `/${path}`}`) ?? null
|
|
170
|
+
},
|
|
171
|
+
byPlugin(name) {
|
|
172
|
+
const out = []
|
|
173
|
+
for (const key of table.keys()) {
|
|
174
|
+
const [m, n, ...rest] = key.split(' ')
|
|
175
|
+
if (n === name) out.push({ method: m, path: rest.join('/') })
|
|
176
|
+
}
|
|
177
|
+
return out
|
|
178
|
+
},
|
|
179
|
+
get size() {
|
|
180
|
+
return table.size
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 校验插件入口导出(宿主装载前的形态检查) */
|
|
186
|
+
export function validatePluginModule(mod, source) {
|
|
187
|
+
const def = mod?.default ?? mod
|
|
188
|
+
if (!def || typeof def !== 'object' || typeof def.name !== 'string' || !def.name.trim()) {
|
|
189
|
+
return { ok: false, error: `插件缺少 name 或不是对象: ${source}` }
|
|
190
|
+
}
|
|
191
|
+
if (def.setup && typeof def.setup !== 'function') {
|
|
192
|
+
return { ok: false, error: `插件 setup 不是函数: ${def.name}` }
|
|
193
|
+
}
|
|
194
|
+
if (def.client && typeof def.client !== 'string') {
|
|
195
|
+
return { ok: false, error: `插件 client 必须是相对路径字符串: ${def.name}` }
|
|
196
|
+
}
|
|
197
|
+
return { ok: true, def }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 便捷:SDK 侧 definePlugin(纯类型糖;宿主同样接受裸对象) */
|
|
201
|
+
export function definePlugin(def) {
|
|
202
|
+
const check = validatePluginModule({ default: def }, def?.name ?? '(anonymous)')
|
|
203
|
+
if (!check.ok) throw new Error(check.error)
|
|
204
|
+
return def
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 插件 KV 目录探测(存在性只读) */
|
|
208
|
+
export function pluginKvExists(dataDir, name) {
|
|
209
|
+
return existsSync(join(resolve(dataDir), 'plugins', name, 'kv.json'))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export default {
|
|
213
|
+
SDK_VERSION,
|
|
214
|
+
HookBus,
|
|
215
|
+
createPluginContext,
|
|
216
|
+
createRouteTable,
|
|
217
|
+
validatePluginModule,
|
|
218
|
+
definePlugin,
|
|
219
|
+
}
|
|
@@ -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.d.mts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// AgentWorkShop SDK 类型声明(agentworkshop/sdk)
|
|
2
|
+
import type { HookBus } from './hooks.mjs'
|
|
3
|
+
|
|
4
|
+
export declare const SDK_VERSION: string
|
|
5
|
+
|
|
6
|
+
/** 生命周期钩子总线:异步串行、错误隔离、'*' 通配、连续失败自动熔断 */
|
|
7
|
+
export declare class HookBus {
|
|
8
|
+
constructor(opts?: { name?: string, onError?: (err: Error, meta?: { bus?: string, type?: string, fails?: number }) => void })
|
|
9
|
+
on(type: string, fn: (payload: any) => any): () => void
|
|
10
|
+
once(type: string, fn: (payload: any) => any): () => void
|
|
11
|
+
off(type: string, fn: (payload: any) => any): void
|
|
12
|
+
emit(type: string, payload?: any): Promise<any>
|
|
13
|
+
readonly size: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PluginLogger {
|
|
17
|
+
debug(...args: unknown[]): void
|
|
18
|
+
info(...args: unknown[]): void
|
|
19
|
+
warn(...args: unknown[]): void
|
|
20
|
+
error(...args: unknown[]): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PluginKv {
|
|
24
|
+
get(key: string): any
|
|
25
|
+
set(key: string, value: any): any
|
|
26
|
+
all(): Record<string, any>
|
|
27
|
+
bump(key: string, by?: number): number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PluginHttp {
|
|
31
|
+
get(url: string, opts?: { timeoutMs?: number, headers?: Record<string, string> }): Promise<Response>
|
|
32
|
+
post(url: string, body?: unknown, opts?: { timeoutMs?: number, headers?: Record<string, string> }): Promise<Response>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 平台 REST 客户端(SDK 作为项目服务 client 的门面) */
|
|
36
|
+
export interface PlatformClient {
|
|
37
|
+
call<T = any>(method: string, path: string, body?: unknown): Promise<T>
|
|
38
|
+
get<T = any>(path: string, query?: Record<string, unknown>): Promise<T>
|
|
39
|
+
post<T = any>(path: string, body?: unknown): Promise<T>
|
|
40
|
+
patch<T = any>(path: string, body?: unknown): Promise<T>
|
|
41
|
+
delete<T = any>(path: string): Promise<T>
|
|
42
|
+
setToken(token: string | null): PlatformClient
|
|
43
|
+
ping(): Promise<any>
|
|
44
|
+
users: { list(q?: any): Promise<any>, get(id: string): Promise<any>, create(b: any): Promise<any>, login(email: string, password: string): Promise<any>, me(): Promise<any> }
|
|
45
|
+
lines: { list(q?: any): Promise<any>, get(id: string): Promise<any>, create(b: any): Promise<any>, update(id: string, p: any): Promise<any>, remove(id: string): Promise<any>, start(id: string, recipeId?: string): Promise<any>, stop(id: string): Promise<any> }
|
|
46
|
+
products: { list(q?: any): Promise<any>, create(b: any): Promise<any> }
|
|
47
|
+
recipes: { list(q?: any): Promise<any>, create(b: any): Promise<any> }
|
|
48
|
+
dcwNodes: { list(q?: any): Promise<any>, create(b: any): Promise<any> }
|
|
49
|
+
daqNodes: { list(q?: any): Promise<any>, create(b: any): Promise<any>, alarms(): Promise<any> }
|
|
50
|
+
templates: { daq(): Promise<any[]>, dcw(): Promise<any[]> }
|
|
51
|
+
twins: { list(q?: any): Promise<any>, create(b: any): Promise<any> }
|
|
52
|
+
teams: { list(q?: any): Promise<any>, create(b: any): Promise<any> }
|
|
53
|
+
agents: { list(q?: any): Promise<any>, create(b: any): Promise<any> }
|
|
54
|
+
channels: { list(q?: any): Promise<any> }
|
|
55
|
+
plugins: { manifest(): Promise<any> }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PluginContext {
|
|
59
|
+
name: string
|
|
60
|
+
scope: 'project' | 'user'
|
|
61
|
+
dir: string
|
|
62
|
+
sdkVersion: string
|
|
63
|
+
hooks: HookBus
|
|
64
|
+
logger: PluginLogger
|
|
65
|
+
config: {
|
|
66
|
+
get(key: string): any
|
|
67
|
+
all(): Record<string, any>
|
|
68
|
+
onChange(fn: (payload?: { at: string }) => any): () => void
|
|
69
|
+
}
|
|
70
|
+
paths: { home: string, configRoot: string, dataDir: string }
|
|
71
|
+
dataDir: string
|
|
72
|
+
kv: PluginKv
|
|
73
|
+
timer: {
|
|
74
|
+
setInterval(fn: (...args: any[]) => void, ms: number, ...rest: any[]): NodeJS.Timeout
|
|
75
|
+
setTimeout(fn: (...args: any[]) => void, ms: number, ...rest: any[]): NodeJS.Timeout
|
|
76
|
+
}
|
|
77
|
+
onDispose(fn: () => any): () => void
|
|
78
|
+
subscriptions: { add(d: { dispose(): any } | (() => any)): any }
|
|
79
|
+
route(method: string, path: string, handler: (event: any) => any): boolean
|
|
80
|
+
api: PlatformClient
|
|
81
|
+
http: PluginHttp
|
|
82
|
+
events: { on(type: string, fn: (payload: any) => any): () => void, off(type: string, fn: (payload: any) => any): void }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface PluginRouteDef {
|
|
86
|
+
method?: string
|
|
87
|
+
path: string
|
|
88
|
+
handler: (event: any) => any
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface PluginDef {
|
|
92
|
+
name: string
|
|
93
|
+
version?: string
|
|
94
|
+
description?: string
|
|
95
|
+
setup?(ctx: PluginContext): void | Promise<void>
|
|
96
|
+
client?: string
|
|
97
|
+
routes?: PluginRouteDef[]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 显式糖:类型化定义插件(宿主同样接受裸对象导出) */
|
|
101
|
+
export declare function definePlugin(def: PluginDef): PluginDef
|
|
102
|
+
|
|
103
|
+
export declare function createPluginContext(opts: Record<string, any>): PluginContext
|
|
104
|
+
export declare function createRouteTable(): {
|
|
105
|
+
register(name: string, method: string, path: string, handler: (event: any) => any): boolean
|
|
106
|
+
resolve(name: string, method: string, path: string): ((event: any) => any) | null
|
|
107
|
+
byPlugin(name: string): Array<{ method: string, path: string }>
|
|
108
|
+
readonly size: number
|
|
109
|
+
}
|
|
110
|
+
export declare function validatePluginModule(mod: any, source: string): { ok: boolean, def?: PluginDef, error?: string }
|
|
111
|
+
export declare function pluginKvExists(dataDir: string, name: string): boolean
|
|
112
|
+
|
|
113
|
+
export declare function createPlatformClient(opts?: { baseUrl?: string, token?: string, logger?: PluginLogger, timeoutMs?: number }): PlatformClient
|
|
114
|
+
|
|
115
|
+
export declare const LIFECYCLE_EVENTS: readonly ['plugin:host:init', 'config:changed', 'event:*', 'daq:sample', 'dcw:write', 'line:start', 'line:stop', 'server:close']
|
|
116
|
+
export declare const CLIENT_EVENTS: readonly ['client:init', 'event:*', 'page:change', 'client:destroy']
|
|
117
|
+
|
|
118
|
+
/** 客户端插件上下文(sdk/client.mjs) */
|
|
119
|
+
export declare const CLIENT_SDK_VERSION: string
|
|
120
|
+
export interface ClientContext {
|
|
121
|
+
name: string
|
|
122
|
+
sdkVersion: string
|
|
123
|
+
hooks: HookBus
|
|
124
|
+
on(type: string, fn: (payload: any) => any): () => void
|
|
125
|
+
fetch<T = any>(path: string, opt?: { method?: string, body?: unknown, headers?: Record<string, string> }): Promise<T>
|
|
126
|
+
el(tag: string, attrs?: Record<string, unknown>, children?: Array<Node | string>): HTMLElement
|
|
127
|
+
mount(target: string | Element, node: Node): Node
|
|
128
|
+
root(): HTMLElement
|
|
129
|
+
log: PluginLogger
|
|
130
|
+
dispose(): void
|
|
131
|
+
}
|
|
132
|
+
export declare function createClientContext(opts: { name: string, eventBridge?: (fn: (type: string, payload: any) => void) => (() => void), baseUrl?: string }): ClientContext
|
|
133
|
+
|
|
134
|
+
declare const _default: Record<string, unknown>
|
|
135
|
+
export default _default
|
package/sdk/index.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
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 { createPlatformClient } from './api.mjs'
|
|
19
|
+
import { CLIENT_SDK_VERSION, createClientContext } from './client.mjs'
|
|
20
|
+
import { LIFECYCLE_EVENTS, CLIENT_EVENTS } from './lifecycle.mjs'
|
|
21
|
+
|
|
22
|
+
export { SDK_VERSION, definePlugin, createPluginContext, createRouteTable, validatePluginModule, pluginKvExists } from './context.mjs'
|
|
23
|
+
export { HookBus } from './hooks.mjs'
|
|
24
|
+
export { createPlatformClient } from './api.mjs'
|
|
25
|
+
export { CLIENT_SDK_VERSION, createClientContext } from './client.mjs'
|
|
26
|
+
export { LIFECYCLE_EVENTS, CLIENT_EVENTS } from './lifecycle.mjs'
|
|
27
|
+
|
|
28
|
+
export default {
|
|
29
|
+
SDK_VERSION,
|
|
30
|
+
definePlugin,
|
|
31
|
+
HookBus,
|
|
32
|
+
createPluginContext,
|
|
33
|
+
createRouteTable,
|
|
34
|
+
validatePluginModule,
|
|
35
|
+
pluginKvExists,
|
|
36
|
+
createPlatformClient,
|
|
37
|
+
CLIENT_SDK_VERSION,
|
|
38
|
+
createClientContext,
|
|
39
|
+
LIFECYCLE_EVENTS,
|
|
40
|
+
CLIENT_EVENTS,
|
|
41
|
+
}
|