agentworkshop 0.7.14 → 0.7.15
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/.output/nitro.json +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/65f6bed9-b794-44a4-805a-e22700c10817.json +1 -0
- package/.output/server/chunks/_/nitro.mjs +697 -697
- package/.output/server/package.json +1 -1
- package/package.json +1 -1
- package/scripts/_dbg-harness-ui-e2e.py +23 -13
- package/scripts/aw-expose.mjs +159 -0
- package/server/data/dcw-rollback.json +6 -6
- package/server/data/dcw-writes.json +24 -0
- package/server/data/dcws.json +198 -198
- package/.output/public/_nuxt/builds/meta/23c30854-f9e1-4651-bc3a-b695ef5faca4.json +0 -1
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Harness 可用性 UI E2E:用户名登录 → 仪表盘 Harness 面板(dsh=未安装灰化) → agents 模板选择禁用 → teams 页截图。
|
|
2
|
-
dsh 未安装态由外部脚本预置;恢复也由外部执行(本脚本不发起 HTTP)。
|
|
3
|
-
|
|
2
|
+
dsh 未安装态由外部脚本预置;恢复也由外部执行(本脚本不发起 HTTP)。
|
|
3
|
+
AW_BASE 环境变量切换目标实例(默认 :3021);EXPECT_OFF=1 时断言恰好 1 条灰化。"""
|
|
4
|
+
import asyncio, os, sys
|
|
4
5
|
from playwright.async_api import async_playwright
|
|
5
6
|
|
|
6
|
-
BASE = 'http://127.0.0.1:3021'
|
|
7
|
+
BASE = os.environ.get('AW_BASE', 'http://127.0.0.1:3021')
|
|
8
|
+
EXPECT_OFF = os.environ.get('EXPECT_OFF', '1') == '1'
|
|
7
9
|
SHOT = '.e2e-shots'
|
|
8
10
|
|
|
9
11
|
async def main():
|
|
@@ -49,12 +51,17 @@ async def main():
|
|
|
49
51
|
ok(items == 6, f'仪表盘 6 引擎条目(实际 {items})')
|
|
50
52
|
off = page.locator('.harness-row .h-item.off')
|
|
51
53
|
off_n = await off.count()
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
if EXPECT_OFF:
|
|
55
|
+
ok(off_n == 1, f'未安装灰化条目=1(实际 {off_n})')
|
|
56
|
+
if off_n == 1:
|
|
57
|
+
txt = await off.first.inner_text()
|
|
58
|
+
ok('未安装' in txt and 'dsh' in txt, f'灰化条目为 dsh:{txt!r}')
|
|
59
|
+
total = await page.locator('.dp-hd:has-text("执行引擎") .fleet-total').inner_text()
|
|
60
|
+
ok('5/6' in total, f'就绪计数 5/6(实际 {total!r})')
|
|
61
|
+
else:
|
|
62
|
+
print(f' · 灰化条目={off_n}(当前环境探测结果;全部安装时为 0)', flush=True)
|
|
63
|
+
total = await page.locator('.dp-hd:has-text("执行引擎") .fleet-total').inner_text()
|
|
64
|
+
print(f' · 就绪计数={total!r}', flush=True)
|
|
58
65
|
await page.screenshot(path=f'{SHOT}/harness-1-dashboard-off.png', full_page=False)
|
|
59
66
|
|
|
60
67
|
# ---- 3. agents 模板页:选择器禁用未安装项 ----
|
|
@@ -64,12 +71,15 @@ async def main():
|
|
|
64
71
|
await page.click('.ant-modal .ant-select')
|
|
65
72
|
await page.wait_for_selector('.ant-select-dropdown .ant-select-item-option', timeout=10000)
|
|
66
73
|
opts = await page.locator('.ant-select-dropdown .ant-select-item-option').all_inner_texts()
|
|
67
|
-
ok(any('未安装' in o for o in opts), f'下拉含(未安装)标记:{[o for o in opts if "未安装" in o]!r}')
|
|
68
74
|
disabled = page.locator('.ant-select-dropdown .ant-select-item-option-disabled')
|
|
69
75
|
dis_n = await disabled.count()
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
ok(
|
|
76
|
+
if EXPECT_OFF:
|
|
77
|
+
ok(any('未安装' in o for o in opts), f'下拉含(未安装)标记:{[o for o in opts if "未安装" in o]!r}')
|
|
78
|
+
ok(dis_n == 1, f'禁用选项=1(实际 {dis_n})')
|
|
79
|
+
if dis_n == 1:
|
|
80
|
+
ok('dsh' in (await disabled.first.inner_text()), '禁用项为 dsh')
|
|
81
|
+
else:
|
|
82
|
+
print(f' · 下拉选项={len(opts)},禁用={dis_n}(全部安装时为 0)', flush=True)
|
|
73
83
|
await page.screenshot(path=f'{SHOT}/harness-2-agent-select-disabled.png', full_page=False)
|
|
74
84
|
await page.keyboard.press('Escape')
|
|
75
85
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aw-expose —— 把本机 AgentWorkShop 通过 Cloudflare Tunnel 映射到公网。
|
|
4
|
+
*
|
|
5
|
+
* 用法(在本机已能访问 Web UI 的前提下):
|
|
6
|
+
* node scripts/aw-expose.mjs # 自动探测本机实例(默认端口 3001/顺延)
|
|
7
|
+
* node scripts/aw-expose.mjs --port 3002 # 指定本地端口
|
|
8
|
+
* node scripts/aw-expose.mjs --token <TUNNEL_TOKEN> # 具名隧道(绑定自己的域名;token 来自
|
|
9
|
+
* # Cloudflare Zero Trust → Networks → Tunnels → Create tunnel 的安装令牌,
|
|
10
|
+
* # 也可用环境变量 CLOUDFLARE_TUNNEL_TOKEN 传入,避免进入 shell 历史)
|
|
11
|
+
*
|
|
12
|
+
* 两种模式:
|
|
13
|
+
* - 快速隧道(默认,免账号):分配 https://<随机>.trycloudflare.com 公网地址,每次启动会变;
|
|
14
|
+
* - 具名隧道(--token):固定绑定你 Cloudflare 账号里的域名,地址长期不变,生产推荐。
|
|
15
|
+
*
|
|
16
|
+
* 行为:
|
|
17
|
+
* 1) 定位 cloudflared(PATH → Windows 默认安装位 → ~/.AgentWorkShop/bin);
|
|
18
|
+
* 2) 探活 http://127.0.0.1:<port>/api/health,实例未启动则给出 `aw start` 提示(--start 自动拉起);
|
|
19
|
+
* 3) 拉起隧道进程,从输出解析公网 URL 并打印;
|
|
20
|
+
* 4) 常驻保活(cloudflared 崩溃自动重启,10s 退避);Ctrl+C 一并收尾本地实例(--start 模式)与隧道。
|
|
21
|
+
*
|
|
22
|
+
* 安全提示:公网地址任何人可访问,登录门/权限体系是唯一边界;建议用具名隧道 +
|
|
23
|
+
* Cloudflare Access(Zero Trust)做外层鉴权后再长期公开。
|
|
24
|
+
*/
|
|
25
|
+
import { spawn } from 'node:child_process'
|
|
26
|
+
import { existsSync } from 'node:fs'
|
|
27
|
+
import { homedir, platform } from 'node:os'
|
|
28
|
+
import { join } from 'node:path'
|
|
29
|
+
|
|
30
|
+
// ---------- 参数 ----------
|
|
31
|
+
const argv = process.argv.slice(2)
|
|
32
|
+
const argOf = (name) => {
|
|
33
|
+
const i = argv.indexOf(name)
|
|
34
|
+
return i >= 0 && argv[i + 1] ? argv[i + 1] : undefined
|
|
35
|
+
}
|
|
36
|
+
const hasFlag = name => argv.includes(name)
|
|
37
|
+
const PORT = argOf('--port') ?? '3001'
|
|
38
|
+
const LOCAL = `http://127.0.0.1:${PORT}`
|
|
39
|
+
const TUNNEL_TOKEN = argOf('--token') ?? process.env.CLOUDFLARE_TUNNEL_TOKEN ?? ''
|
|
40
|
+
const WANT_START = hasFlag('--start')
|
|
41
|
+
const IS_WIN = platform() === 'win32'
|
|
42
|
+
const BIN = join(homedir(), '.AgentWorkShop', 'bin')
|
|
43
|
+
|
|
44
|
+
const say = msg => console.log(`[aw-expose] ${msg}`)
|
|
45
|
+
const die = (msg) => {
|
|
46
|
+
say(`✖ ${msg}`)
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------- 定位 cloudflared ----------
|
|
51
|
+
function findCloudflared() {
|
|
52
|
+
const candidates = [
|
|
53
|
+
argOf('--cloudflared'),
|
|
54
|
+
join(BIN, IS_WIN ? 'cloudflared.exe' : 'cloudflared'),
|
|
55
|
+
...(IS_WIN ? [join(process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)', 'cloudflared', 'cloudflared.exe')] : ['/usr/local/bin/cloudflared', '/usr/bin/cloudflared']),
|
|
56
|
+
]
|
|
57
|
+
for (const c of candidates) {
|
|
58
|
+
if (c && existsSync(c)) return c
|
|
59
|
+
}
|
|
60
|
+
return null // 最后交给 PATH
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function healthy() {
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(`${LOCAL}/api/health`, { signal: AbortSignal.timeout(3000) })
|
|
66
|
+
return res.ok
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function waitForHealthy(timeoutMs) {
|
|
74
|
+
const deadline = Date.now() + timeoutMs
|
|
75
|
+
while (Date.now() < deadline) {
|
|
76
|
+
if (await healthy()) return true
|
|
77
|
+
await new Promise(r => setTimeout(r, 1500))
|
|
78
|
+
}
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------- 主流程 ----------
|
|
83
|
+
const cloudflared = findCloudflared()
|
|
84
|
+
if (!cloudflared) {
|
|
85
|
+
die(`未找到 cloudflared。安装方式任选其一:
|
|
86
|
+
winget install --id Cloudflare.cloudflared
|
|
87
|
+
# 或下载到 ~/.AgentWorkShop/bin/cloudflared.exe
|
|
88
|
+
https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/`)
|
|
89
|
+
}
|
|
90
|
+
say(`cloudflared: ${cloudflared}`)
|
|
91
|
+
say(`本地实例: ${LOCAL}`)
|
|
92
|
+
|
|
93
|
+
if (!(await healthy())) {
|
|
94
|
+
if (!WANT_START) {
|
|
95
|
+
die(`本地实例未就绪。请先启动(或加 --start 由本脚本拉起):
|
|
96
|
+
aw start # 生产模式(默认端口 3001,占用时自动顺延)
|
|
97
|
+
aw start --port ${PORT}`)
|
|
98
|
+
}
|
|
99
|
+
say('本地实例未就绪,--start 模式:拉起 aw start …')
|
|
100
|
+
const appRoot = process.env.AW_PACKAGE_ROOT ?? process.cwd()
|
|
101
|
+
const awBin = join(appRoot, 'bin', 'aw.mjs')
|
|
102
|
+
if (!existsSync(awBin)) die(`未找到 ${awBin};请在项目/包目录内运行,或用 --port 指向已运行实例`)
|
|
103
|
+
const app = spawn(process.execPath, [awBin, 'start', '--port', PORT], {
|
|
104
|
+
cwd: appRoot,
|
|
105
|
+
stdio: 'ignore',
|
|
106
|
+
detached: false,
|
|
107
|
+
shell: false,
|
|
108
|
+
env: { ...process.env },
|
|
109
|
+
})
|
|
110
|
+
app.on('exit', (code) => {
|
|
111
|
+
if (code !== 0 && code !== null) say(`aw start 退出(code=${code})`)
|
|
112
|
+
})
|
|
113
|
+
say('等待本地实例就绪(最长 120s)…')
|
|
114
|
+
if (!(await waitForHealthy(120_000))) die('本地实例 120s 内未就绪,放弃(可手工 aw start 后重试)')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------- 隧道进程(崩溃自动重启) ----------
|
|
118
|
+
let quitting = false
|
|
119
|
+
let child = null
|
|
120
|
+
let currentUrl = ''
|
|
121
|
+
|
|
122
|
+
function startTunnel() {
|
|
123
|
+
const args = TUNNEL_TOKEN
|
|
124
|
+
? ['tunnel', '--no-autoupdate', 'run', '--token', TUNNEL_TOKEN]
|
|
125
|
+
: ['tunnel', '--no-autoupdate', '--url', LOCAL]
|
|
126
|
+
child = spawn(cloudflared, args, { stdio: ['ignore', 'pipe', 'pipe'], shell: false })
|
|
127
|
+
const scan = (buf) => {
|
|
128
|
+
for (const line of String(buf).split(/\r?\n/)) {
|
|
129
|
+
const m = line.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i)
|
|
130
|
+
if (m && m[0] !== currentUrl) {
|
|
131
|
+
currentUrl = m[0]
|
|
132
|
+
say('公网地址(快速隧道,重启会变):')
|
|
133
|
+
console.log(`\n ${currentUrl}\n`)
|
|
134
|
+
say('浏览器打开即为本机 AgentWorkShop;Ctrl+C 结束映射。')
|
|
135
|
+
}
|
|
136
|
+
if (/Registered tunnel connection|connect.*registered/i.test(line) && TUNNEL_TOKEN) {
|
|
137
|
+
say('具名隧道已连接(域名以 Cloudflare Tunnels 配置为准)。')
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
child.stdout.on('data', scan)
|
|
142
|
+
child.stderr.on('data', scan)
|
|
143
|
+
child.on('exit', (code) => {
|
|
144
|
+
if (quitting) return
|
|
145
|
+
say(`隧道进程退出(code=${code}),10s 后重启…`)
|
|
146
|
+
setTimeout(startTunnel, 10_000)
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
startTunnel()
|
|
151
|
+
|
|
152
|
+
const cleanup = () => {
|
|
153
|
+
quitting = true
|
|
154
|
+
say('正在结束隧道…')
|
|
155
|
+
if (child) child.kill(IS_WIN ? undefined : 'SIGTERM')
|
|
156
|
+
process.exit(0)
|
|
157
|
+
}
|
|
158
|
+
process.on('SIGINT', cleanup)
|
|
159
|
+
process.on('SIGTERM', cleanup)
|
|
@@ -5120,7 +5120,7 @@
|
|
|
5120
5120
|
"channels": [],
|
|
5121
5121
|
"degraded": true
|
|
5122
5122
|
},
|
|
5123
|
-
"evaluatedAt": "2026-09-06T19:
|
|
5123
|
+
"evaluatedAt": "2026-09-06T19:44:49.775+08:00",
|
|
5124
5124
|
"windowAgg": {
|
|
5125
5125
|
"at": "2026-08-31T16:25:17.398Z",
|
|
5126
5126
|
"fromMs": 1788193396897,
|
|
@@ -11569,7 +11569,7 @@
|
|
|
11569
11569
|
}
|
|
11570
11570
|
]
|
|
11571
11571
|
},
|
|
11572
|
-
"evaluatedAt": "2026-09-06T19:
|
|
11572
|
+
"evaluatedAt": "2026-09-06T19:44:49.775+08:00",
|
|
11573
11573
|
"windowAgg": {
|
|
11574
11574
|
"at": "2026-09-05T02:07:03.383Z",
|
|
11575
11575
|
"fromMs": 1788573903195,
|
|
@@ -11630,7 +11630,7 @@
|
|
|
11630
11630
|
}
|
|
11631
11631
|
]
|
|
11632
11632
|
},
|
|
11633
|
-
"evaluatedAt": "2026-09-06T19:
|
|
11633
|
+
"evaluatedAt": "2026-09-06T19:44:49.775+08:00",
|
|
11634
11634
|
"windowAgg": {
|
|
11635
11635
|
"at": "2026-09-05T02:44:52.815Z",
|
|
11636
11636
|
"fromMs": 1788576172595,
|
|
@@ -11690,7 +11690,7 @@
|
|
|
11690
11690
|
}
|
|
11691
11691
|
]
|
|
11692
11692
|
},
|
|
11693
|
-
"evaluatedAt": "2026-09-06T19:
|
|
11693
|
+
"evaluatedAt": "2026-09-06T19:44:49.775+08:00",
|
|
11694
11694
|
"windowAgg": {
|
|
11695
11695
|
"at": "2026-09-05T03:09:49.742Z",
|
|
11696
11696
|
"fromMs": 1788577669697,
|
|
@@ -11750,7 +11750,7 @@
|
|
|
11750
11750
|
}
|
|
11751
11751
|
]
|
|
11752
11752
|
},
|
|
11753
|
-
"evaluatedAt": "2026-09-06T19:
|
|
11753
|
+
"evaluatedAt": "2026-09-06T19:44:49.775+08:00",
|
|
11754
11754
|
"windowAgg": {
|
|
11755
11755
|
"at": "2026-09-05T03:34:32.268Z",
|
|
11756
11756
|
"fromMs": 1788579151974,
|
|
@@ -11799,7 +11799,7 @@
|
|
|
11799
11799
|
"channels": [],
|
|
11800
11800
|
"degraded": true
|
|
11801
11801
|
},
|
|
11802
|
-
"evaluatedAt": "2026-09-06T19:
|
|
11802
|
+
"evaluatedAt": "2026-09-06T19:44:49.775+08:00",
|
|
11803
11803
|
"windowAgg": {
|
|
11804
11804
|
"at": "2026-09-05T09:01:07.414Z",
|
|
11805
11805
|
"fromMs": 1788598747074,
|
|
@@ -12766,5 +12766,29 @@
|
|
|
12766
12766
|
"message": "Mock PLC 写入成功:170 → raw 170,回读一致(标定后物理值 170)",
|
|
12767
12767
|
"recipeRunId": null,
|
|
12768
12768
|
"at": "2026-09-06T13:46:26.226+08:00"
|
|
12769
|
+
},
|
|
12770
|
+
{
|
|
12771
|
+
"id": "wh-85ff5529",
|
|
12772
|
+
"nodeId": "dw-d341d62e",
|
|
12773
|
+
"nodeName": "温度设定器·2号线(示例)",
|
|
12774
|
+
"param": "烘箱温度设定",
|
|
12775
|
+
"eng": 170,
|
|
12776
|
+
"raw": 170,
|
|
12777
|
+
"ok": true,
|
|
12778
|
+
"message": "Mock PLC 写入成功:170 → raw 170,回读一致(标定后物理值 170)",
|
|
12779
|
+
"recipeRunId": null,
|
|
12780
|
+
"at": "2026-09-06T19:24:52.674+08:00"
|
|
12781
|
+
},
|
|
12782
|
+
{
|
|
12783
|
+
"id": "wh-caa8f07a",
|
|
12784
|
+
"nodeId": "dw-d341d62e",
|
|
12785
|
+
"nodeName": "温度设定器·2号线(示例)",
|
|
12786
|
+
"param": "烘箱温度设定",
|
|
12787
|
+
"eng": 170,
|
|
12788
|
+
"raw": 170,
|
|
12789
|
+
"ok": true,
|
|
12790
|
+
"message": "Mock PLC 写入成功:170 → raw 170,回读一致(标定后物理值 170)",
|
|
12791
|
+
"recipeRunId": null,
|
|
12792
|
+
"at": "2026-09-06T19:25:01.709+08:00"
|
|
12769
12793
|
}
|
|
12770
12794
|
]
|