agentworkshop 0.3.0 → 0.4.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/LICENSE +91 -0
- package/README-zh.md +431 -424
- package/README.md +10 -3
- package/app/components/AppSidebar.vue +1 -0
- package/app/pages/plugins/index.vue +258 -0
- package/app/plugins/aw-plugins.client.ts +81 -48
- package/cli/commands/plugin.mjs +57 -7
- package/docs/plugins.md +133 -63
- package/docs/sdk.md +265 -0
- package/i18n/locales/en.ts +17 -0
- package/i18n/locales/zh-CN.ts +17 -0
- package/package.json +4 -2
- package/scripts/_dbg-docs-site-shot.mjs +33 -0
- package/scripts/home-bootstrap.mjs +26 -1
- package/sdk/client.mjs +6 -2
- package/sdk/examples/line-sentinel/client.mjs +31 -0
- package/sdk/examples/line-sentinel/index.mjs +102 -0
- package/sdk/examples/ops-notifier/client.mjs +23 -0
- package/sdk/examples/ops-notifier/index.mjs +34 -0
- package/server/api/workshop/plugins/[name]/disable.post.ts +19 -0
- package/server/api/workshop/plugins/[name]/enable.post.ts +19 -0
- package/server/api/workshop/plugins/[name]/index.get.ts +14 -0
- package/server/api/workshop/plugins/index.get.ts +17 -0
- package/server/data/daqs.json +68 -46
- package/server/data/dcw-lines.json +7 -0
- package/server/data/dcw-products.json +7 -0
- package/server/data/dcw-recipes.json +20 -0
- package/server/data/dcw-rollback.json +1 -1
- package/server/data/dcw-runs.json +25 -18
- package/server/data/dcws.json +21 -0
- package/server/data/line-runs.json +30 -20
- package/server/services/workshop/plugins/host.mjs +177 -49
package/docs/plugins.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
# AgentWorkShop
|
|
1
|
+
# AgentWorkShop 插件开发指南
|
|
2
2
|
|
|
3
3
|
> 插件 = 配置根 `plugins/<name>/` 下的一个 node 项目。基于内置 SDK 的生命周期钩子,
|
|
4
|
-
>
|
|
4
|
+
> 同时增强**服务端**(数据/事件/API)与**浏览器**(面板/遥测/交互)。
|
|
5
5
|
> 与 `aw` 指令同哲学:放入目录即装载,约定优于配置。
|
|
6
|
+
> SDK 全部成员的逐项详解见 [sdk.md](./sdk.md)——本文聚焦插件视角的装配与生命周期。
|
|
6
7
|
|
|
7
8
|
## 一、快速开始
|
|
8
9
|
|
|
@@ -22,9 +23,26 @@ plugins/my-plugin/
|
|
|
22
23
|
└── README.md
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
##
|
|
26
|
+
## 二、装载流程(项目启动时自动发现)
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
```
|
|
29
|
+
服务启动(nitro 插件 server/plugins/aw-plugins.ts)
|
|
30
|
+
└─ 宿主扫描(双作用域,同名项目级覆盖用户级)
|
|
31
|
+
├─ <检出>/.AgentWorkShop/plugins/*/index.mjs (project)
|
|
32
|
+
└─ ~/.AgentWorkShop/plugins/*/index.mjs (user / AW_HOME 重定向)
|
|
33
|
+
└─ 逐插件: 动态 import → 形态校验 → createPluginContext → setup(ctx)
|
|
34
|
+
└─ 注册插件路由(/api/plugins/<name>/**) + 解析客户端入口
|
|
35
|
+
└─ emit plugin:host:init
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- **错误隔离**:单插件装载/执行失败记入 `failures` 并告警,绝不拖垮主服务。
|
|
39
|
+
- **修改插件文件后需重启服务**(插件目录不在热更新监听范围)。
|
|
40
|
+
- 客户端插件由 `app/plugins/aw-plugins.client.ts` loader 在浏览器侧动态装载,互不影响。
|
|
41
|
+
|
|
42
|
+
## 三、插件契约
|
|
43
|
+
|
|
44
|
+
`index.mjs` 导出**普通对象**(零导入依赖——`ctx` 由宿主注入;TypeScript 可 type-only
|
|
45
|
+
导入 `agentworkshop/sdk` 获得类型,运行时擦除):
|
|
28
46
|
|
|
29
47
|
```js
|
|
30
48
|
export default {
|
|
@@ -39,51 +57,89 @@ export default {
|
|
|
39
57
|
}
|
|
40
58
|
```
|
|
41
59
|
|
|
42
|
-
##
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
## 四、服务端生命周期(完整)
|
|
61
|
+
|
|
62
|
+
装载完成后,以下事件按业务发生顺序流经 `ctx.hooks`(**每个都可直接 `ctx.hooks.on` 消费**):
|
|
63
|
+
|
|
64
|
+
### 4.1 `plugin:host:init`
|
|
65
|
+
- **时机**:宿主装载完所有插件后(一次性)。
|
|
66
|
+
- **payload**:`{ plugins: string[], failures: number }`。
|
|
67
|
+
- **用途**:就绪自证;延迟初始化(依赖其他插件已注册路由的场景)。
|
|
68
|
+
|
|
69
|
+
### 4.2 `daq:sample`
|
|
70
|
+
- **时机**:数采**下发级**采样——与 WS `daq.reading` 同一点、按节点 `publishIntervalMs` 节拍。
|
|
71
|
+
- **payload**:`{ nodeId, templateRef, value, state, at }`。
|
|
72
|
+
- **用途**:越限告警、采样统计、实时联动。
|
|
73
|
+
- **示例**:
|
|
74
|
+
```js
|
|
75
|
+
ctx.hooks.on('daq:sample', (s) => {
|
|
76
|
+
if (s.value > (ctx.kv.get('threshold') ?? 180)) ctx.kv.bump('alarms')
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 4.3 `dcw:write`
|
|
81
|
+
- **时机**:写控 ACK 之后(与运维入册同点、同 10s 去重节流)——**观察语义,不影响写控决策**。
|
|
82
|
+
- **payload**:`{ nodeId, name, eng, prevValue, ok, source('manual'|'recipe'|'agent'|'rollback'), lineId, at }`。
|
|
83
|
+
- **用途**:写审计、下发趋势、回写记录。
|
|
84
|
+
|
|
85
|
+
### 4.4 `line:start` / `line:stop`
|
|
86
|
+
- **时机**:产线开跑/停止(批次窗口开/闭)。
|
|
87
|
+
- **payload**:`{ lineId, runId, recipeId?, productName? }` / `{ lineId, runId }`。
|
|
88
|
+
- **用途**:批次开始联动、停线报告。
|
|
89
|
+
|
|
90
|
+
### 4.5 `event:<type>` / `event:*`
|
|
91
|
+
- **时机**:scene 全部实时事件(`device.created|updated|deleted`、`daq.reading`、
|
|
92
|
+
`daq.node.changed`、`daq.controller`、`ops.log` …)——与浏览器 WS 完全同源。
|
|
93
|
+
- **消费**:`ctx.events.on('daq.reading', fn)`(糖衣)或 `ctx.hooks.on('event:daq.reading', fn)`。
|
|
94
|
+
- **用途**:任意平台事件的观察与增强,无需自建 WS 连接。
|
|
95
|
+
|
|
96
|
+
### 4.6 `config:changed`
|
|
97
|
+
- **时机**:`runtime-settings.json` 变化(`aw config set` / 网页设置写入;宿主 fs.watch 防抖 300ms)。
|
|
98
|
+
- **payload**:`{ at }`;配合 `ctx.config.get/all()` 读取新值。
|
|
99
|
+
- **用途**:阈值热更新、联动参数刷新。
|
|
100
|
+
|
|
101
|
+
### 4.7 `server:close`
|
|
102
|
+
- **时机**:服务关闭——**先逐插件执行 `ctx.onDispose` 队列,再广播本事件**。
|
|
103
|
+
- **payload**:`{ at }`。
|
|
104
|
+
- **用途**:最终落盘、对外通知。
|
|
105
|
+
|
|
106
|
+
## 五、服务端 ctx 全成员
|
|
107
|
+
|
|
108
|
+
| 分组 | 成员 | 说明 |
|
|
59
109
|
|---|---|---|
|
|
60
|
-
| `
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
| `
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
`
|
|
110
|
+
| 身份 | `ctx.name / scope / dir / sdkVersion` | 插件名 / `'project'\|'user'` / 目录 / SDK 版本 |
|
|
111
|
+
| 钩子 | `ctx.hooks` | HookBus:`on/once/off/emit`(异步串行、错误隔离、`'*'` 通配、连续失败 8 次熔断) |
|
|
112
|
+
| 日志 | `ctx.logger` | `debug/info/warn/error`,自动前缀 `[插件名]` |
|
|
113
|
+
| 配置 | `ctx.config.get(key)` / `all()` / `onChange(fn)` | 有效配置只读 + 变更订阅 |
|
|
114
|
+
| 存储 | `ctx.kv.get/set/all/bump` | 插件私有 KV(内存态 + 200ms 防抖落盘,高频钩子零竞态) |
|
|
115
|
+
| 定时 | `ctx.timer.setInterval/setTimeout` | **服务关闭自动回收**,杜绝定时器泄漏 |
|
|
116
|
+
| 清理 | `ctx.onDispose(fn)` / `ctx.subscriptions.add(d)` | `server:close` 前逐个执行(先于广播) |
|
|
117
|
+
| 路由 | `ctx.route(method, path, handler)` | 插件 API → `/api/plugins/<name><path>` |
|
|
118
|
+
| 平台 | `ctx.api` | 平台 REST 客户端(自环 origin;鉴权端点 `ctx.api.setToken(token)`) |
|
|
119
|
+
| 网络 | `ctx.http.get/post` | 通用请求(仅 http/https,默认 8s 超时) |
|
|
120
|
+
| 事件 | `ctx.events.on(type, fn)` | scene 实时事件订阅 |
|
|
121
|
+
| 路径 | `ctx.paths` | `{ home, configRoot, dataDir }` |
|
|
122
|
+
|
|
123
|
+
> **鉴权说明**:`ctx.api` 自环调用遵循平台 REST 鉴权——免鉴权端点(manifest/ping)开箱即用;
|
|
124
|
+
> 鉴权端点需 `ctx.api.setToken(token)`(token 可经 `AW_TOKEN` 环境变量注入插件)。
|
|
125
|
+
> 仅需进程内数据时优先 `ctx.events`/`ctx.hooks`(零鉴权、零开销)。
|
|
126
|
+
|
|
127
|
+
## 六、浏览器增强(client.mjs)
|
|
128
|
+
|
|
129
|
+
`client.mjs` 为**自包含 ESM**(无裸导入),导出 `setup(ctx)`。loader
|
|
130
|
+
(`app/plugins/aw-plugins.client.ts`) 启动期经 `/api/plugins/manifest` 发现并动态装载,
|
|
131
|
+
事件与 WS 完全同源:
|
|
72
132
|
|
|
73
133
|
| 成员 | 说明 |
|
|
74
134
|
|---|---|
|
|
75
|
-
| `ctx.on(
|
|
135
|
+
| `ctx.on(type, fn)` | `daq:sample` / `event:<type>` / `event:*` / `page:change` 订阅(pagehide 自动回收) |
|
|
136
|
+
| `ctx.fetch(path, opt?)` | 同源平台 API(JSON + 信封解包,非 2xx 抛错) |
|
|
76
137
|
| `ctx.el(tag, attrs, children)` | DOM 构建 |
|
|
77
|
-
| `ctx.root()`
|
|
78
|
-
| `ctx.
|
|
79
|
-
| `ctx.
|
|
80
|
-
| `ctx.log` | 前缀 console |
|
|
81
|
-
|
|
82
|
-
客户端脚本由服务端端点 `/api/plugins/client/<name>` 以 `text/javascript` 提供,应用启动时
|
|
83
|
-
`aw-plugins.client.ts` loader 自动动态 import 并装载。
|
|
138
|
+
| `ctx.root()` / `ctx.mount(target, node)` | 私有挂载点 / 任意位置挂载 |
|
|
139
|
+
| `ctx.hooks` | 本地 HookBus(`client:init` / `page:change` / `client:destroy`) |
|
|
140
|
+
| `ctx.dispose()` | 卸载(回收订阅 + 清空挂载点;pagehide 自动触发) |
|
|
84
141
|
|
|
85
142
|
```js
|
|
86
|
-
// client.mjs
|
|
87
143
|
export function setup(ctx) {
|
|
88
144
|
const badge = ctx.el('div', { style: 'color:#35e0a0' }, ['⌁ 0'])
|
|
89
145
|
ctx.root().append(badge)
|
|
@@ -92,36 +148,50 @@ export function setup(ctx) {
|
|
|
92
148
|
}
|
|
93
149
|
```
|
|
94
150
|
|
|
95
|
-
##
|
|
151
|
+
## 七、插件 API(增强后端)
|
|
96
152
|
|
|
97
153
|
```js
|
|
98
154
|
ctx.route('GET', '/stats', () => ctx.kv.all())
|
|
99
|
-
ctx.route('POST', '/reset', () => {
|
|
155
|
+
ctx.route('POST', '/reset', (event) => {
|
|
156
|
+
const body = event.awBody // 宿主已预读 JSON body
|
|
157
|
+
ctx.kv.reset()
|
|
158
|
+
return { ok: true }
|
|
159
|
+
})
|
|
100
160
|
```
|
|
101
161
|
|
|
102
|
-
→
|
|
103
|
-
`resolveUser(event)`
|
|
162
|
+
→ `/api/plugins/<name><path>`。宿主 catchall 转发(exact-match),body 预读挂 `event.awBody`;
|
|
163
|
+
v1 鉴权由插件自理(可在 handler 内 `resolveUser(event)` 复用业务鉴权)。
|
|
104
164
|
|
|
105
|
-
##
|
|
165
|
+
## 八、真实案例:line-sentinel(产线哨兵)
|
|
106
166
|
|
|
107
|
-
|
|
167
|
+
用户级安装,展示 SDK 全部能力面——源码 `~/.AgentWorkShop/plugins/line-sentinel/`:
|
|
108
168
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
169
|
+
- **`ctx.api`**:启动读取平台产线清单(自环 REST)
|
|
170
|
+
- **`ctx.hooks.on('daq:sample')`**:逐样本计数 + 越限告警(阈值可调)
|
|
171
|
+
- **`ctx.hooks.on('line:start'/'line:stop')`**:运行态跟踪
|
|
172
|
+
- **`ctx.config.onChange`**:配置热更新感知
|
|
173
|
+
- **`ctx.timer`**:5s 心跳 + 免鉴权 manifest ping 自证 REST 通道
|
|
174
|
+
- **`ctx.route`**:`GET /report` 综合报告、`POST /threshold` 阈值设置(`event.awBody`)
|
|
175
|
+
- **`client.mjs`**:右下角实时徽标(样本数 + 告警数)
|
|
113
176
|
|
|
114
|
-
|
|
177
|
+
实测记录(dev 3127 / prod 3601 双形态):8 秒采样计数 288+、12 节点越限告警、
|
|
178
|
+
心跳间隔 ~1s 内新鲜、浏览器徽标挂载零 pageerror。
|
|
115
179
|
|
|
116
|
-
|
|
117
|
-
- 插件随配置根走:repo 检出内 = `<repo>/.AgentWorkShop/plugins`(团队可 git 版本化);
|
|
118
|
-
全局安装 = `~/.AgentWorkShop/plugins`(AW_HOME 可重定向)。
|
|
119
|
-
- 服务端生命周期结束(`server:close`)、卸载插件 = 直接删目录重启。
|
|
180
|
+
## 九、调试与陷阱
|
|
120
181
|
|
|
121
|
-
|
|
182
|
+
| 现象 | 原因 / 处理 |
|
|
183
|
+
|---|---|
|
|
184
|
+
| 改了插件代码没生效 | 插件目录不在热更新范围——**重启 `aw start` / `aw dev`** |
|
|
185
|
+
| 客户端徽标没出现 | 打开浏览器 console 看 `[aw-plugins]` 告警;确认 manifest 中 `hasClient: true` |
|
|
186
|
+
| 插件路由 404 | 路由为 exact-match;注意 method 与 path 前导 `/` |
|
|
187
|
+
| `ctx.api` 鉴权 401 | 鉴权端点需 `ctx.api.setToken(token)`;免鉴权端点(manifest/ping)无需 |
|
|
188
|
+
| 插件装载失败 | `aw dev/start` 启动日志有 `[aw-plugins] 装载失败` 详情;修复后重启 |
|
|
122
189
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
190
|
+
**信任模型**:插件是任意 node 代码,与 aw commands 同级——只安装/启用你信任的插件。
|
|
191
|
+
|
|
192
|
+
## 十、发布与作用域
|
|
193
|
+
|
|
194
|
+
- 用户级(全局):`~/.AgentWorkShop/plugins/` —— `AW_HOME` 可重定向。
|
|
195
|
+
- 项目级(检出):`<repo>/.AgentWorkShop/plugins/` —— 团队可 git 版本化共享。
|
|
196
|
+
- 同名指令式覆盖规则与 commands 一致:**项目级 > 用户级 > 内建**。
|
|
197
|
+
- 卸载 = 删除目录后重启。
|
package/docs/sdk.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# AgentWorkShop SDK 开发与使用指南
|
|
2
|
+
|
|
3
|
+
> SDK 是 AgentWorkShop 的**编程客户端与扩展基座**:外部项目经它消费平台 REST 服务;
|
|
4
|
+
> 插件经它获得宿主注入的运行时上下文(钩子 / 配置 / 存储 / 定时器 / 平台 API / 事件流)。
|
|
5
|
+
> 零第三方运行时依赖,Node ≥ 23.4 与现代浏览器双端可用。
|
|
6
|
+
|
|
7
|
+
**版本**:与主包同步(`SDK_VERSION = 0.3.0`)· **协议**:ESM only
|
|
8
|
+
**类型**:`agentworkshop/sdk` 自带 `index.d.mts`,TS 项目零配置获得完整提示。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. 获取 SDK
|
|
13
|
+
|
|
14
|
+
### 1.1 全局安装形态(含 CLI 与平台本体)
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g agentworkshop
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
SDK 位于全局包内:`$(npm prefix -g)/node_modules/agentworkshop/sdk/`。
|
|
21
|
+
插件**无需也不应该**直接 import 它(见 §2 的宿主注入模型),它主要服务两类人:
|
|
22
|
+
|
|
23
|
+
- **外部项目集成**:把 AgentWorkShop 的产线/数采/写控/孪生能力嵌进你自己的 node 服务;
|
|
24
|
+
- **插件开发**(TS):仅导入类型获得完整 IntelliSense,运行时由宿主注入。
|
|
25
|
+
|
|
26
|
+
### 1.2 项目依赖形态(推荐集成方)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install agentworkshop # 作为依赖(含完整 SDK 与类型)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 1.3 导入路径
|
|
33
|
+
|
|
34
|
+
| 导入 | 内容 |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `agentworkshop/sdk` | 全部门面(客户端 + 服务端 + 平台 REST 客户端 + 类型) |
|
|
37
|
+
| `agentworkshop/sdk/client` | 仅浏览器端(`createClientContext`) |
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { createPlatformClient, definePlugin, HookBus } from 'agentworkshop/sdk'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. 核心模型:两种身份,一个 SDK
|
|
46
|
+
|
|
47
|
+
| 身份 | 形态 | SDK 的角色 |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| **外部集成者** | 普通依赖 | 直接调用 `createPlatformClient()` 消费平台 REST 面 |
|
|
50
|
+
| **插件作者** | 导出 `{ name, setup(ctx) }` | `ctx` 即宿主注入的 SDK 上下文——**零导入依赖**,SDK 运行时由宿主提供 |
|
|
51
|
+
|
|
52
|
+
> 为什么插件不直接 import SDK?全局安装的插件目录(`~/.AgentWorkShop/plugins/`)
|
|
53
|
+
> 不在 node_modules 解析链上,宿主注入是唯一零坑形态(VSCode `activate(context)` 同范式)。
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 3. 平台 REST 客户端 `createPlatformClient`
|
|
58
|
+
|
|
59
|
+
SDK 作为"项目服务客户端"的门面。自动携带鉴权、自动解包平台统一信封 `{ code, message, data } → data`、非 2xx 抛错(含 `err.status` 与 `err.body`)。
|
|
60
|
+
|
|
61
|
+
### 3.1 创建
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import { createPlatformClient } from 'agentworkshop/sdk'
|
|
65
|
+
|
|
66
|
+
const api = createPlatformClient({
|
|
67
|
+
baseUrl: 'http://127.0.0.1:3001', // 缺省 ''(同源相对路径);集成方填平台地址
|
|
68
|
+
token: '<bearer-token>', // 可选;后续可 api.setToken() 更换
|
|
69
|
+
timeoutMs: 10_000, // 可选;单请求超时
|
|
70
|
+
logger, // 可选;请求失败时告警
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3.2 通用调用(任意平台路径)
|
|
75
|
+
|
|
76
|
+
| 方法 | 签名 | 说明 |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `api.call(method, path, body?)` | 底层 | 返回解包后的 `data` |
|
|
79
|
+
| `api.get(path, query?)` | query 对象自动序列化 | `api.get('/api/workshop/dcw', { page: 1 })` |
|
|
80
|
+
| `api.post(path, body?)` | JSON 序列化 | |
|
|
81
|
+
| `api.patch(path, body?)` | | |
|
|
82
|
+
| `api.delete(path)` | | |
|
|
83
|
+
| `api.setToken(token)` | 链式 | 登录后 `api.setToken(res.token)` |
|
|
84
|
+
|
|
85
|
+
### 3.3 资源面(开箱即用)
|
|
86
|
+
|
|
87
|
+
| 资源 | 方法 |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `api.users` | `login(email, password)` · `me()` · `list/get/create/update/remove` |
|
|
90
|
+
| `api.lines` | CRUD + **`start(id, recipeId?)`** · `stop(id)` |
|
|
91
|
+
| `api.products / api.recipes` | CRUD |
|
|
92
|
+
| `api.dcwNodes` | 写控制节点 CRUD |
|
|
93
|
+
| `api.daqNodes` | 数采节点 CRUD + `alarms()` |
|
|
94
|
+
| `api.templates` | `daq()` / `dcw()` 信号模板注册表 |
|
|
95
|
+
| `api.twins` | 数字孪生设备 CRUD |
|
|
96
|
+
| `api.teams / api.agents / api.channels` | Agent 编组面 |
|
|
97
|
+
| `api.plugins.manifest()` | 已装载插件清单(免鉴权) |
|
|
98
|
+
| `api.ping()` | 存活探测(免鉴权) |
|
|
99
|
+
|
|
100
|
+
### 3.4 示例:外部项目集成产线
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
import { createPlatformClient } from 'agentworkshop/sdk'
|
|
104
|
+
|
|
105
|
+
const api = createPlatformClient({ baseUrl: 'http://plant.local:3001' })
|
|
106
|
+
|
|
107
|
+
const { token } = await api.users.login('you@example.com', 'secret')
|
|
108
|
+
api.setToken(token)
|
|
109
|
+
|
|
110
|
+
await api.lines.create({ name: '一号产线' })
|
|
111
|
+
const line = (await api.lines.list())[0]
|
|
112
|
+
|
|
113
|
+
await api.lines.start(line.id) // 开跑
|
|
114
|
+
const nodes = await api.daqNodes.list({ lineId: line.id })
|
|
115
|
+
console.log(`产线 ${line.name} 在线节点 ${nodes.filter(n => n.state !== 'offline').length}`)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 4. 插件上下文 `ctx`(完整成员)
|
|
121
|
+
|
|
122
|
+
`setup(ctx)` 收到的 `ctx` 是宿主装配的 SDK 运行时。**全部成员一览**:
|
|
123
|
+
|
|
124
|
+
| 分组 | 成员 | 类型 | 说明 |
|
|
125
|
+
|---|---|---|---|
|
|
126
|
+
| 身份 | `ctx.name` | `string` | 插件名(= 目录名/声明名) |
|
|
127
|
+
| | `ctx.scope` | `'project' \| 'user'` | 装载作用域 |
|
|
128
|
+
| | `ctx.dir` | `string` | 插件目录绝对路径 |
|
|
129
|
+
| | `ctx.sdkVersion` | `string` | 宿主 SDK 版本 |
|
|
130
|
+
| 钩子 | `ctx.hooks` | `HookBus` | 全局钩子总线(§5) |
|
|
131
|
+
| 日志 | `ctx.logger` | `{ debug, info, warn, error }` | 自动前缀 `[插件名]` |
|
|
132
|
+
| 配置 | `ctx.config.get(key)` | 任意 | 有效配置项(四层合并后) |
|
|
133
|
+
| | `ctx.config.all()` | `Record<string, any>` | 全部有效配置快照 |
|
|
134
|
+
| | `ctx.config.onChange(fn)` | 退订函数 | 订阅 `config:changed` |
|
|
135
|
+
| 存储 | `ctx.kv.get(key)` / `set(key, v)` / `all()` / `bump(key, by?)` | | 插件私有 KV(内存态 + 200ms 防抖落盘,防竞态) |
|
|
136
|
+
| 定时 | `ctx.timer.setInterval(fn, ms)` / `setTimeout(fn, ms)` | id | **服务关闭自动回收**,无需手动 clear |
|
|
137
|
+
| 清理 | `ctx.onDispose(fn)` / `ctx.subscriptions.add(d)` | | 登记 `server:close` 时执行的清理 |
|
|
138
|
+
| 路由 | `ctx.route(method, path, handler)` | boolean | 注册插件 API → `/api/plugins/<name><path>` |
|
|
139
|
+
| 平台 | `ctx.api` | `PlatformClient` | 自环 REST 客户端(origin 由宿主权威解析) |
|
|
140
|
+
| 网络 | `ctx.http.get(url)` / `post(url, body)` | `Response` | 通用请求(**仅 http/https**,8s 默认超时) |
|
|
141
|
+
| 事件 | `ctx.events.on(type, fn)` / `off` | 退订函数 | scene 实时事件订阅(§5 `event:*` 糖衣) |
|
|
142
|
+
| 路径 | `ctx.paths` | `{ home, configRoot, dataDir }` | 配置根信息(home 模式 = `~/.AgentWorkShop`) |
|
|
143
|
+
| 数据 | `ctx.dataDir` | `string` | 插件私有数据目录(`data/plugins/<name>`) |
|
|
144
|
+
|
|
145
|
+
### 4.1 `ctx.hooks` — 钩子总线
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
const off = ctx.hooks.on('daq:sample', (sample) => {
|
|
149
|
+
ctx.kv.bump('samples')
|
|
150
|
+
})
|
|
151
|
+
ctx.hooks.once('server:close', () => { /* 收尾 */ })
|
|
152
|
+
ctx.hooks.off('daq:sample', handler) // 手动解绑
|
|
153
|
+
await ctx.hooks.emit('my-plugin:custom', { hello: 1 }) // 插件间通信(其他插件可监听)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- **异步串行**:同 type 监听器按注册序 `await`,返回值链式传递(waterfall)。
|
|
157
|
+
- **错误隔离**:单监听器抛错只计数告警,不影响兄弟监听器与主服务。
|
|
158
|
+
- **熔断**:同一监听器连续失败 ≥ 8 次自动摘除(防病态插件刷屏)。
|
|
159
|
+
- **`'*'` 通配**:`ctx.hooks.on('*', ({ type, payload }) => …)` 收全部事件。
|
|
160
|
+
|
|
161
|
+
### 4.2 `ctx.kv` — 插件私有持久化
|
|
162
|
+
|
|
163
|
+
- 存储位置:`<配置根>/data/plugins/<name>/kv.json`(200ms 防抖原子写盘)。
|
|
164
|
+
- **内存态为准**:高频钩子(`daq:sample`)与低频钩子(`line:stop`)并发调用无读改写竞态。
|
|
165
|
+
- 典型用途:计数器、告警状态、阈值配置、心跳时间戳。
|
|
166
|
+
|
|
167
|
+
### 4.3 `ctx.timer` / `ctx.onDispose` — 生命周期安全的后台工作
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
ctx.timer.setInterval(() => ctx.kv.set('heartbeat', Date.now()), 5000) // 关闭自动回收
|
|
171
|
+
ctx.onDispose(() => ctx.logger.info('插件清理完成')) // 显式清理登记
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
服务关闭序列:**先逐插件执行 onDispose 队列(逐个 try/catch)→ 再广播 `server:close`**。
|
|
175
|
+
|
|
176
|
+
### 4.4 `ctx.route` — 插件 API(增强后端)
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
ctx.route('GET', '/stats', () => ctx.kv.all())
|
|
180
|
+
ctx.route('POST', '/reset', (event) => {
|
|
181
|
+
const body = event.awBody // 宿主已预读 JSON body
|
|
182
|
+
ctx.kv.reset()
|
|
183
|
+
return { ok: true } // 返回值由 nitro 序列化为 JSON
|
|
184
|
+
})
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
→ `GET /api/plugins/<name>/stats`。v1 鉴权由插件自理(handler 内可用 `resolveUser(event)` 复用业务鉴权)。
|
|
188
|
+
|
|
189
|
+
### 4.5 `ctx.api` vs `ctx.http` vs `ctx.events` — 怎么选
|
|
190
|
+
|
|
191
|
+
| 需求 | 用 | 原因 |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| 读/写**平台业务数据**(产线、节点、孪生…) | `ctx.api` | 鉴权/信封/资源语义开箱即用 |
|
|
194
|
+
| 调**外部系统**(MES、webhook、邮件网关) | `ctx.http` | 通用请求 + 协议守卫 |
|
|
195
|
+
| 对**实时流**做反应(采样、告警、启停) | `ctx.events` / `ctx.hooks.on` | 进程内直连,零 HTTP 开销 |
|
|
196
|
+
|
|
197
|
+
> 鉴权说明:`ctx.api` 自环调用遵循平台 REST 鉴权策略——免鉴权端点(manifest/ping)
|
|
198
|
+
> 开箱即用;鉴权端点需 `ctx.api.setToken(token)`(token 可来自环境变量,如 `AW_TOKEN`)。
|
|
199
|
+
> 服务端插件若仅需进程内数据,优先用 `ctx.events` 与 `ctx.hooks`(零鉴权、零开销)。
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 5. 生命周期事件(服务端,全部钩子)
|
|
204
|
+
|
|
205
|
+
| 事件 | 触发时机 | payload | 典型用途 |
|
|
206
|
+
|---|---|---|---|
|
|
207
|
+
| `plugin:host:init` | 宿主装载完所有插件后 | `{ plugins: string[], failures: number }` | 就绪自证、延迟初始化 |
|
|
208
|
+
| `config:changed` | `runtime-settings.json` 变化(`aw config set` / 设置页写入) | `{ at }` | 热更新阈值、刷新缓存的配置 |
|
|
209
|
+
| `daq:sample` | 数采**下发级**采样(与 WS `daq.reading` 同点、按节点 publishIntervalMs 节拍) | `{ nodeId, templateRef, value, state, at }` | 越限告警、统计、联动 |
|
|
210
|
+
| `dcw:write` | 写控 ACK 后观察(与运维入册同点、10s 去重) | `{ nodeId, name, eng, prevValue, ok, source, lineId, at }` | 写审计、趋势记录 |
|
|
211
|
+
| `line:start` | 产线开跑 | `{ lineId, runId, recipeId, productName? }` | 批次开始联动 |
|
|
212
|
+
| `line:stop` | 产线停止 | `{ lineId, runId }` | 批次收尾、报告生成 |
|
|
213
|
+
| `event:<scene-type>` / `event:*` | scene 全部实时事件(`device.created` · `daq.node.changed` · `ops.log` …与 WS 同源) | 事件 payload | 任意平台事件的观察与增强 |
|
|
214
|
+
| `server:close` | 服务关闭(disposables 回收之后) | `{ at }` | 最终落盘、对外通知 |
|
|
215
|
+
|
|
216
|
+
> v1 钩子为**观察语义**——不改变联锁/写控决策;veto(拦截/改写)钩子在路线图。
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 6. 浏览器端 SDK(`agentworkshop/sdk/client`)
|
|
221
|
+
|
|
222
|
+
插件 `client.mjs`(自包含 ESM,**无裸导入**)导出 `setup(ctx)`,由宿主 loader 动态装载:
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
export function setup(ctx) {
|
|
226
|
+
const badge = ctx.el('div', { style: 'color:#35e0a0' }, ['⌁ 0'])
|
|
227
|
+
ctx.root().append(badge) // 私有挂载点(右下角)
|
|
228
|
+
let n = 0
|
|
229
|
+
ctx.on('daq:sample', () => { badge.textContent = `⌁ ${++n}` })
|
|
230
|
+
ctx.on('page:change', ({ path }) => ctx.log.info('page →', path))
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
| 成员 | 说明 |
|
|
235
|
+
|---|---|
|
|
236
|
+
| `ctx.on(type, fn)` | 实时事件订阅(`daq:sample` / `event:<type>` / `event:*` / `page:change`),**pagehide 自动回收** |
|
|
237
|
+
| `ctx.fetch(path, opt?)` | 同源平台 API 助手(自动 JSON + 信封解包;非 2xx 抛错) |
|
|
238
|
+
| `ctx.el(tag, attrs, children)` | DOM 构建(style/class/事件 attrs 特判) |
|
|
239
|
+
| `ctx.root()` | 插件私有挂载点 `#aw-plugin-<name>`(懒创建) |
|
|
240
|
+
| `ctx.mount(target, node)` | 挂载到任意选择器/元素 |
|
|
241
|
+
| `ctx.hooks` | 本地 HookBus(`client:init` / `page:change` / `client:destroy`) |
|
|
242
|
+
| `ctx.dispose()` | 卸载:回收订阅 + 清空挂载点 + 广播 `client:destroy`(pagehide 自动触发,幂等) |
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 7. 类型与 TypeScript
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import type { PluginDef, PluginContext, PlatformClient } from 'agentworkshop/sdk'
|
|
250
|
+
|
|
251
|
+
export default {
|
|
252
|
+
name: 'typed-plugin',
|
|
253
|
+
async setup(ctx: PluginContext) {
|
|
254
|
+
const threshold: number = ctx.config.get('api.timeout')
|
|
255
|
+
},
|
|
256
|
+
} satisfies PluginDef
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
运行时**不引入** SDK(保持零依赖形态);类型在构建期擦除。
|
|
260
|
+
|
|
261
|
+
## 8. 版本与兼容性策略
|
|
262
|
+
|
|
263
|
+
- SDK 遵循 semver:patch = 修复;minor = 新增钩子/ctx 成员(向后兼容);major = 破坏性契约变更。
|
|
264
|
+
- `aw start` 启动时对配置根做校验与就地迁移——升级不丢数据。
|
|
265
|
+
- 宿主在 ctx 中暴露 `ctx.sdkVersion`,插件可按版本特性降级。
|
package/i18n/locales/en.ts
CHANGED
|
@@ -23,6 +23,7 @@ export default {
|
|
|
23
23
|
logs: 'Logs',
|
|
24
24
|
users: 'Users',
|
|
25
25
|
monitor: 'Runtime Monitor',
|
|
26
|
+
plugins: 'Plugins',
|
|
26
27
|
settings: 'Settings',
|
|
27
28
|
},
|
|
28
29
|
header: {
|
|
@@ -1730,6 +1731,22 @@ export default {
|
|
|
1730
1731
|
kvzhi95004: '{p0}✔ HITL 已应答:{p1}{p2}',
|
|
1731
1732
|
ke67kwf005: '{p0}── 切换目标 · {\'$\'}{\'{\'}props.pid ? ',
|
|
1732
1733
|
},
|
|
1734
|
+
plugins: {
|
|
1735
|
+
title: 'Plugins',
|
|
1736
|
+
subtitle: 'Config-root plugins/ · toggles hot-reload instantly',
|
|
1737
|
+
refresh: 'Refresh',
|
|
1738
|
+
enabled: 'Enabled',
|
|
1739
|
+
disabled: 'Disabled',
|
|
1740
|
+
scopeProject: 'Project',
|
|
1741
|
+
scopeUser: 'User',
|
|
1742
|
+
hasClient: 'Client enhancer',
|
|
1743
|
+
routes: 'API routes',
|
|
1744
|
+
empty: 'No plugins yet — aw plugin create <name>, or aw plugin enable to turn on examples',
|
|
1745
|
+
failures: 'Load failures',
|
|
1746
|
+
loadFailures: '{n} plugin(s) failed to load, see server startup log',
|
|
1747
|
+
enableFail: 'Toggle failed',
|
|
1748
|
+
detail: 'Plugin detail',
|
|
1749
|
+
},
|
|
1733
1750
|
error: {
|
|
1734
1751
|
backHome: 'Back to Home',
|
|
1735
1752
|
lost: 'Page lost',
|
package/i18n/locales/zh-CN.ts
CHANGED
|
@@ -23,6 +23,7 @@ export default {
|
|
|
23
23
|
logs: '日志管理',
|
|
24
24
|
users: '用户管理',
|
|
25
25
|
monitor: '运行时监控',
|
|
26
|
+
plugins: '插件管理',
|
|
26
27
|
settings: '系统设置',
|
|
27
28
|
},
|
|
28
29
|
header: {
|
|
@@ -1729,6 +1730,22 @@ export default {
|
|
|
1729
1730
|
kvzhi95004: '{p0}✔ HITL 已应答:{p1}{p2}',
|
|
1730
1731
|
ke67kwf005: '{p0}── 切换目标 · {\'$\'}{\'{\'}props.pid ? ',
|
|
1731
1732
|
},
|
|
1733
|
+
plugins: {
|
|
1734
|
+
title: '插件管理',
|
|
1735
|
+
subtitle: '配置根 plugins/ 目录 · 启停即时热重载',
|
|
1736
|
+
refresh: '刷新',
|
|
1737
|
+
enabled: '已启用',
|
|
1738
|
+
disabled: '已停用',
|
|
1739
|
+
scopeProject: '项目级',
|
|
1740
|
+
scopeUser: '用户级',
|
|
1741
|
+
hasClient: '浏览器增强',
|
|
1742
|
+
routes: 'API 路由',
|
|
1743
|
+
empty: '暂无插件 —— aw plugin create <name> 创建,或 aw plugin enable 开启示例',
|
|
1744
|
+
failures: '装载失败',
|
|
1745
|
+
loadFailures: '{n} 个插件装载失败,详情见服务启动日志',
|
|
1746
|
+
enableFail: '启停操作失败',
|
|
1747
|
+
detail: '插件详情',
|
|
1748
|
+
},
|
|
1732
1749
|
error: {
|
|
1733
1750
|
backHome: '返回首页',
|
|
1734
1751
|
lost: '页面走失了',
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentworkshop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgentWorkShop 软件开发系统 - 基于 Nuxt 4 的配置驱动运行范式",
|
|
6
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
6
7
|
"exports": {
|
|
7
8
|
".": {
|
|
8
9
|
"types": "./sdk/index.d.mts",
|
|
@@ -41,7 +42,8 @@
|
|
|
41
42
|
".env.example",
|
|
42
43
|
"README-zh.md",
|
|
43
44
|
"docs/cli.md",
|
|
44
|
-
"docs/plugins.md"
|
|
45
|
+
"docs/plugins.md",
|
|
46
|
+
"docs/sdk.md"
|
|
45
47
|
],
|
|
46
48
|
"packageManager": "pnpm@11.9.0",
|
|
47
49
|
"engines": {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文档站截图走查 — vitepress preview(127.0.0.1:4477)+ Edge + puppeteer-core。
|
|
3
|
+
* 用法:node scripts/_dbg-docs-site-shot.mjs
|
|
4
|
+
*/
|
|
5
|
+
import puppeteer from 'puppeteer-core'
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
|
|
8
|
+
const EDGE = 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'
|
|
9
|
+
const BASE = 'http://127.0.0.1:4477/AgentWorkShop'
|
|
10
|
+
const OUT = 'gui-test-screenshots/docs-site-v4'
|
|
11
|
+
|
|
12
|
+
const PAGES = [
|
|
13
|
+
{ name: "guide-getting-started", path: '/', width: 1440 },
|
|
14
|
+
{ name: 'guide-getting-started', path: '/guide/getting-started', width: 1440 },
|
|
15
|
+
{ name: 'license', path: '/guide/license', width: 1440 },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const main = async () => {
|
|
19
|
+
fs.mkdirSync(OUT, { recursive: true })
|
|
20
|
+
const browser = await puppeteer.launch({ executablePath: EDGE, headless: 'new' })
|
|
21
|
+
for (const p of PAGES) {
|
|
22
|
+
const page = await browser.newPage()
|
|
23
|
+
await page.setViewport({ width: p.width, height: 900, deviceScaleFactor: 1 })
|
|
24
|
+
await page.goto(`${BASE}${p.path}`, { waitUntil: 'networkidle0', timeout: 30000 })
|
|
25
|
+
await new Promise(r => setTimeout(r, 600))
|
|
26
|
+
await page.screenshot({ path: `${OUT}/${p.name}.png`, fullPage: true })
|
|
27
|
+
console.log('shot', p.name)
|
|
28
|
+
await page.close()
|
|
29
|
+
}
|
|
30
|
+
await browser.close()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main().catch(e => { console.error(e); process.exit(1) })
|