agentworkshop 0.2.2 → 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/package.json +16 -1
- package/scripts/dev-guard.mjs +13 -0
- package/sdk/api.mjs +101 -0
- package/sdk/client.mjs +52 -8
- package/sdk/context.mjs +56 -15
- 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/data/daqs.json +237 -61
- package/server/data/dcw-lines.json +56 -0
- package/server/data/dcw-products.json +42 -0
- package/server/data/dcw-recipes.json +120 -0
- package/server/data/dcw-rollback.json +1 -1
- package/server/data/dcw-runs.json +150 -108
- package/server/data/dcws.json +126 -0
- package/server/data/line-runs.json +75 -15
- package/server/plugins/aw-plugins.ts +7 -1
- package/server/services/workshop/plugins/host.mjs +69 -3
package/package.json
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentworkshop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgentWorkShop 软件开发系统 - 基于 Nuxt 4 的配置驱动运行范式",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./sdk/index.d.mts",
|
|
9
|
+
"default": "./sdk/index.mjs"
|
|
10
|
+
},
|
|
11
|
+
"./sdk": {
|
|
12
|
+
"types": "./sdk/index.d.mts",
|
|
13
|
+
"default": "./sdk/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"./sdk/client": {
|
|
16
|
+
"types": "./sdk/client.d.mts",
|
|
17
|
+
"default": "./sdk/client.mjs"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
6
21
|
"bin": {
|
|
7
22
|
"aw": "bin/aw.mjs",
|
|
8
23
|
"agentworkshop": "bin/aw.mjs"
|
package/scripts/dev-guard.mjs
CHANGED
|
@@ -60,6 +60,19 @@ const devHost = String(eff.effective['server.host'] ?? '0.0.0.0')
|
|
|
60
60
|
const portSource = eff.sources['server.dev.port']
|
|
61
61
|
console.log(`[config] 开发端口 -> ${devPort} (source: ${portSource}${hasPort ? ', CLI 显式覆盖生效' : ''})`)
|
|
62
62
|
|
|
63
|
+
// 端口注入 worker 环境链(PORT):CLI 显式值优先,否则引擎有效值 —— 与实际监听端口一致。
|
|
64
|
+
// 插件宿主(经 PORT env)自环调用 ctx.api 依赖此值;注意 nuxt dev 会自行覆盖 PORT,
|
|
65
|
+
// 因此必须在重入之前于顶层 guard spawn 时就位 —— 见文件头部 guard 块的 env 透传。
|
|
66
|
+
const cliPort = (() => {
|
|
67
|
+
const i = rest.indexOf('--port')
|
|
68
|
+
if (i >= 0 && rest[i + 1]) return rest[i + 1]
|
|
69
|
+
const eq = rest.find(a => a.startsWith('--port='))
|
|
70
|
+
return eq ? eq.slice(7) : null
|
|
71
|
+
})()
|
|
72
|
+
const effectivePort = cliPort ?? devPort
|
|
73
|
+
process.env.PORT = String(effectivePort)
|
|
74
|
+
console.log(`[config] worker PORT env -> ${effectivePort}`)
|
|
75
|
+
|
|
63
76
|
const inject = [...rest]
|
|
64
77
|
if (!hasPort) inject.push('--port', String(devPort))
|
|
65
78
|
if (!hasHost) inject.push('--host', devHost)
|
package/sdk/api.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// AgentWorkShop SDK — 平台 REST 客户端
|
|
3
|
+
// ------------------------------------------------------------
|
|
4
|
+
// SDK 作为项目服务的 client:对平台 REST 面(/api/**)的类型化轻封装。
|
|
5
|
+
// 三个使用形态:
|
|
6
|
+
// 1) 插件内: ctx.api.lines.list() —— 宿主注入(自环 origin)
|
|
7
|
+
// 2) 外部脚本: createPlatformClient({ baseUrl, token }).daq.nodes()
|
|
8
|
+
// 3) 浏览器: createPlatformClient({ baseUrl: '' }) 同源相对路径
|
|
9
|
+
// 资源方法返回原始 JSON body(data 字段已解包);4xx/5xx 抛错(含 status)。
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
export function createPlatformClient({ baseUrl = '', token, logger, timeoutMs = 10000 } = {}) {
|
|
13
|
+
// baseUrl 支持函数(延迟解析):插件宿主的监听端口在 nitro listen 钩子后才确定
|
|
14
|
+
const resolveBase = () => String(typeof baseUrl === 'function' ? baseUrl() : (baseUrl ?? '')).replace(/\/+$/, '')
|
|
15
|
+
let authToken = token ?? null
|
|
16
|
+
|
|
17
|
+
async function call(method, path, body, opt = {}) {
|
|
18
|
+
const base = resolveBase()
|
|
19
|
+
const headers = { accept: 'application/json', ...(opt.headers ?? {}) }
|
|
20
|
+
if (authToken) headers.authorization = `Bearer ${authToken}`
|
|
21
|
+
if (body !== undefined) headers['content-type'] = 'application/json'
|
|
22
|
+
const res = await fetch(`${base}${path}`, {
|
|
23
|
+
method,
|
|
24
|
+
headers,
|
|
25
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
26
|
+
signal: AbortSignal.timeout(opt.timeoutMs ?? timeoutMs),
|
|
27
|
+
})
|
|
28
|
+
const json = await res.json().catch(() => null)
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
const err = new Error(json?.message ?? `HTTP ${res.status} ${path}`)
|
|
31
|
+
err.status = res.status
|
|
32
|
+
err.body = json
|
|
33
|
+
if (logger) logger.warn(`API ${method} ${path} → ${res.status}`)
|
|
34
|
+
throw err
|
|
35
|
+
}
|
|
36
|
+
// 平台统一信封 { code, message, data } → 解包 data;非信封原样返回
|
|
37
|
+
return json && typeof json === 'object' && 'data' in json ? json.data : json
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const resource = root => ({
|
|
41
|
+
list: (query = {}) => call('GET', `${root}${toQuery(query)}`),
|
|
42
|
+
get: id => call('GET', `${root}/${id}`),
|
|
43
|
+
create: body => call('POST', root, body),
|
|
44
|
+
update: (id, patch) => call('PATCH', `${root}/${id}`, patch),
|
|
45
|
+
remove: id => call('DELETE', `${root}/${id}`),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const client = {
|
|
49
|
+
/** 底层调用(任意平台路径;自动带 token 与信封解包) */
|
|
50
|
+
call,
|
|
51
|
+
get: (path, query = {}) => call('GET', `${path}${toQuery(query)}`),
|
|
52
|
+
post: (path, body) => call('POST', path, body),
|
|
53
|
+
patch: (path, body) => call('PATCH', path, body),
|
|
54
|
+
delete: path => call('DELETE', path),
|
|
55
|
+
setToken: (t) => {
|
|
56
|
+
authToken = t ?? null
|
|
57
|
+
return client
|
|
58
|
+
},
|
|
59
|
+
/** 平台健康(免鉴权) */
|
|
60
|
+
ping: () => call('GET', '/api/plugins/manifest'),
|
|
61
|
+
|
|
62
|
+
// ---- 业务资源面 ----
|
|
63
|
+
users: {
|
|
64
|
+
...resource('/api/users'),
|
|
65
|
+
login: (email, password) => call('POST', '/api/users/login', { email, password }),
|
|
66
|
+
me: () => call('GET', '/api/users/me'),
|
|
67
|
+
},
|
|
68
|
+
lines: {
|
|
69
|
+
...resource('/api/workshop/dcw/lines'),
|
|
70
|
+
start: (id, recipeId = '') => call('POST', `/api/workshop/dcw/lines/${id}/start`, { recipeId }),
|
|
71
|
+
stop: id => call('POST', `/api/workshop/dcw/lines/${id}/stop`),
|
|
72
|
+
},
|
|
73
|
+
products: resource('/api/workshop/dcw/products'),
|
|
74
|
+
recipes: resource('/api/workshop/dcw/recipes'),
|
|
75
|
+
dcwNodes: resource('/api/workshop/dcw'),
|
|
76
|
+
daqNodes: {
|
|
77
|
+
...resource('/api/workshop/daq'),
|
|
78
|
+
alarms: () => call('GET', '/api/workshop/daq/alarms'),
|
|
79
|
+
},
|
|
80
|
+
templates: {
|
|
81
|
+
daq: () => call('GET', '/api/workshop/daq').then(d => d?.templates ?? []),
|
|
82
|
+
dcw: () => call('GET', '/api/workshop/dcw').then(d => d?.templates ?? []),
|
|
83
|
+
},
|
|
84
|
+
twins: resource('/api/workshop/device-twins'),
|
|
85
|
+
teams: resource('/api/workshop/teams'),
|
|
86
|
+
agents: resource('/api/workshop/agents'),
|
|
87
|
+
channels: resource('/api/workshop/channels'),
|
|
88
|
+
plugins: {
|
|
89
|
+
manifest: () => call('GET', '/api/plugins/manifest'),
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
return client
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toQuery(query) {
|
|
96
|
+
const entries = Object.entries(query ?? {}).filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
97
|
+
if (!entries.length) return ''
|
|
98
|
+
return `?${new URLSearchParams(Object.fromEntries(entries.map(([k, v]) => [k, String(v)]))).toString()}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default createPlatformClient
|
package/sdk/client.mjs
CHANGED
|
@@ -5,16 +5,18 @@
|
|
|
5
5
|
// export function setup(ctx) { ctx.on('daq.reading', d => { ... }) }
|
|
6
6
|
// ctx 形态(浏览器侧,自包含、零框架依赖):
|
|
7
7
|
// ctx.name / ctx.sdkVersion
|
|
8
|
-
// ctx.hooks 客户端本地 HookBus(client:init / event:* / page:change)
|
|
9
|
-
// ctx.on(type, fn) scene 实时事件订阅(与 WS 同源;'*' 通配)
|
|
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)
|
|
10
11
|
// ctx.el(tag, attrs, children) DOM 助手(挂到任意面板/宿主节点)
|
|
11
12
|
// ctx.mount(selector|el, node) 挂载节点(缺失时挂 body 角落)
|
|
12
|
-
// ctx.log 前缀 console
|
|
13
13
|
// ctx.root 插件 UI 挂载点(懒创建,自动附加到 body,#aw-plugin-<name>)
|
|
14
|
+
// ctx.log 前缀 console
|
|
15
|
+
// ctx.dispose() 卸载:清空挂载点 + 回收订阅 + 广播 client:destroy(pagehide 自动触发)
|
|
14
16
|
// ============================================================
|
|
15
17
|
import { HookBus } from './hooks.mjs'
|
|
16
18
|
|
|
17
|
-
export const CLIENT_SDK_VERSION = '0.
|
|
19
|
+
export const CLIENT_SDK_VERSION = '0.3.0'
|
|
18
20
|
|
|
19
21
|
function el(tag, attrs = {}, children = []) {
|
|
20
22
|
const node = document.createElement(tag)
|
|
@@ -30,16 +32,36 @@ function el(tag, attrs = {}, children = []) {
|
|
|
30
32
|
return node
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
export function createClientContext({ name, eventBridge }) {
|
|
35
|
+
export function createClientContext({ name, eventBridge, baseUrl = '' }) {
|
|
34
36
|
const hooks = new HookBus({ name: `client:${name}`, onError: err => console.warn(`[aw-plugin:${name}]`, err) })
|
|
37
|
+
const disposables = []
|
|
35
38
|
let rootEl = null
|
|
39
|
+
let disposed = false
|
|
36
40
|
|
|
37
41
|
const ctx = {
|
|
38
42
|
name,
|
|
39
43
|
sdkVersion: CLIENT_SDK_VERSION,
|
|
40
44
|
hooks,
|
|
41
|
-
on: (type, fn) =>
|
|
42
|
-
|
|
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
|
+
},
|
|
43
65
|
el,
|
|
44
66
|
log: {
|
|
45
67
|
info: (...a) => console.info(`[aw-plugin:${name}]`, ...a),
|
|
@@ -61,13 +83,35 @@ export function createClientContext({ name, eventBridge }) {
|
|
|
61
83
|
;(host ?? ctx.root()).append(node)
|
|
62
84
|
return node
|
|
63
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
|
+
},
|
|
64
100
|
}
|
|
65
101
|
|
|
66
102
|
// scene 事件桥 → ctx.hooks(event:<type> 与 '*' 均可订阅)
|
|
67
103
|
if (eventBridge) {
|
|
68
|
-
eventBridge((type, payload) => {
|
|
104
|
+
const offBridge = eventBridge((type, payload) => {
|
|
69
105
|
void hooks.emit(`event:${type}`, payload)
|
|
70
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 })
|
|
71
115
|
}
|
|
72
116
|
|
|
73
117
|
return ctx
|
package/sdk/context.mjs
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
// ============================================================
|
|
2
2
|
// AgentWorkShop SDK — 服务端插件上下文工厂(宿主调用;插件经 setup(ctx) 获得)
|
|
3
3
|
// ------------------------------------------------------------
|
|
4
|
-
// ctx
|
|
5
|
-
// ctx.name / ctx.scope('home'|'project') / ctx.dir
|
|
6
|
-
// ctx.hooks
|
|
7
|
-
// ctx.logger
|
|
8
|
-
// ctx.config
|
|
9
|
-
// ctx.
|
|
10
|
-
// ctx.
|
|
11
|
-
// ctx.
|
|
12
|
-
// ctx.
|
|
13
|
-
// ctx.
|
|
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]
|
|
14
17
|
// ============================================================
|
|
15
18
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs'
|
|
16
|
-
import {
|
|
19
|
+
import { join, resolve } from 'node:path'
|
|
17
20
|
import { HookBus } from './hooks.mjs'
|
|
21
|
+
import { createPlatformClient } from './api.mjs'
|
|
18
22
|
|
|
19
|
-
export const SDK_VERSION = '0.
|
|
23
|
+
export const SDK_VERSION = '0.3.0'
|
|
20
24
|
|
|
21
25
|
/** 允许的对外请求协议守卫(拒绝 file:/data: 等;宿主/插件同守此规则) */
|
|
22
26
|
function safeUrl(raw, timeoutMs = 8000) {
|
|
@@ -28,7 +32,7 @@ function safeUrl(raw, timeoutMs = 8000) {
|
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* 创建服务端插件上下文。
|
|
31
|
-
* @param {{ name, scope, dir, hooks, logger, config, paths, emitter }} opts 宿主装配
|
|
35
|
+
* @param {{ name, scope, dir, hooks, logger, config, paths, emitter, onDispose, selfOrigin }} opts 宿主装配
|
|
32
36
|
*/
|
|
33
37
|
export function createPluginContext(opts) {
|
|
34
38
|
const { name, scope, dir, hooks, config, paths, emitter } = opts
|
|
@@ -41,6 +45,29 @@ export function createPluginContext(opts) {
|
|
|
41
45
|
...(opts.logger ?? {}),
|
|
42
46
|
}
|
|
43
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
|
+
|
|
44
71
|
// ---- 插件私有持久化:内存态为准 + 200ms 防抖落盘 —— 高频钩子(daq:sample)
|
|
45
72
|
// 与低频钩子(line:stop)并发时无 read-modify-write 竞态(JS 同步内存操作原子) ----
|
|
46
73
|
const kvDir = join(paths.dataDir, 'plugins', name)
|
|
@@ -77,6 +104,8 @@ export function createPluginContext(opts) {
|
|
|
77
104
|
config: {
|
|
78
105
|
get: key => config?.effective?.[key],
|
|
79
106
|
all: () => ({ ...config?.effective }),
|
|
107
|
+
/** 运行时覆盖变更订阅(runtime-settings.json 变化;宿主 fs.watch 驱动) */
|
|
108
|
+
onChange: fn => hooks.on('config:changed', fn),
|
|
80
109
|
},
|
|
81
110
|
paths: { ...paths },
|
|
82
111
|
dataDir: kvDir,
|
|
@@ -94,7 +123,21 @@ export function createPluginContext(opts) {
|
|
|
94
123
|
return kvState[key]
|
|
95
124
|
},
|
|
96
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
|
+
},
|
|
97
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
|
+
}),
|
|
98
141
|
http: {
|
|
99
142
|
get: (url, opts2 = {}) => fetch(url, { ...safeUrl(url, opts2.timeoutMs), ...opts2 }),
|
|
100
143
|
post: (url, body, opts2 = {}) => fetch(url, {
|
|
@@ -166,8 +209,6 @@ export function pluginKvExists(dataDir, name) {
|
|
|
166
209
|
return existsSync(join(resolve(dataDir), 'plugins', name, 'kv.json'))
|
|
167
210
|
}
|
|
168
211
|
|
|
169
|
-
export { dirname }
|
|
170
|
-
|
|
171
212
|
export default {
|
|
172
213
|
SDK_VERSION,
|
|
173
214
|
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
CHANGED
|
@@ -15,29 +15,15 @@
|
|
|
15
15
|
// ============================================================
|
|
16
16
|
import { SDK_VERSION, definePlugin, createPluginContext, createRouteTable, validatePluginModule, pluginKvExists } from './context.mjs'
|
|
17
17
|
import { HookBus } from './hooks.mjs'
|
|
18
|
+
import { createPlatformClient } from './api.mjs'
|
|
18
19
|
import { CLIENT_SDK_VERSION, createClientContext } from './client.mjs'
|
|
20
|
+
import { LIFECYCLE_EVENTS, CLIENT_EVENTS } from './lifecycle.mjs'
|
|
19
21
|
|
|
20
22
|
export { SDK_VERSION, definePlugin, createPluginContext, createRouteTable, validatePluginModule, pluginKvExists } from './context.mjs'
|
|
21
23
|
export { HookBus } from './hooks.mjs'
|
|
24
|
+
export { createPlatformClient } from './api.mjs'
|
|
22
25
|
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
|
-
])
|
|
26
|
+
export { LIFECYCLE_EVENTS, CLIENT_EVENTS } from './lifecycle.mjs'
|
|
41
27
|
|
|
42
28
|
export default {
|
|
43
29
|
SDK_VERSION,
|
|
@@ -47,6 +33,7 @@ export default {
|
|
|
47
33
|
createRouteTable,
|
|
48
34
|
validatePluginModule,
|
|
49
35
|
pluginKvExists,
|
|
36
|
+
createPlatformClient,
|
|
50
37
|
CLIENT_SDK_VERSION,
|
|
51
38
|
createClientContext,
|
|
52
39
|
LIFECYCLE_EVENTS,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// AgentWorkShop SDK — 生命周期事件清单(宿主触发;单一事实源)
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
/** 服务端生命周期事件清单(宿主触发;文档见 docs/plugins.md) */
|
|
6
|
+
export const LIFECYCLE_EVENTS = Object.freeze([
|
|
7
|
+
'plugin:host:init',
|
|
8
|
+
'config:changed',
|
|
9
|
+
'event:*',
|
|
10
|
+
'daq:sample',
|
|
11
|
+
'dcw:write',
|
|
12
|
+
'line:start',
|
|
13
|
+
'line:stop',
|
|
14
|
+
'server:close',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
/** 客户端生命周期事件清单 */
|
|
18
|
+
export const CLIENT_EVENTS = Object.freeze([
|
|
19
|
+
'client:init',
|
|
20
|
+
'event:*',
|
|
21
|
+
'page:change',
|
|
22
|
+
'client:destroy',
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
export default { LIFECYCLE_EVENTS, CLIENT_EVENTS }
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* 插件经 ctx.route(method, path, handler) 注册;handler(event) 返回值由 nitro 序列化。
|
|
4
4
|
* 鉴权由插件自行处理(v1 不强制;可经 resolveUser 复用业务鉴权)。
|
|
5
5
|
*/
|
|
6
|
-
import { defineEventHandler, createError } from 'h3'
|
|
6
|
+
import { defineEventHandler, createError, readBody } from 'h3'
|
|
7
7
|
import { getPluginHost } from '@/server/services/workshop/plugins/host.mjs'
|
|
8
8
|
|
|
9
|
-
export default defineEventHandler((event) => {
|
|
9
|
+
export default defineEventHandler(async (event) => {
|
|
10
10
|
const host = getPluginHost()
|
|
11
11
|
if (!host) throw createError({ statusCode: 503, statusMessage: 'plugin host not ready' })
|
|
12
12
|
const name = String(event.context.params?.name ?? '')
|
|
@@ -15,5 +15,8 @@ export default defineEventHandler((event) => {
|
|
|
15
15
|
if (!handler) {
|
|
16
16
|
throw createError({ statusCode: 404, statusMessage: `plugin route not found: ${event.method} /api/plugins/${name}${path}` })
|
|
17
17
|
}
|
|
18
|
+
// 预读 body 挂到 event(插件 handler 无 h3 导入能力,经 event.awBody 消费)
|
|
19
|
+
const awBody = await readBody(event).catch(() => undefined)
|
|
20
|
+
;(event as Record<string, unknown>).awBody = awBody
|
|
18
21
|
return handler(event)
|
|
19
22
|
})
|