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.
Files changed (34) hide show
  1. package/app/plugins/aw-plugins.client.ts +79 -0
  2. package/cli/commands/plugin.mjs +169 -0
  3. package/cli/commands/update.mjs +12 -4
  4. package/docs/plugins.md +127 -0
  5. package/package.json +19 -2
  6. package/scripts/dev-guard.mjs +13 -0
  7. package/scripts/home-bootstrap.mjs +25 -1
  8. package/sdk/api.mjs +101 -0
  9. package/sdk/client.mjs +120 -0
  10. package/sdk/context.mjs +219 -0
  11. package/sdk/examples/sample-insight/client.mjs +23 -0
  12. package/sdk/examples/sample-insight/index.mjs +39 -0
  13. package/sdk/hooks.mjs +103 -0
  14. package/sdk/index.d.mts +135 -0
  15. package/sdk/index.mjs +41 -0
  16. package/sdk/lifecycle.mjs +25 -0
  17. package/server/api/plugins/[name]/[...path].ts +22 -0
  18. package/server/api/plugins/client/[name].get.ts +18 -0
  19. package/server/api/plugins/manifest.get.ts +10 -0
  20. package/server/api/workshop/dcw/lines/[id]/start.post.ts +2 -0
  21. package/server/api/workshop/dcw/lines/[id]/stop.post.ts +2 -0
  22. package/server/data/daqs.json +347 -61
  23. package/server/data/dcw-lines.json +91 -0
  24. package/server/data/dcw-products.json +77 -0
  25. package/server/data/dcw-recipes.json +220 -0
  26. package/server/data/dcw-rollback.json +1 -1
  27. package/server/data/dcw-runs.json +275 -198
  28. package/server/data/dcws.json +231 -0
  29. package/server/data/line-runs.json +74 -14
  30. package/server/plugins/aw-plugins.ts +24 -0
  31. package/server/services/workshop/daq/daq-controller.ts +6 -2
  32. package/server/services/workshop/dcw/dcw-controller.ts +3 -0
  33. package/server/services/workshop/plugins/host.mjs +289 -0
  34. 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 }
@@ -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
@@ -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
+ }