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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aw 插件客户端装载器 —— 前端增强入口。
|
|
3
|
+
* - 启动期拉取 /api/plugins/manifest → 对含 client 的插件动态 import 脚本
|
|
4
|
+
* - 每个插件获得独立 ctx(sdk/client.mjs):事件订阅/Hooks/DOM 助手/私有挂载点
|
|
5
|
+
* - 事件桥:useTownBus(AEP 信封,与 WS 同源) → ctx.hooks(event:<type> / '*')
|
|
6
|
+
* - 错误隔离:单插件装载失败仅告警,不影响应用与其他插件
|
|
7
|
+
* 插件契约见 docs/plugins.md;信任模型与 aw commands 相同(仅装可信代码)。
|
|
8
|
+
*/
|
|
9
|
+
import { createClientContext } from '@/sdk/client.mjs'
|
|
10
|
+
import type { TownBus } from '~/composables/workshop/useTownBus'
|
|
11
|
+
|
|
12
|
+
export default defineNuxtPlugin(async (nuxtApp) => {
|
|
13
|
+
if (!import.meta.client) return
|
|
14
|
+
|
|
15
|
+
const loaded: Array<{ name: string, ctx: ReturnType<typeof createClientContext> }> = []
|
|
16
|
+
|
|
17
|
+
const bridgeFactory = (bus: TownBus | null) => (fn: (type: string, payload: unknown) => void) => {
|
|
18
|
+
if (!bus) return () => {}
|
|
19
|
+
return bus.subscribe((e) => {
|
|
20
|
+
try {
|
|
21
|
+
fn(e.type, e.payload)
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.warn(`[aw-plugins] 事件分发异常:`, err)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch('/api/plugins/manifest', { headers: { accept: 'application/json' } })
|
|
31
|
+
if (!res.ok) return
|
|
32
|
+
const body = await res.json().catch(() => null) as { plugins?: Array<{ name: string, hasClient?: boolean }> } | null
|
|
33
|
+
const plugins = (body?.plugins ?? []).filter(p => p.hasClient)
|
|
34
|
+
if (!plugins.length) return
|
|
35
|
+
|
|
36
|
+
let bus: TownBus | null = null
|
|
37
|
+
try {
|
|
38
|
+
bus = useTownBus()
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
bus = null // WS 总线不可用(离线)时插件仍可装载,只是无事件流
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const p of plugins) {
|
|
45
|
+
try {
|
|
46
|
+
const mod = await import(/* @vite-ignore */ `/api/plugins/client/${encodeURIComponent(p.name)}`)
|
|
47
|
+
const setup = (mod as { setup?: unknown }).setup ?? (mod as { default?: { setup?: unknown } }).default?.setup
|
|
48
|
+
if (typeof setup !== 'function') {
|
|
49
|
+
console.warn(`[aw-plugins] ${p.name} 客户端入口缺少 setup(ctx)`)
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
const ctx = createClientContext({
|
|
53
|
+
name: p.name,
|
|
54
|
+
eventBridge: bridgeFactory(bus),
|
|
55
|
+
})
|
|
56
|
+
await (setup as (ctx: unknown) => void | Promise<void>)(ctx)
|
|
57
|
+
void ctx.hooks.emit('client:init', { name: p.name })
|
|
58
|
+
loaded.push({ name: p.name, ctx })
|
|
59
|
+
console.info(`[aw-plugins] ✔ 客户端插件已装载: ${p.name}`)
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.warn(`[aw-plugins] 客户端插件装载失败 ${p.name}:`, err)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 页面切换广播(page:change)
|
|
67
|
+
if (loaded.length) {
|
|
68
|
+
nuxtApp.hooks.hook('page:finish', () => {
|
|
69
|
+
const route = useRoute()
|
|
70
|
+
for (const { ctx } of loaded) {
|
|
71
|
+
void ctx.hooks.emit('page:change', { path: route.path })
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// 网络不可达/服务未就绪:静默(插件是增强层,绝不阻断应用)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 指令:plugin — 插件管理(list / create 脚手架)
|
|
3
|
+
// ------------------------------------------------------------
|
|
4
|
+
// 插件目录(与配置根一致):
|
|
5
|
+
// project: <repo>/.AgentWorkShop/plugins/<name>/
|
|
6
|
+
// user: ~/.AgentWorkShop/plugins/<name>/
|
|
7
|
+
// 插件契约:入口 index.mjs 导出 { name, version?, description?,
|
|
8
|
+
// setup(ctx)?, client?: './client.mjs', routes?: [...] } —— 零导入依赖。
|
|
9
|
+
// ============================================================
|
|
10
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import { color } from '../core/logger.mjs'
|
|
13
|
+
import { CliError } from '../core/errors.mjs'
|
|
14
|
+
|
|
15
|
+
export const meta = {
|
|
16
|
+
name: 'plugin',
|
|
17
|
+
aliases: ['plugins', 'plug'],
|
|
18
|
+
group: '扩展',
|
|
19
|
+
summary: '插件管理:查看已装插件 / 脚手架新插件',
|
|
20
|
+
usage: 'aw plugin <list|create> [name] [--project|--global] [--force]',
|
|
21
|
+
description: [
|
|
22
|
+
'插件 = 配置根 plugins/<name>/ 下的 node 项目:入口 index.mjs 导出',
|
|
23
|
+
'{ name, setup(ctx) } 即自动装载(服务端钩子/API 路由),client.mjs 可选(浏览器增强)。',
|
|
24
|
+
'服务端钩子:daq:sample / dcw:write / line:start|stop / event:*(scene 全事件)/ server:close。',
|
|
25
|
+
],
|
|
26
|
+
needsProject: false,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ENTRY_TEMPLATE = name => `/**
|
|
30
|
+
* ${name} — AgentWorkShop 插件(入口)。
|
|
31
|
+
* setup(ctx) 服务端生命周期:ctx.hooks / ctx.config / ctx.kv / ctx.route / ctx.logger / ctx.http / ctx.events
|
|
32
|
+
* 完整契约见 docs/plugins.md。
|
|
33
|
+
*/
|
|
34
|
+
export default {
|
|
35
|
+
name: '${name}',
|
|
36
|
+
version: '0.1.0',
|
|
37
|
+
description: '${name} 插件',
|
|
38
|
+
client: './client.mjs',
|
|
39
|
+
|
|
40
|
+
async setup(ctx) {
|
|
41
|
+
ctx.logger.info('已装载 ✓')
|
|
42
|
+
|
|
43
|
+
// 示例:订阅数采采样(下发级),计数落插件私有 KV
|
|
44
|
+
ctx.hooks.on('daq:sample', (s) => {
|
|
45
|
+
ctx.kv.bump('samples')
|
|
46
|
+
void s
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// 示例:产线生命周期
|
|
50
|
+
ctx.hooks.on('line:start', (p) => {
|
|
51
|
+
ctx.kv.set('lineRunning', true)
|
|
52
|
+
ctx.logger.info('产线开跑', p?.lineId)
|
|
53
|
+
})
|
|
54
|
+
ctx.hooks.on('line:stop', () => ctx.kv.set('lineRunning', false))
|
|
55
|
+
|
|
56
|
+
// 示例:注册插件 API → GET /api/plugins/${name}/stats
|
|
57
|
+
ctx.route('GET', '/stats', () => ({
|
|
58
|
+
plugin: ctx.name,
|
|
59
|
+
samples: ctx.kv.get('samples') ?? 0,
|
|
60
|
+
lineRunning: ctx.kv.get('lineRunning') ?? false,
|
|
61
|
+
at: new Date().toISOString(),
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
// 示例:scene 实时事件订阅(与 WS 同源)
|
|
65
|
+
ctx.events.on('daq.node.changed', (p) => {
|
|
66
|
+
ctx.logger.debug('node changed', p)
|
|
67
|
+
})
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
`
|
|
71
|
+
|
|
72
|
+
const CLIENT_TEMPLATE = name => `// ${name} — 客户端增强(可选;自包含 ESM,无裸导入)
|
|
73
|
+
// ctx: on(type,fn) 事件订阅 / el() DOM 助手 / root() 私有挂载点 / log
|
|
74
|
+
export function setup(ctx) {
|
|
75
|
+
const badge = ctx.el('div', {
|
|
76
|
+
style: 'padding:6px 10px;border:1px solid #2de0a0;border-radius:8px;color:#2de0a0;background:rgba(0,0,0,.55)',
|
|
77
|
+
}, [\`\${ctx.name} · 0\`])
|
|
78
|
+
ctx.root().append(badge)
|
|
79
|
+
|
|
80
|
+
let n = 0
|
|
81
|
+
ctx.on('daq:sample', () => {
|
|
82
|
+
n++
|
|
83
|
+
badge.textContent = \`\${ctx.name} · \${n} 样本\`
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
ctx.log.info('client ready')
|
|
87
|
+
}
|
|
88
|
+
`
|
|
89
|
+
|
|
90
|
+
const PLUGIN_README = name => `# ${name}
|
|
91
|
+
|
|
92
|
+
AgentWorkShop 插件。重启 \`aw start\` / \`aw dev\` 自动装载。
|
|
93
|
+
|
|
94
|
+
- \`index.mjs\` — 服务端入口(setup(ctx))
|
|
95
|
+
- \`client.mjs\` — 浏览器增强(可选)
|
|
96
|
+
- API:GET /api/plugins/${name}/stats
|
|
97
|
+
`
|
|
98
|
+
|
|
99
|
+
export async function run(argv, ctx) {
|
|
100
|
+
const { flags, positionals } = argv
|
|
101
|
+
const sub = positionals[0] ?? 'list'
|
|
102
|
+
|
|
103
|
+
if (sub === 'list' || sub === 'ls')
|
|
104
|
+
return list(ctx)
|
|
105
|
+
|
|
106
|
+
if (sub === 'create' || sub === 'new' || sub === 'add')
|
|
107
|
+
return create(ctx, positionals[1], flags)
|
|
108
|
+
|
|
109
|
+
throw new CliError('USAGE', '用法: aw plugin <list|create> [name] [--project|--global]')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function scopes(ctx) {
|
|
113
|
+
return [
|
|
114
|
+
{ scope: 'project', dir: ctx.root ? join(ctx.root, '.AgentWorkShop', 'plugins') : null, label: '项目级' },
|
|
115
|
+
{ scope: 'user', dir: ctx.commandsDir?.global ? join(ctx.home, 'plugins') : join(ctx.home, 'plugins'), label: '用户级' },
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function list(ctx) {
|
|
120
|
+
console.log('')
|
|
121
|
+
console.log(`${color.bold('AgentWorkShop 插件')} ${color.dim('— 配置根 plugins/ 目录,重启自动装载')}`)
|
|
122
|
+
let total = 0
|
|
123
|
+
for (const { dir, label } of scopes(ctx)) {
|
|
124
|
+
console.log('')
|
|
125
|
+
console.log(color.bold(` ${label} ${color.dim(dir)}`))
|
|
126
|
+
if (!dir || !existsSync(dir)) {
|
|
127
|
+
console.log(` ${color.dim('(目录不存在)')}`)
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
const dirs = readdirSync(dir).filter(n => existsSync(join(dir, n, 'index.mjs')))
|
|
131
|
+
if (!dirs.length) {
|
|
132
|
+
console.log(` ${color.dim('(空 —— aw plugin create <name> 创建)')}`)
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
for (const name of dirs) {
|
|
136
|
+
total++
|
|
137
|
+
const hasClient = existsSync(join(dir, name, 'client.mjs'))
|
|
138
|
+
console.log(` ${color.green('●')} ${color.cyan(name.padEnd(24))}${hasClient ? `${color.dim(' +client')}` : ''}`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
console.log('')
|
|
142
|
+
console.log(color.dim(`共 ${total} 个 · 契约: index.mjs 导出 { name, setup(ctx) } · 文档: docs/plugins.md`))
|
|
143
|
+
console.log('')
|
|
144
|
+
return 0
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function create(ctx, name, flags) {
|
|
148
|
+
if (!name || !/^[a-z][a-z0-9-]{1,31}$/.test(name)) {
|
|
149
|
+
throw new CliError('USAGE', '用法: aw plugin create <name> [--project|--global](name: 小写字母开头,2-32 位 a-z0-9-)')
|
|
150
|
+
}
|
|
151
|
+
const global = Boolean(flags.global ?? flags.g)
|
|
152
|
+
const force = Boolean(flags.force ?? flags.f)
|
|
153
|
+
const target = global
|
|
154
|
+
? join(ctx.home, 'plugins', name)
|
|
155
|
+
: join((ctx.root ?? process.cwd()), '.AgentWorkShop', 'plugins', name)
|
|
156
|
+
|
|
157
|
+
if (existsSync(join(target, 'index.mjs')) && !force) {
|
|
158
|
+
throw new CliError('CONFLICT', `插件已存在: ${target}(--force 覆盖)`)
|
|
159
|
+
}
|
|
160
|
+
mkdirSync(target, { recursive: true })
|
|
161
|
+
writeFileSync(join(target, 'index.mjs'), ENTRY_TEMPLATE(name), 'utf8')
|
|
162
|
+
writeFileSync(join(target, 'client.mjs'), CLIENT_TEMPLATE(name), 'utf8')
|
|
163
|
+
writeFileSync(join(target, 'README.md'), PLUGIN_README(name), 'utf8')
|
|
164
|
+
|
|
165
|
+
console.log(`${color.green('✔')} 插件已创建: ${color.bold(name)} → ${target}`)
|
|
166
|
+
console.log(` › 重启服务自动装载;API 示例: ${color.cyan(`GET /api/plugins/${name}/stats`)}`)
|
|
167
|
+
console.log(` › 查看列表: ${color.cyan('aw plugin list')}`)
|
|
168
|
+
return 0
|
|
169
|
+
}
|
package/cli/commands/update.mjs
CHANGED
|
@@ -59,13 +59,22 @@ function cmpVersions(a, b) {
|
|
|
59
59
|
return 0
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** 全局实际安装的版本(npm ls -g);拿不到回退 null */
|
|
63
|
+
function globalVersion(name, registry) {
|
|
64
|
+
const ls = npmRun(['ls', '-g', '--depth=0', name], { registry })
|
|
65
|
+
return new RegExp(`${name}@(\\S+)`).exec(ls.stdout ?? '')?.[1] ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
export async function run(argv, ctx) {
|
|
63
69
|
const { flags } = argv
|
|
64
70
|
const registry = flags.registry ? String(flags.registry) : undefined
|
|
65
71
|
const name = pkgName(ctx)
|
|
66
|
-
|
|
72
|
+
// 当前版本 = 全局实际安装的版本(即 `aw update` 要更新的对象);
|
|
73
|
+
// 拿不到(未全局安装)才回退运行中 CLI 版本
|
|
74
|
+
const global = globalVersion(name, registry)
|
|
75
|
+
const current = global ?? packageVersion()
|
|
67
76
|
|
|
68
|
-
console.log(`${color.cyan('›')} 当前版本: ${color.bold(`v${current}`)}`)
|
|
77
|
+
console.log(`${color.cyan('›')} 当前版本: ${color.bold(`v${current}`)}${global ? '' : color.dim(' (运行中 CLI;未检测到全局安装)')}`)
|
|
69
78
|
console.log(`${color.cyan('›')} 查询 npm registry${registry ? ` (${registry})` : ''} ...`)
|
|
70
79
|
|
|
71
80
|
const view = npmRun(['view', name, 'version'], { registry })
|
|
@@ -107,8 +116,7 @@ export async function run(argv, ctx) {
|
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
// 复核:从全局包清单读回实际安装版本
|
|
110
|
-
const
|
|
111
|
-
const installed = new RegExp(`${name}@(\\S+)`).exec(ls.stdout ?? '')?.[1]
|
|
119
|
+
const installed = globalVersion(name, registry)
|
|
112
120
|
if (installed) console.log(`${color.green('✔')} 更新完成: ${color.dim(current)} → ${color.bold(installed)}`)
|
|
113
121
|
else console.log(`${color.green('✔')} 更新完成 → ${color.bold(latest)}`)
|
|
114
122
|
console.log(`${color.dim('›')} 新开 aw 进程即运行新版本(当前进程仍是旧代码)`)
|
package/docs/plugins.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# AgentWorkShop 插件开发指南(SDK)
|
|
2
|
+
|
|
3
|
+
> 插件 = 配置根 `plugins/<name>/` 下的一个 node 项目。基于内置 SDK 的生命周期钩子,
|
|
4
|
+
> 可以同时增强**服务端**(数据/事件/API)与**浏览器**(面板/遥测/交互)。
|
|
5
|
+
> 与 `aw` 指令同哲学:放入目录即装载,约定优于配置。
|
|
6
|
+
|
|
7
|
+
## 一、快速开始
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
aw plugin create my-plugin # 脚手架到 ~/.AgentWorkShop/plugins/(用户级)
|
|
11
|
+
aw plugin create my-plugin --project # 或项目级 <repo>/.AgentWorkShop/plugins/
|
|
12
|
+
aw plugin list # 查看两处已装插件
|
|
13
|
+
aw start # 重启即自动装载
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
目录结构(标准 node 项目形态):
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
plugins/my-plugin/
|
|
20
|
+
├── index.mjs # 服务端入口(必需): export default { name, setup(ctx) }
|
|
21
|
+
├── client.mjs # 浏览器增强(可选): export function setup(ctx)
|
|
22
|
+
└── README.md
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 二、插件契约
|
|
26
|
+
|
|
27
|
+
`index.mjs` 导出**普通对象**(零导入依赖——ctx 由宿主注入,这是全局安装零解析链约束下的标准形态;TypeScript 作者可 `import type { definePlugin } from 'agentworkshop/sdk'` 获得类型提示,构建期擦除):
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
export default {
|
|
31
|
+
name: 'my-plugin', // 必填,全局唯一
|
|
32
|
+
version: '1.0.0',
|
|
33
|
+
description: '…',
|
|
34
|
+
client: './client.mjs', // 可选:浏览器增强入口(相对路径)
|
|
35
|
+
routes: [ // 可选:声明式 API(也可在 setup 里 ctx.route())
|
|
36
|
+
{ method: 'GET', path: '/health', handler: () => ({ ok: true }) },
|
|
37
|
+
],
|
|
38
|
+
async setup(ctx) { /* 服务端生命周期 */ },
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 三、服务端 ctx(SDK 注入)
|
|
43
|
+
|
|
44
|
+
| 成员 | 说明 |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `ctx.name / scope('project'\|'user') / dir` | 身份 |
|
|
47
|
+
| `ctx.hooks` | 全局 HookBus:`on / once / off / emit`(异步串行、错误隔离、`*` 通配、连续失败熔断) |
|
|
48
|
+
| `ctx.logger` | `debug/info/warn/error`,自动带插件名前缀 |
|
|
49
|
+
| `ctx.config.get(key) / all()` | 有效配置只读(四层引擎) |
|
|
50
|
+
| `ctx.kv` | 插件私有持久化:`get/set/all/bump`(内存态 + 200ms 防抖落盘 `data/plugins/<name>/kv.json`,高频钩子零竞态) |
|
|
51
|
+
| `ctx.route(method, path, handler)` | 注册插件 API → `**/api/plugins/<name><path>**` |
|
|
52
|
+
| `ctx.http.get/post(url, …)` | 带超时 fetch(仅 http/https,拒绝其他协议) |
|
|
53
|
+
| `ctx.events.on(type, fn)` | scene 实时事件订阅(`event:*` 桥的糖) |
|
|
54
|
+
| `ctx.paths` | `{ home, configRoot, dataDir }` |
|
|
55
|
+
|
|
56
|
+
## 四、生命周期钩子(服务端)
|
|
57
|
+
|
|
58
|
+
| 钩子 | 触发时机 | payload |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `plugin:host:init` | 宿主装载完所有插件后 | `{ plugins, failures }` |
|
|
61
|
+
| `daq:sample` | DAQ 下发级采样(与 WS `daq.reading` 同点、同节拍) | `{ nodeId, templateRef, value, state, at }` |
|
|
62
|
+
| `dcw:write` | 写控 ACK 后观察(与运维入册同点同去重) | `{ nodeId, name, eng, prevValue, ok, source, lineId, at }` |
|
|
63
|
+
| `line:start` / `line:stop` | 产线开跑/停止 | `{ lineId, runId, recipeId? }` |
|
|
64
|
+
| `event:<type>` / `event:*` | **scene 全部实时事件**(device.created · daq.node.changed · ops.log · daq.reading … 与 WS 同源) | 事件 payload |
|
|
65
|
+
| `server:close` | 服务关闭 | `{ at }` |
|
|
66
|
+
|
|
67
|
+
> v1 钩子为**观察语义**(不改变联锁/写控决策)。veto 类(拦截/改写)钩子在路线图中。
|
|
68
|
+
|
|
69
|
+
## 五、浏览器增强(client.mjs)
|
|
70
|
+
|
|
71
|
+
`client.mjs` 是**自包含 ESM**(不可用裸导入如 `vue`——浏览器原生动态加载),导出 `setup(ctx)`:
|
|
72
|
+
|
|
73
|
+
| 成员 | 说明 |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `ctx.on('daq:sample', fn)` / `ctx.on('event:daq.reading', fn)` / `ctx.on('*', fn)` | 实时事件订阅(与 WS 同源;另支持 `page:change`) |
|
|
76
|
+
| `ctx.el(tag, attrs, children)` | DOM 构建 |
|
|
77
|
+
| `ctx.root()` | 插件私有挂载点(右下角,懒创建 `#aw-plugin-<name>`) |
|
|
78
|
+
| `ctx.mount(target, node)` | 挂载到任意选择器/元素 |
|
|
79
|
+
| `ctx.hooks` | 客户端本地 HookBus(`client:init` / `event:*` / `page:change`) |
|
|
80
|
+
| `ctx.log` | 前缀 console |
|
|
81
|
+
|
|
82
|
+
客户端脚本由服务端端点 `/api/plugins/client/<name>` 以 `text/javascript` 提供,应用启动时
|
|
83
|
+
`aw-plugins.client.ts` loader 自动动态 import 并装载。
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
// client.mjs
|
|
87
|
+
export function setup(ctx) {
|
|
88
|
+
const badge = ctx.el('div', { style: 'color:#35e0a0' }, ['⌁ 0'])
|
|
89
|
+
ctx.root().append(badge)
|
|
90
|
+
let n = 0
|
|
91
|
+
ctx.on('daq:sample', () => { badge.textContent = `⌁ ${++n}` })
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 六、插件 API(增强后端)
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
ctx.route('GET', '/stats', () => ctx.kv.all())
|
|
99
|
+
ctx.route('POST', '/reset', () => { ctx.kv.set('samples', 0); return { ok: true } })
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
→ `GET/POST /api/plugins/my-plugin/stats|reset`。鉴权 v1 由插件自理(可在 handler 内复用
|
|
103
|
+
`resolveUser(event)` 走业务鉴权)。
|
|
104
|
+
|
|
105
|
+
## 七、完整示例
|
|
106
|
+
|
|
107
|
+
`sdk/examples/sample-insight/` —— 订阅采样计数 + 统计 API + 浏览器徽标:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
cp -r sdk/examples/sample-insight ~/.AgentWorkShop/plugins/
|
|
111
|
+
aw start # → GET /api/plugins/sample-insight/stats · 浏览器右下角徽标
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 八、发布与信任模型
|
|
115
|
+
|
|
116
|
+
- 插件是**任意 node 代码**,与 aw commands 同信任模型:只安装/启用你信任的插件。
|
|
117
|
+
- 插件随配置根走:repo 检出内 = `<repo>/.AgentWorkShop/plugins`(团队可 git 版本化);
|
|
118
|
+
全局安装 = `~/.AgentWorkShop/plugins`(AW_HOME 可重定向)。
|
|
119
|
+
- 服务端生命周期结束(`server:close`)、卸载插件 = 直接删目录重启。
|
|
120
|
+
|
|
121
|
+
## 九、SDK 版本对照
|
|
122
|
+
|
|
123
|
+
| SDK | 随包 | 说明 |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `sdk/hooks.mjs` | `agentworkshop/sdk`(0.2.2+) | HookBus(可独立用于任何 node 项目) |
|
|
126
|
+
| `sdk/context.mjs` | 同上 | 服务端 ctx 工厂 + 路由表 + 插件校验 |
|
|
127
|
+
| `sdk/client.mjs` | 同上 | 浏览器 ctx 工厂 |
|
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"
|
|
@@ -10,6 +25,7 @@
|
|
|
10
25
|
"files": [
|
|
11
26
|
"bin",
|
|
12
27
|
"cli",
|
|
28
|
+
"sdk",
|
|
13
29
|
"app",
|
|
14
30
|
"server",
|
|
15
31
|
"shared",
|
|
@@ -24,7 +40,8 @@
|
|
|
24
40
|
"docker-compose.yml",
|
|
25
41
|
".env.example",
|
|
26
42
|
"README-zh.md",
|
|
27
|
-
"docs/cli.md"
|
|
43
|
+
"docs/cli.md",
|
|
44
|
+
"docs/plugins.md"
|
|
28
45
|
],
|
|
29
46
|
"packageManager": "pnpm@11.9.0",
|
|
30
47
|
"engines": {
|
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)
|
|
@@ -40,6 +40,23 @@ export async function run(argv, ctx) {
|
|
|
40
40
|
也可以用 \`aw register <path|url|npm:pkg> --global\` 自动注册到这里。
|
|
41
41
|
`
|
|
42
42
|
|
|
43
|
+
const PLUGINS_README = `# AW Home 插件目录
|
|
44
|
+
|
|
45
|
+
每个子文件夹 = 一个插件(node 项目,入口 index.mjs),重启 aw start / aw dev 自动装载。
|
|
46
|
+
|
|
47
|
+
\`\`\`
|
|
48
|
+
plugins/
|
|
49
|
+
└── my-plugin/
|
|
50
|
+
├── index.mjs # 服务端: export default { name, setup(ctx) }
|
|
51
|
+
└── client.mjs # 浏览器增强(可选): export function setup(ctx)
|
|
52
|
+
\`\`\`
|
|
53
|
+
|
|
54
|
+
生命周期钩子: daq:sample · dcw:write · line:start|stop · event:*(scene 全事件) · server:close
|
|
55
|
+
插件 API: ctx.route('GET', '/x', handler) → /api/plugins/my-plugin/x
|
|
56
|
+
|
|
57
|
+
完整指南: docs/plugins.md · 脚手架: aw plugin create <name>
|
|
58
|
+
`
|
|
59
|
+
|
|
43
60
|
/**
|
|
44
61
|
* 执行引导。返回 { home, created: string[], seeds: string[] }。
|
|
45
62
|
* @param {{ quiet?: boolean, env?: NodeJS.ProcessEnv }} opts
|
|
@@ -53,7 +70,7 @@ export function runBootstrap({ quiet = false, env = process.env } = {}) {
|
|
|
53
70
|
const seeds = []
|
|
54
71
|
|
|
55
72
|
// 1. 目录骨架
|
|
56
|
-
for (const dir of [home, join(home, 'data'), join(home, 'logs'), join(home, 'commands')]) {
|
|
73
|
+
for (const dir of [home, join(home, 'data'), join(home, 'logs'), join(home, 'commands'), join(home, 'plugins')]) {
|
|
57
74
|
if (!existsSync(dir)) {
|
|
58
75
|
mkdirSync(dir, { recursive: true })
|
|
59
76
|
created.push(dir)
|
|
@@ -97,6 +114,13 @@ export function runBootstrap({ quiet = false, env = process.env } = {}) {
|
|
|
97
114
|
writeFileSync(readmeDest, COMMANDS_README, 'utf8')
|
|
98
115
|
}
|
|
99
116
|
|
|
117
|
+
// 7. 插件目录说明(插件 = 配置根 plugins/<name>/ 的 node 项目,SDK 驱动,见 docs/plugins.md)
|
|
118
|
+
const pluginsReadme = join(home, 'plugins', 'README.md')
|
|
119
|
+
if (!existsSync(pluginsReadme)) {
|
|
120
|
+
writeFileSync(pluginsReadme, PLUGINS_README, 'utf8')
|
|
121
|
+
seeds.push('plugins/README.md')
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
log(`[aw-home] ${home} ${created.length ? `(新建 ${created.length} 项)` : '(已就绪)'}`)
|
|
101
125
|
if (seeds.length) log(`[aw-home] 种子文件: ${seeds.join(', ')}`)
|
|
102
126
|
return { home, created, seeds }
|
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
|