agentworkshop 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }
@@ -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
- const current = packageVersion()
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 ls = npmRun(['ls', '-g', '--depth=0', name], { registry })
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 进程即运行新版本(当前进程仍是旧代码)`)
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "agentworkshop",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "AgentWorkShop 软件开发系统 - 基于 Nuxt 4 的配置驱动运行范式",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "bin",
12
12
  "cli",
13
+ "sdk",
13
14
  "app",
14
15
  "server",
15
16
  "shared",
@@ -24,7 +25,8 @@
24
25
  "docker-compose.yml",
25
26
  ".env.example",
26
27
  "README-zh.md",
27
- "docs/cli.md"
28
+ "docs/cli.md",
29
+ "docs/plugins.md"
28
30
  ],
29
31
  "packageManager": "pnpm@11.9.0",
30
32
  "engines": {
@@ -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/client.mjs ADDED
@@ -0,0 +1,76 @@
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)
9
+ // ctx.on(type, fn) scene 实时事件订阅(与 WS 同源;'*' 通配)
10
+ // ctx.el(tag, attrs, children) DOM 助手(挂到任意面板/宿主节点)
11
+ // ctx.mount(selector|el, node) 挂载节点(缺失时挂 body 角落)
12
+ // ctx.log 前缀 console
13
+ // ctx.root 插件 UI 挂载点(懒创建,自动附加到 body,#aw-plugin-<name>)
14
+ // ============================================================
15
+ import { HookBus } from './hooks.mjs'
16
+
17
+ export const CLIENT_SDK_VERSION = '0.2.2'
18
+
19
+ function el(tag, attrs = {}, children = []) {
20
+ const node = document.createElement(tag)
21
+ for (const [k, v] of Object.entries(attrs)) {
22
+ if (k === 'style') node.style.cssText = v
23
+ else if (k === 'class') node.className = v
24
+ else if (k.startsWith('on') && typeof v === 'function') node.addEventListener(k.slice(2), v)
25
+ else node.setAttribute(k, String(v))
26
+ }
27
+ for (const c of [].concat(children)) {
28
+ node.append(typeof c === 'string' ? document.createTextNode(c) : c)
29
+ }
30
+ return node
31
+ }
32
+
33
+ export function createClientContext({ name, eventBridge }) {
34
+ const hooks = new HookBus({ name: `client:${name}`, onError: err => console.warn(`[aw-plugin:${name}]`, err) })
35
+ let rootEl = null
36
+
37
+ const ctx = {
38
+ name,
39
+ sdkVersion: CLIENT_SDK_VERSION,
40
+ hooks,
41
+ on: (type, fn) => (type === 'event:*' ? hooks.on('*', fn) : hooks.on(`event:${type}`, fn)),
42
+ off: (type, fn) => (type === 'event:*' ? hooks.off('*', fn) : hooks.off(`event:${type}`, fn)),
43
+ el,
44
+ log: {
45
+ info: (...a) => console.info(`[aw-plugin:${name}]`, ...a),
46
+ warn: (...a) => console.warn(`[aw-plugin:${name}]`, ...a),
47
+ error: (...a) => console.error(`[aw-plugin:${name}]`, ...a),
48
+ },
49
+ root: () => {
50
+ rootEl ??= (() => {
51
+ const found = document.getElementById(`aw-plugin-${name}`)
52
+ if (found) return found
53
+ 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' })
54
+ document.body.append(box)
55
+ return box
56
+ })()
57
+ return rootEl
58
+ },
59
+ mount: (target, node) => {
60
+ const host = typeof target === 'string' ? document.querySelector(target) : target
61
+ ;(host ?? ctx.root()).append(node)
62
+ return node
63
+ },
64
+ }
65
+
66
+ // scene 事件桥 → ctx.hooks(event:<type> 与 '*' 均可订阅)
67
+ if (eventBridge) {
68
+ eventBridge((type, payload) => {
69
+ void hooks.emit(`event:${type}`, payload)
70
+ })
71
+ }
72
+
73
+ return ctx
74
+ }
75
+
76
+ export default { CLIENT_SDK_VERSION, createClientContext, el }