@sybz-components/portal-dev 1.0.9 → 1.0.11
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/README.md +78 -38
- package/bin/portal-dev.mjs +38 -4
- package/package.json +3 -2
- package/skills/portal-dev/SKILL.md +10 -9
- package/src/config-file.mjs +34 -39
- package/src/configure.mjs +25 -48
- package/src/macos-chrome.mjs +49 -8
- package/src/portal-dev.mjs +103 -21
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
macOS 会直接复用当前打开的 Google Chrome,在现有窗口中新建标签页完成登录和本地联调,不会启动独立的 Chrome 实例。Windows / Linux 使用独立浏览器实例作为兼容方案。
|
|
11
11
|
|
|
12
|
+
Windows 会优先读取 `CHROME_PATH`,然后自动查找 Chrome 和 Microsoft Edge 的常见安装位置。配置文件优先使用 `%APPDATA%`;自动启动前端项目时通过 `cmd.exe` 运行 npm、pnpm 或 Bun,结束时会清理对应的进程树。
|
|
13
|
+
|
|
12
14
|
## 安装
|
|
13
15
|
|
|
14
16
|
全局安装:
|
|
@@ -50,6 +52,39 @@ portal-dev config --portal custom
|
|
|
50
52
|
|
|
51
53
|
## 使用
|
|
52
54
|
|
|
55
|
+
列出所有已配置的石景山、成华和自定义网站账号,输入序号后直接登录:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
portal-dev login
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
示例:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
请选择要登录的账号:
|
|
65
|
+
1. 石景山 - 测试账号
|
|
66
|
+
2. 成华 - 管理员
|
|
67
|
+
3. 自定义网站 - 内部系统
|
|
68
|
+
请输入序号(1-3):
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 本地调试
|
|
72
|
+
|
|
73
|
+
修改 `portal-dev` 源码后不需要发布或全局安装,在本包目录直接运行:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pnpm dev sjs
|
|
77
|
+
pnpm dev login
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
该命令直接执行当前源码,效果与发布后的 `portal-dev sjs` 一致。其他参数也可以原样传入,例如:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pnpm dev --portal sjs
|
|
84
|
+
pnpm dev config --portal sjs
|
|
85
|
+
pnpm dev --help
|
|
86
|
+
```
|
|
87
|
+
|
|
53
88
|
只登录石景山门户,不读取或启动任何前端项目:
|
|
54
89
|
|
|
55
90
|
```bash
|
|
@@ -74,13 +109,6 @@ portal-dev --portal sjs
|
|
|
74
109
|
portal-dev --portal sjs 测试账号
|
|
75
110
|
```
|
|
76
111
|
|
|
77
|
-
默认快捷命令:
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
portal-dev ds # 登录石景山
|
|
81
|
-
portal-dev sc # 登录成华
|
|
82
|
-
```
|
|
83
|
-
|
|
84
112
|
配置并登录普通网站:
|
|
85
113
|
|
|
86
114
|
```bash
|
|
@@ -90,51 +118,63 @@ portal-dev --portal custom 我的账号
|
|
|
90
118
|
|
|
91
119
|
配置时依次输入登录页 URL、用户名和密码。登录时会自动查找常见的用户名、邮箱、密码输入框,以及“登录”、“Login”或“Sign in”按钮;该模式不会执行验证码识别、样板间搜索或 Token 跳转。
|
|
92
120
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
```bash
|
|
96
|
-
portal-dev config command
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
例如配置一个别名为 `sjs-dev` 的联调命令后,只需运行:
|
|
121
|
+
执行 `portal-dev config` 时,可以直接为账号设置快捷别名。石景山账号还可选择“只登录”或“本地联调”模式。例如将某个账号的别名配置为 `sjs-dev`:
|
|
100
122
|
|
|
101
123
|
```bash
|
|
124
|
+
portal-dev config --portal sjs
|
|
102
125
|
portal-dev sjs-dev
|
|
103
126
|
```
|
|
104
127
|
|
|
105
|
-
|
|
128
|
+
执行时会登录指定账号、在已配置 `project` 时启动本地项目、进入样板间搜索智能体,从匹配的 iframe 地址取得 Token,然后保留查询参数打开本地页面。
|
|
129
|
+
|
|
130
|
+
`project` 是可选项,只在需要 CLI 自动启动前端开发服务时配置。不配置时不会启动项目,联调流程仍使用默认本地地址 `http://localhost:5173`。
|
|
106
131
|
|
|
107
|
-
|
|
132
|
+
配置文件中,别名、模式和联调参数都直接属于对应账号,不再有独立的 `commands`:
|
|
108
133
|
|
|
109
134
|
```json
|
|
110
135
|
{
|
|
111
|
-
"version":
|
|
136
|
+
"version": 5,
|
|
112
137
|
"profiles": {
|
|
113
|
-
"sjs": [
|
|
114
|
-
|
|
138
|
+
"sjs": [
|
|
139
|
+
{
|
|
140
|
+
"name": "测试账号",
|
|
141
|
+
"username": "user1",
|
|
142
|
+
"password": "******",
|
|
143
|
+
"alias": "sjs-dev",
|
|
144
|
+
"mode": "dev",
|
|
145
|
+
"project": "/Users/你的名字/projects/agent-web",
|
|
146
|
+
"localOrigin": "http://localhost:5173",
|
|
147
|
+
"localPath": "/exhibition-hall",
|
|
148
|
+
"roomName": "3D智能展厅智能体",
|
|
149
|
+
"iframeHost": "hia.sjsdoubao.com:31118",
|
|
150
|
+
"iframePath": "/exhibition-hall"
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
"chenghua": [
|
|
154
|
+
{
|
|
155
|
+
"name": "成华账号",
|
|
156
|
+
"username": "user2",
|
|
157
|
+
"password": "******",
|
|
158
|
+
"alias": "ch",
|
|
159
|
+
"mode": "login"
|
|
160
|
+
}
|
|
161
|
+
],
|
|
115
162
|
"custom": [
|
|
116
|
-
{
|
|
163
|
+
{
|
|
164
|
+
"name": "内部系统",
|
|
165
|
+
"loginUrl": "https://example.com/login",
|
|
166
|
+
"username": "user3",
|
|
167
|
+
"password": "******",
|
|
168
|
+
"alias": "internal",
|
|
169
|
+
"mode": "login"
|
|
170
|
+
}
|
|
117
171
|
]
|
|
118
|
-
}
|
|
119
|
-
"commands": [
|
|
120
|
-
{ "alias": "ds", "mode": "login", "portal": "sjs" },
|
|
121
|
-
{ "alias": "sc", "mode": "login", "portal": "chenghua" },
|
|
122
|
-
{
|
|
123
|
-
"alias": "sjs-dev",
|
|
124
|
-
"mode": "dev",
|
|
125
|
-
"portal": "sjs",
|
|
126
|
-
"account": "测试账号",
|
|
127
|
-
"project": "/Users/你的名字/projects/agent-web",
|
|
128
|
-
"localOrigin": "http://localhost:5173",
|
|
129
|
-
"localPath": "/exhibition-hall",
|
|
130
|
-
"roomName": "3D智能展厅智能体",
|
|
131
|
-
"iframeHost": "hia.sjsdoubao.com:31118",
|
|
132
|
-
"iframePath": "/exhibition-hall"
|
|
133
|
-
}
|
|
134
|
-
]
|
|
172
|
+
}
|
|
135
173
|
}
|
|
136
174
|
```
|
|
137
175
|
|
|
176
|
+
首次读取旧版 `version: 4` 配置时,CLI 会自动将 `commands` 合并到对应 profile 并升级为 `version: 5`。
|
|
177
|
+
|
|
138
178
|
查看命令帮助:
|
|
139
179
|
|
|
140
180
|
```bash
|
|
@@ -178,4 +218,4 @@ pnpm portal:skill:install
|
|
|
178
218
|
pnpm portal:release
|
|
179
219
|
```
|
|
180
220
|
|
|
181
|
-
`portal:release`
|
|
221
|
+
`portal:release` 会检查工作区和 npm 登录状态、升级 patch 版本并公开发布到 npm。只有确认新版本能从 npm registry 查询到之后,才会创建版本提交和 `portal-dev-v<版本号>` Git tag,并将当前分支及 tags 推送到 Git 远端。发布失败时会恢复本地版本号,避免留下无法安装的空版本。执行前请先提交当前改动,并确认当前分支和远端正确。
|
package/bin/portal-dev.mjs
CHANGED
|
@@ -2,10 +2,44 @@
|
|
|
2
2
|
import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs'
|
|
3
3
|
import { spawn } from 'node:child_process'
|
|
4
4
|
import { dirname, resolve } from 'node:path'
|
|
5
|
+
import { createInterface } from 'node:readline/promises'
|
|
5
6
|
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { readPortalConfig } from '../src/config-file.mjs'
|
|
6
8
|
|
|
7
9
|
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
8
|
-
|
|
10
|
+
let args = process.argv.slice(2)
|
|
11
|
+
|
|
12
|
+
if (args[0] === 'login') {
|
|
13
|
+
const portalNames = { sjs: '石景山', chenghua: '成华', custom: '自定义网站' }
|
|
14
|
+
const { profiles } = readPortalConfig()
|
|
15
|
+
const options = Object.keys(portalNames).flatMap((portal) =>
|
|
16
|
+
(Array.isArray(profiles[portal]) ? profiles[portal] : [])
|
|
17
|
+
.filter(
|
|
18
|
+
(account) =>
|
|
19
|
+
account?.name &&
|
|
20
|
+
typeof account.username === 'string' &&
|
|
21
|
+
typeof account.password === 'string' &&
|
|
22
|
+
(portal !== 'custom' || account.loginUrl),
|
|
23
|
+
)
|
|
24
|
+
.map((account) => ({ portal, account })),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if (!options.length) throw new Error('没有可登录的已配置账号,请先运行 portal-dev config')
|
|
28
|
+
|
|
29
|
+
console.log('请选择要登录的账号:')
|
|
30
|
+
options.forEach(({ portal, account }, index) => {
|
|
31
|
+
console.log(` ${index + 1}. ${portalNames[portal]} - ${account.name}`)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout })
|
|
35
|
+
const answer = (await readline.question(`请输入序号(1-${options.length}):`)).trim()
|
|
36
|
+
readline.close()
|
|
37
|
+
const selectedIndex = Number(answer) - 1
|
|
38
|
+
if (!Number.isInteger(selectedIndex) || !options[selectedIndex]) throw new Error(`无效的选项:${answer || '(空)'}`)
|
|
39
|
+
|
|
40
|
+
const selected = options[selectedIndex]
|
|
41
|
+
args = ['--profile', selected.account.name, '--portal', selected.portal]
|
|
42
|
+
}
|
|
9
43
|
|
|
10
44
|
if (args[0] === 'skill' && args[1] === 'install') {
|
|
11
45
|
const homeDir = process.env.HOME || process.env.USERPROFILE
|
|
@@ -34,15 +68,15 @@ if (args[0] === 'config') {
|
|
|
34
68
|
} else if (args.includes('--help') || args.includes('-h')) {
|
|
35
69
|
console.log(`用法:
|
|
36
70
|
portal-dev
|
|
71
|
+
portal-dev login
|
|
37
72
|
portal-dev config [--portal sjs|chenghua|custom]
|
|
38
|
-
portal-dev config command
|
|
39
73
|
portal-dev <快捷命令别名>
|
|
40
74
|
portal-dev --portal sjs|chenghua|custom [账号名称]
|
|
41
75
|
portal-dev skill install [Codex skills 目录]
|
|
42
76
|
|
|
43
77
|
首次使用只需运行一次 portal-dev config。
|
|
44
|
-
|
|
45
|
-
|
|
78
|
+
运行 portal-dev login 可列出所有已配置账号,输入序号直接登录。
|
|
79
|
+
每个账号都可在 portal-dev config 时配置别名和登录/联调模式。`)
|
|
46
80
|
process.exit(0)
|
|
47
81
|
} else {
|
|
48
82
|
const child = spawn(process.execPath, [resolve(packageDir, 'src/portal-dev.mjs'), ...args], {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sybz-components/portal-dev",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "成华和石景山门户自动登录与本地前端联调 CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
"registry": "https://registry.npmjs.org"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
+
"dev": "node ./bin/portal-dev.mjs",
|
|
23
24
|
"skill:install": "node ./bin/portal-dev.mjs skill install",
|
|
24
25
|
"check": "node --check bin/portal-dev.mjs && node --check src/config-file.mjs && node --check src/configure.mjs && node --check src/macos-chrome.mjs && node --check src/portal-dev.mjs && node --check src/recognize-captcha.mjs",
|
|
25
|
-
"release": "
|
|
26
|
+
"release": "node ./scripts/release.mjs"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
29
|
"@napi-rs/canvas": "^1.0.3",
|
|
@@ -11,6 +11,14 @@ macOS 会复用用户当前打开的 Google Chrome,并在现有窗口的标签
|
|
|
11
11
|
|
|
12
12
|
## 执行
|
|
13
13
|
|
|
14
|
+
需要让用户从所有已配置账号中选择时执行:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
portal-dev login
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
该命令会显示石景山、成华和自定义网站的可登录账号,输入序号后直接登录。
|
|
21
|
+
|
|
14
22
|
缺少账号配置时先执行:
|
|
15
23
|
|
|
16
24
|
```bash
|
|
@@ -35,17 +43,10 @@ portal-dev --portal chenghua
|
|
|
35
43
|
portal-dev --portal custom <账号名称>
|
|
36
44
|
```
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
portal-dev ds
|
|
42
|
-
portal-dev sc
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
只有用户明确要求门户本地联调时才执行 JSON 中 `mode: "dev"` 的快捷命令,例如 `portal-dev sjs-dev`。项目、本地地址、路由、智能体名称和 iframe 识别参数均从该命令配置读取。脚本从门户 iframe URL 取得 Token 后,必须保留查询参数打开本地地址,且不得输出 Token。
|
|
46
|
+
每个 profile 可以直接配置 `alias` 和 `mode`。只有用户明确要求门户本地联调时才执行 `mode: "dev"` 的快捷别名,例如 `portal-dev sjs-dev`。`project` 仅用于需要 CLI 自动启动前端项目的情况,未配置时直接使用默认本地地址 `http://localhost:5173`。本地地址、路由、智能体名称和 iframe 识别参数均从该 profile 读取。脚本从门户 iframe URL 取得 Token 后,必须保留查询参数打开本地地址,且不得输出 Token。
|
|
46
47
|
|
|
47
48
|
保持进程运行并告诉用户当前状态;只有用户明确要求停止时才终止。不得输出用户名、密码或门户 Token。
|
|
48
49
|
|
|
49
50
|
## 配置
|
|
50
51
|
|
|
51
|
-
账号密码只从当前用户的专属配置文件读取,每个门户可保存多个账号。`portal-dev config`
|
|
52
|
+
账号密码只从当前用户的专属配置文件读取,每个门户可保存多个账号。`portal-dev config` 会新增账号或按账号名称更新已有账号,并在 profile 中直接配置别名和模式;登录命令不写账号名称时使用数组第一个账号,在命令末尾追加账号名称时使用指定账号。普通网站使用 `portal-dev config --portal custom` 配置登录 URL、账号名称、用户名和密码。缺少凭据时,引导用户在本机终端运行 `portal-dev config`,不索要或展示具体密码。
|
package/src/config-file.mjs
CHANGED
|
@@ -2,33 +2,54 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'n
|
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
3
|
import { dirname, resolve } from 'node:path'
|
|
4
4
|
|
|
5
|
-
const defaultCommands = () => [
|
|
6
|
-
{ alias: 'ds', mode: 'login', portal: 'sjs' },
|
|
7
|
-
{ alias: 'sc', mode: 'login', portal: 'chenghua' },
|
|
8
|
-
]
|
|
9
|
-
|
|
10
5
|
export const getPortalConfigPath = () => {
|
|
11
6
|
const configRoot =
|
|
12
|
-
process.platform === 'win32'
|
|
7
|
+
process.platform === 'win32'
|
|
8
|
+
? process.env.APPDATA || resolve(homedir(), 'AppData', 'Roaming')
|
|
9
|
+
: resolve(homedir(), '.config')
|
|
13
10
|
return resolve(configRoot, 'sybz-components', 'portal-dev.json')
|
|
14
11
|
}
|
|
15
12
|
|
|
16
13
|
export const readPortalConfig = () => {
|
|
17
14
|
const configPath = getPortalConfigPath()
|
|
18
|
-
if (!existsSync(configPath)) return { version:
|
|
15
|
+
if (!existsSync(configPath)) return { version: 5, profiles: {} }
|
|
19
16
|
try {
|
|
20
17
|
const config = JSON.parse(readFileSync(configPath, 'utf8'))
|
|
21
18
|
if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('根节点必须是对象')
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
const profiles = config.profiles && typeof config.profiles === 'object' ? structuredClone(config.profiles) : {}
|
|
20
|
+
|
|
21
|
+
for (const command of Array.isArray(config.commands) ? config.commands : []) {
|
|
22
|
+
if (!command?.alias || !['sjs', 'chenghua', 'custom'].includes(command.portal)) continue
|
|
23
|
+
const accounts = Array.isArray(profiles[command.portal]) ? profiles[command.portal] : []
|
|
24
|
+
const accountIndex = command.account
|
|
25
|
+
? accounts.findIndex((account) => account?.name === command.account)
|
|
26
|
+
: accounts.length
|
|
27
|
+
? 0
|
|
28
|
+
: -1
|
|
29
|
+
if (accountIndex < 0) continue
|
|
30
|
+
const { portal: _portal, account: _account, ...profileOptions } = command
|
|
31
|
+
accounts[accountIndex] = { ...accounts[accountIndex], ...profileOptions }
|
|
32
|
+
profiles[command.portal] = accounts
|
|
26
33
|
}
|
|
34
|
+
|
|
35
|
+
const normalizedConfig = { version: 5, profiles }
|
|
36
|
+
if (config.version !== 5 || Object.hasOwn(config, 'commands')) writeConfig(configPath, normalizedConfig)
|
|
37
|
+
return normalizedConfig
|
|
27
38
|
} catch (error) {
|
|
28
39
|
throw new Error(`门户配置文件格式错误:${configPath}\n${error.message}`)
|
|
29
40
|
}
|
|
30
41
|
}
|
|
31
42
|
|
|
43
|
+
const writeConfig = (configPath, config) => {
|
|
44
|
+
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 })
|
|
45
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 })
|
|
46
|
+
try {
|
|
47
|
+
chmodSync(configPath, 0o600)
|
|
48
|
+
} catch {
|
|
49
|
+
// Windows 等不支持 POSIX 权限的环境由系统用户目录权限保护。
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
export const writePortalAccount = (portal, account) => {
|
|
33
54
|
const configPath = getPortalConfigPath()
|
|
34
55
|
const config = readPortalConfig()
|
|
@@ -44,35 +65,9 @@ export const writePortalAccount = (portal, account) => {
|
|
|
44
65
|
if (accountIndex >= 0) nextAccounts[accountIndex] = account
|
|
45
66
|
else nextAccounts.push(account)
|
|
46
67
|
const nextConfig = {
|
|
47
|
-
|
|
48
|
-
version: 4,
|
|
68
|
+
version: 5,
|
|
49
69
|
profiles: { ...normalizedProfiles, [portal]: nextAccounts },
|
|
50
|
-
commands: config.commands,
|
|
51
|
-
}
|
|
52
|
-
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 })
|
|
53
|
-
writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, { mode: 0o600 })
|
|
54
|
-
try {
|
|
55
|
-
chmodSync(configPath, 0o600)
|
|
56
|
-
} catch {
|
|
57
|
-
// Windows 等不支持 POSIX 权限的环境由系统用户目录权限保护。
|
|
58
70
|
}
|
|
71
|
+
writeConfig(configPath, nextConfig)
|
|
59
72
|
return { configPath, accountCount: nextAccounts.length }
|
|
60
73
|
}
|
|
61
|
-
|
|
62
|
-
export const writePortalCommand = (command) => {
|
|
63
|
-
const configPath = getPortalConfigPath()
|
|
64
|
-
const config = readPortalConfig()
|
|
65
|
-
const commandIndex = config.commands.findIndex((item) => item.alias === command.alias)
|
|
66
|
-
const nextCommands = [...config.commands]
|
|
67
|
-
if (commandIndex >= 0) nextCommands[commandIndex] = command
|
|
68
|
-
else nextCommands.push(command)
|
|
69
|
-
const nextConfig = { ...config, version: 4, commands: nextCommands }
|
|
70
|
-
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 })
|
|
71
|
-
writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, { mode: 0o600 })
|
|
72
|
-
try {
|
|
73
|
-
chmodSync(configPath, 0o600)
|
|
74
|
-
} catch {
|
|
75
|
-
// Windows 等不支持 POSIX 权限的环境由系统用户目录权限保护。
|
|
76
|
-
}
|
|
77
|
-
return { configPath, commandCount: nextCommands.length }
|
|
78
|
-
}
|
package/src/configure.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline/promises'
|
|
2
|
-
import { writePortalAccount
|
|
2
|
+
import { writePortalAccount } from './config-file.mjs'
|
|
3
3
|
|
|
4
4
|
const args = process.argv.slice(2)
|
|
5
5
|
const readArg = (name) => {
|
|
@@ -52,52 +52,6 @@ const askPassword = (message) => {
|
|
|
52
52
|
})
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
if (args[0] === 'command') {
|
|
56
|
-
const commandRl = createInterface({ input: process.stdin, output: process.stdout })
|
|
57
|
-
const alias = (await commandRl.question('请输入快捷命令别名(例如:ds、sc、sjs-dev):')).trim()
|
|
58
|
-
if (!alias || !/^[a-zA-Z][\w-]*$/.test(alias)) {
|
|
59
|
-
commandRl.close()
|
|
60
|
-
throw new Error('别名只能包含字母、数字、下划线和短横线,并且必须以字母开头')
|
|
61
|
-
}
|
|
62
|
-
const modeAnswer = (await commandRl.question('请选择命令类型(1=只登录,2=石景山联调,默认1):')).trim()
|
|
63
|
-
const mode = modeAnswer === '2' ? 'dev' : 'login'
|
|
64
|
-
const portalAnswer =
|
|
65
|
-
mode === 'dev' ? 'sjs' : (await commandRl.question('请选择门户(1=石景山,2=成华,3=自定义,默认石景山):')).trim()
|
|
66
|
-
const portal =
|
|
67
|
-
portalAnswer === '3' || portalAnswer === 'custom'
|
|
68
|
-
? 'custom'
|
|
69
|
-
: portalAnswer === '2' || portalAnswer === 'chenghua'
|
|
70
|
-
? 'chenghua'
|
|
71
|
-
: 'sjs'
|
|
72
|
-
const account = (await commandRl.question('请输入账号名称(直接回车使用该门户第一个账号):')).trim()
|
|
73
|
-
const command = { alias, mode, portal, ...(account ? { account } : {}) }
|
|
74
|
-
|
|
75
|
-
if (mode === 'dev') {
|
|
76
|
-
const project = (await commandRl.question('请输入前端项目绝对路径:')).trim()
|
|
77
|
-
if (!project) {
|
|
78
|
-
commandRl.close()
|
|
79
|
-
throw new Error('联调命令必须配置前端项目路径')
|
|
80
|
-
}
|
|
81
|
-
command.project = project
|
|
82
|
-
command.localOrigin =
|
|
83
|
-
(await commandRl.question('请输入本地服务地址(默认 http://localhost:5173):')).trim() || 'http://localhost:5173'
|
|
84
|
-
command.localPath =
|
|
85
|
-
(await commandRl.question('请输入本地接收 Token 的路由(默认 /exhibition-hall):')).trim() || '/exhibition-hall'
|
|
86
|
-
command.roomName =
|
|
87
|
-
(await commandRl.question('请输入样板间智能体名称(默认 3D智能展厅智能体):')).trim() || '3D智能展厅智能体'
|
|
88
|
-
command.iframeHost =
|
|
89
|
-
(await commandRl.question('请输入门户 iframe 域名(默认 hia.sjsdoubao.com:31118):')).trim() ||
|
|
90
|
-
'hia.sjsdoubao.com:31118'
|
|
91
|
-
command.iframePath =
|
|
92
|
-
(await commandRl.question('请输入门户 iframe 路径(默认 /exhibition-hall):')).trim() || '/exhibition-hall'
|
|
93
|
-
}
|
|
94
|
-
commandRl.close()
|
|
95
|
-
const { configPath, commandCount } = writePortalCommand(command)
|
|
96
|
-
console.log(`快捷命令配置完成:portal-dev ${alias}`)
|
|
97
|
-
console.log(`当前共 ${commandCount} 个快捷命令,配置文件:${configPath}`)
|
|
98
|
-
process.exit(0)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
55
|
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
102
56
|
const portalArg = readArg('--portal')
|
|
103
57
|
const portalAnswer = portalArg || (await rl.question('请选择门户(1=石景山,2=成华,3=自定义,直接回车默认石景山):'))
|
|
@@ -113,6 +67,28 @@ if (!name) {
|
|
|
113
67
|
rl.close()
|
|
114
68
|
throw new Error('账号名称不能为空')
|
|
115
69
|
}
|
|
70
|
+
const alias = (await rl.question('请输入快捷命令别名(可直接回车跳过):')).trim()
|
|
71
|
+
if (alias && !/^[a-zA-Z][\w-]*$/.test(alias)) {
|
|
72
|
+
rl.close()
|
|
73
|
+
throw new Error('别名只能包含字母、数字、下划线和短横线,并且必须以字母开头')
|
|
74
|
+
}
|
|
75
|
+
const modeAnswer = portal === 'sjs' ? (await rl.question('请选择模式(1=只登录,2=本地联调,默认1):')).trim() : '1'
|
|
76
|
+
const mode = modeAnswer === '2' ? 'dev' : 'login'
|
|
77
|
+
const profileOptions = { ...(alias ? { alias } : {}), mode }
|
|
78
|
+
if (mode === 'dev') {
|
|
79
|
+
const project = (await rl.question('请输入需要自动启动的前端项目绝对路径(可直接回车跳过):')).trim()
|
|
80
|
+
if (project) profileOptions.project = project
|
|
81
|
+
profileOptions.localOrigin =
|
|
82
|
+
(await rl.question('请输入本地服务地址(默认 http://localhost:5173):')).trim() || 'http://localhost:5173'
|
|
83
|
+
profileOptions.localPath =
|
|
84
|
+
(await rl.question('请输入本地接收 Token 的路由(默认 /exhibition-hall):')).trim() || '/exhibition-hall'
|
|
85
|
+
profileOptions.roomName =
|
|
86
|
+
(await rl.question('请输入样板间智能体名称(默认 3D智能展厅智能体):')).trim() || '3D智能展厅智能体'
|
|
87
|
+
profileOptions.iframeHost =
|
|
88
|
+
(await rl.question('请输入门户 iframe 域名(默认 hia.sjsdoubao.com:31118):')).trim() || 'hia.sjsdoubao.com:31118'
|
|
89
|
+
profileOptions.iframePath =
|
|
90
|
+
(await rl.question('请输入门户 iframe 路径(默认 /exhibition-hall):')).trim() || '/exhibition-hall'
|
|
91
|
+
}
|
|
116
92
|
let loginUrl
|
|
117
93
|
if (portal === 'custom') {
|
|
118
94
|
loginUrl = (await rl.question('请输入登录页 URL(例如:https://example.com/login):')).trim()
|
|
@@ -134,8 +110,9 @@ const { configPath, accountCount } = writePortalAccount(portal, {
|
|
|
134
110
|
username,
|
|
135
111
|
password,
|
|
136
112
|
...(loginUrl ? { loginUrl } : {}),
|
|
113
|
+
...profileOptions,
|
|
137
114
|
})
|
|
138
115
|
|
|
139
116
|
console.log(`配置完成:${portalName}现有 ${accountCount} 个账号`)
|
|
140
117
|
console.log(`配置文件:${configPath}`)
|
|
141
|
-
console.log(`现在可以在任意目录运行:portal-dev
|
|
118
|
+
console.log(`现在可以在任意目录运行:portal-dev ${alias || `--portal ${portal} ${name}`}`)
|
package/src/macos-chrome.mjs
CHANGED
|
@@ -3,6 +3,18 @@ import { promisify } from 'node:util'
|
|
|
3
3
|
|
|
4
4
|
const execFileAsync = promisify(execFile)
|
|
5
5
|
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
|
|
6
|
+
const waitForExitSignal = () =>
|
|
7
|
+
new Promise((resolve) => {
|
|
8
|
+
const keepAlive = setInterval(() => undefined, 60_000)
|
|
9
|
+
const finish = () => {
|
|
10
|
+
clearInterval(keepAlive)
|
|
11
|
+
process.removeListener('SIGINT', finish)
|
|
12
|
+
process.removeListener('SIGTERM', finish)
|
|
13
|
+
resolve()
|
|
14
|
+
}
|
|
15
|
+
process.once('SIGINT', finish)
|
|
16
|
+
process.once('SIGTERM', finish)
|
|
17
|
+
})
|
|
6
18
|
const escapeAppleScript = (value) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\r?\n/g, ' ')
|
|
7
19
|
|
|
8
20
|
const runAppleScript = async (script) => {
|
|
@@ -74,6 +86,28 @@ const fillSearch = async (tab, roomName) =>
|
|
|
74
86
|
})()`,
|
|
75
87
|
)) === 'true'
|
|
76
88
|
|
|
89
|
+
const waitForPortalReady = async (tab) => {
|
|
90
|
+
let stableCount = 0
|
|
91
|
+
for (let index = 0; index < 120; index += 1) {
|
|
92
|
+
const ready =
|
|
93
|
+
(await execute(
|
|
94
|
+
tab,
|
|
95
|
+
`(() => {
|
|
96
|
+
const visible = (element) => { const style = getComputedStyle(element); const rect = element.getBoundingClientRect(); return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 0 && rect.height > 0; };
|
|
97
|
+
const loading = Array.from(document.querySelectorAll('.el-loading-mask, .ant-spin-spinning, .n-spin-body, .loading-mask, .loading-overlay, .v-loading-mask, [aria-busy="true"]')).some(visible);
|
|
98
|
+
return document.readyState === 'complete' && !loading ? 'true' : 'false';
|
|
99
|
+
})()`,
|
|
100
|
+
)) === 'true'
|
|
101
|
+
stableCount = ready ? stableCount + 1 : 0
|
|
102
|
+
if (stableCount >= 2) {
|
|
103
|
+
await sleep(800)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
await sleep(500)
|
|
107
|
+
}
|
|
108
|
+
throw new Error('门户页面 loading 等待超时')
|
|
109
|
+
}
|
|
110
|
+
|
|
77
111
|
const fillAndSubmit = async (tab, { username, password, captchaText, custom }) =>
|
|
78
112
|
JSON.parse(
|
|
79
113
|
await execute(
|
|
@@ -83,13 +117,15 @@ const fillAndSubmit = async (tab, { username, password, captchaText, custom }) =
|
|
|
83
117
|
const visible = (element) => { const style = getComputedStyle(element); const rect = element.getBoundingClientRect(); return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0; };
|
|
84
118
|
const inputs = Array.from(document.querySelectorAll('input')).filter(visible);
|
|
85
119
|
const setValue = (element, value) => { const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; setter.call(element, value); element.dispatchEvent(new Event('input', {bubbles:true})); element.dispatchEvent(new Event('change', {bubbles:true})); };
|
|
86
|
-
const
|
|
87
|
-
const passwordInput = inputs.find((input) => input
|
|
88
|
-
const captchaInput = inputs.find((input) => /验证码/.test(input
|
|
89
|
-
|
|
120
|
+
const attribute = (input, name) => input.getAttribute(name) || '';
|
|
121
|
+
const passwordInput = inputs.find((input) => attribute(input, 'autocomplete') === 'current-password' || /^password$/i.test(attribute(input, 'name')) || input.type === 'password' || /密码/.test(attribute(input, 'placeholder')));
|
|
122
|
+
const captchaInput = inputs.find((input) => /验证码/.test(attribute(input, 'placeholder')) || /captcha|code/i.test(attribute(input, 'name')));
|
|
123
|
+
const usernameInput = inputs.find((input) => attribute(input, 'autocomplete') === 'username' || /^(username|account|login|user|email|phone)$/i.test(attribute(input, 'name')) || /用户名|账号|邮箱|手机/.test(attribute(input, 'placeholder'))) || inputs.find((input) => input !== passwordInput && input !== captchaInput && /^(text|email|tel|number)$/.test(input.type));
|
|
124
|
+
const missing = [!usernameInput && '账号输入框', !passwordInput && '密码输入框', !${custom} && !captchaInput && '验证码输入框'].filter(Boolean);
|
|
125
|
+
if (missing.length) return JSON.stringify({submitted:false, reason:'未找到' + missing.join('、')});
|
|
90
126
|
setValue(usernameInput, values.username); setValue(passwordInput, values.password); if (captchaInput) setValue(captchaInput, values.captchaText);
|
|
91
127
|
const button = Array.from(document.querySelectorAll('button, input[type="submit"], [role="button"], .btn-box .btn')).filter(visible).find((element) => element.type === 'submit' || /登录|Login|Sign in/i.test(element.textContent || element.value || ''));
|
|
92
|
-
if (!button) return JSON.stringify({submitted:false}); button.click(); return JSON.stringify({submitted:true});
|
|
128
|
+
if (!button) return JSON.stringify({submitted:false, reason:'未找到登录按钮'}); button.click(); return JSON.stringify({submitted:true});
|
|
93
129
|
})()`,
|
|
94
130
|
),
|
|
95
131
|
)
|
|
@@ -107,7 +143,7 @@ const login = async (tab, config, recognizeCaptcha, custom) => {
|
|
|
107
143
|
console.log(`已识别图形验证码(第 ${attempt}/3 次)`)
|
|
108
144
|
}
|
|
109
145
|
const result = await fillAndSubmit(tab, { ...config, captchaText, custom })
|
|
110
|
-
if (!result.submitted) throw new Error('登录表单字段不完整或未找到登录按钮')
|
|
146
|
+
if (!result.submitted) throw new Error(result.reason || '登录表单字段不完整或未找到登录按钮')
|
|
111
147
|
await sleep(1800)
|
|
112
148
|
const state = await pageState(tab)
|
|
113
149
|
if (custom || !state.url.includes('/passport/login/')) return
|
|
@@ -145,9 +181,13 @@ export const runInExistingChrome = async ({
|
|
|
145
181
|
await execute(tab, `location.href = 'https://www.chenghua-ai.com/chat/pages/application'; ''`)
|
|
146
182
|
}
|
|
147
183
|
console.log(`${portalName}登录流程已完成,按 Ctrl+C 结束。`)
|
|
148
|
-
await
|
|
184
|
+
await waitForExitSignal()
|
|
185
|
+
return
|
|
149
186
|
}
|
|
150
187
|
|
|
188
|
+
console.log('正在等待门户页面加载完成。')
|
|
189
|
+
await waitForPortalReady(tab)
|
|
190
|
+
console.log('门户页面已加载,正在进入智能体样板间。')
|
|
151
191
|
let sampleRoomClicked = false
|
|
152
192
|
let searched = false
|
|
153
193
|
for (let index = 0; index < 180; index += 1) {
|
|
@@ -158,7 +198,8 @@ export const runInExistingChrome = async ({
|
|
|
158
198
|
await openTab(destination.href)
|
|
159
199
|
console.log(`门户本地调试已就绪:${localOrigin}${targetUrl.pathname}`)
|
|
160
200
|
console.log('Token 未打印、未写入文件。按 Ctrl+C 结束。')
|
|
161
|
-
|
|
201
|
+
await waitForExitSignal()
|
|
202
|
+
return
|
|
162
203
|
}
|
|
163
204
|
if (!sampleRoomClicked) sampleRoomClicked = await clickText(tab, '智能体样板间')
|
|
164
205
|
else if (!searched) searched = await fillSearch(tab, roomName)
|
package/src/portal-dev.mjs
CHANGED
|
@@ -10,15 +10,29 @@ const sleep = (milliseconds) => new Promise((resolvePromise) => setTimeout(resol
|
|
|
10
10
|
const args = process.argv.slice(2)
|
|
11
11
|
const portalConfig = readPortalConfig()
|
|
12
12
|
const commandAlias = args[0] && args[0] !== 'dev' && !args[0].startsWith('-') ? args[0] : undefined
|
|
13
|
-
const savedCommand = commandAlias ? portalConfig.commands.find((command) => command?.alias === commandAlias) : undefined
|
|
14
|
-
if (commandAlias && !savedCommand)
|
|
15
|
-
throw new Error(`未找到快捷命令“${commandAlias}”,请运行 portal-dev config command 创建`)
|
|
16
13
|
const readArg = (name) => {
|
|
17
14
|
const index = args.indexOf(name)
|
|
18
15
|
return index >= 0 ? args[index + 1] : undefined
|
|
19
16
|
}
|
|
17
|
+
const selectedProfileName = readArg('--profile')
|
|
18
|
+
const savedCommand = commandAlias
|
|
19
|
+
? Object.entries(portalConfig.profiles)
|
|
20
|
+
.flatMap(([portal, profiles]) =>
|
|
21
|
+
(Array.isArray(profiles) ? profiles : []).map((profile) => ({ ...profile, portal })),
|
|
22
|
+
)
|
|
23
|
+
.find((profile) => profile.alias === commandAlias)
|
|
24
|
+
: selectedProfileName
|
|
25
|
+
? Object.entries(portalConfig.profiles)
|
|
26
|
+
.flatMap(([portal, profiles]) =>
|
|
27
|
+
(Array.isArray(profiles) ? profiles : []).map((profile) => ({ ...profile, portal })),
|
|
28
|
+
)
|
|
29
|
+
.find((profile) => profile.portal === readArg('--portal') && profile.name === selectedProfileName)
|
|
30
|
+
: undefined
|
|
31
|
+
if (commandAlias && !savedCommand)
|
|
32
|
+
throw new Error(`未找到快捷命令“${commandAlias}”,请运行 portal-dev config 为账号配置别名`)
|
|
33
|
+
if (selectedProfileName && !savedCommand) throw new Error(`未找到账号“${selectedProfileName}”`)
|
|
20
34
|
const readPositionalAccount = () => {
|
|
21
|
-
const valueOptions = new Set(['--portal', '--project'])
|
|
35
|
+
const valueOptions = new Set(['--portal', '--project', '--profile', '--host', '--port'])
|
|
22
36
|
for (let index = 0; index < args.length; index += 1) {
|
|
23
37
|
const argument = args[index]
|
|
24
38
|
if (valueOptions.has(argument)) {
|
|
@@ -50,7 +64,8 @@ const findProject = () => {
|
|
|
50
64
|
throw new Error('未找到前端项目,请先进入项目目录,或使用 --project <路径> 指定')
|
|
51
65
|
}
|
|
52
66
|
|
|
53
|
-
const
|
|
67
|
+
const configuredProject = savedCommand?.project || readArg('--project')
|
|
68
|
+
const projectDir = devMode && (configuredProject || args[0] === 'dev') ? findProject() : undefined
|
|
54
69
|
if (projectDir && !existsSync(resolve(projectDir, 'package.json')))
|
|
55
70
|
throw new Error(`目标项目不存在或缺少 package.json:${projectDir}`)
|
|
56
71
|
const configuredAccounts = portalConfig.profiles[portal]
|
|
@@ -66,7 +81,8 @@ if (!portalAccounts.length) {
|
|
|
66
81
|
}
|
|
67
82
|
|
|
68
83
|
const selectAccount = () => {
|
|
69
|
-
|
|
84
|
+
if (savedCommand) return savedCommand
|
|
85
|
+
const accountName = readPositionalAccount()
|
|
70
86
|
if (accountName) {
|
|
71
87
|
const matched = portalAccounts.find((account) => account.name === accountName)
|
|
72
88
|
if (!matched) throw new Error(`未找到账号“${accountName}”,请检查账号名称或重新配置`)
|
|
@@ -112,11 +128,16 @@ const chromeCandidates = {
|
|
|
112
128
|
`${process.env.PROGRAMFILES || ''}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
113
129
|
`${process.env['PROGRAMFILES(X86)'] || ''}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
114
130
|
`${process.env.LOCALAPPDATA || ''}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
131
|
+
`${process.env.PROGRAMFILES || ''}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
132
|
+
`${process.env['PROGRAMFILES(X86)'] || ''}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
133
|
+
`${process.env.LOCALAPPDATA || ''}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
115
134
|
],
|
|
116
135
|
linux: ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser'],
|
|
117
136
|
}
|
|
118
|
-
const executablePath = chromeCandidates[process.platform]
|
|
119
|
-
|
|
137
|
+
const executablePath = [process.env.CHROME_PATH, ...(chromeCandidates[process.platform] || [])]
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.find(existsSync)
|
|
140
|
+
if (!executablePath) throw new Error('未找到 Chrome 或 Edge,请先安装浏览器,或通过 CHROME_PATH 指定可执行文件')
|
|
120
141
|
|
|
121
142
|
const localUrl = new URL(localOrigin)
|
|
122
143
|
const isReady = async () => {
|
|
@@ -128,7 +149,15 @@ const isReady = async () => {
|
|
|
128
149
|
}
|
|
129
150
|
|
|
130
151
|
let devServer
|
|
131
|
-
|
|
152
|
+
const stopDevServer = () => {
|
|
153
|
+
if (!devServer?.pid) return
|
|
154
|
+
if (process.platform === 'win32') {
|
|
155
|
+
spawn('taskkill.exe', ['/pid', String(devServer.pid), '/t', '/f'], { stdio: 'ignore' }).unref()
|
|
156
|
+
} else {
|
|
157
|
+
devServer.kill('SIGTERM')
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (devMode && projectDir && !(await isReady())) {
|
|
132
161
|
const packageJson = JSON.parse(readFileSync(resolve(projectDir, 'package.json'), 'utf8'))
|
|
133
162
|
if (!packageJson.scripts?.dev) throw new Error(`目标项目没有 dev script:${projectDir}`)
|
|
134
163
|
const runner = existsSync(resolve(projectDir, 'bun.lock'))
|
|
@@ -136,14 +165,24 @@ if (devMode && !(await isReady())) {
|
|
|
136
165
|
: existsSync(resolve(projectDir, 'pnpm-lock.yaml'))
|
|
137
166
|
? 'pnpm'
|
|
138
167
|
: 'npm'
|
|
168
|
+
const runnerArgs = ['run', 'dev', '--', '--host', localUrl.hostname, '--port', localUrl.port || '80']
|
|
139
169
|
console.log(`正在启动项目开发服务:${projectDir}`)
|
|
140
|
-
devServer =
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
170
|
+
devServer =
|
|
171
|
+
process.platform === 'win32'
|
|
172
|
+
? spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', runner, ...runnerArgs], {
|
|
173
|
+
cwd: projectDir,
|
|
174
|
+
env: process.env,
|
|
175
|
+
stdio: 'inherit',
|
|
176
|
+
})
|
|
177
|
+
: spawn(runner, runnerArgs, {
|
|
178
|
+
cwd: projectDir,
|
|
179
|
+
env: process.env,
|
|
180
|
+
stdio: 'inherit',
|
|
181
|
+
})
|
|
145
182
|
for (let index = 0; index < 120 && !(await isReady()); index += 1) await sleep(500)
|
|
146
183
|
if (!(await isReady())) throw new Error(`本地开发服务启动超时:${localOrigin}`)
|
|
184
|
+
} else if (devMode && !projectDir && !(await isReady())) {
|
|
185
|
+
console.log(`未配置 project,不自动启动前端项目,后续将直接打开默认地址:${localOrigin}`)
|
|
147
186
|
}
|
|
148
187
|
|
|
149
188
|
if (process.platform === 'darwin') {
|
|
@@ -160,11 +199,23 @@ if (process.platform === 'darwin') {
|
|
|
160
199
|
portal === 'custom' ? `自定义网站“${portalAccount.name}”` : `${portal === 'chenghua' ? '成华' : '石景山'}门户`,
|
|
161
200
|
recognizeCaptcha,
|
|
162
201
|
})
|
|
202
|
+
stopDevServer()
|
|
203
|
+
process.exit(0)
|
|
163
204
|
}
|
|
164
205
|
|
|
165
206
|
const browser = await chromium.launch({ headless: false, executablePath })
|
|
166
207
|
const context = await browser.newContext()
|
|
167
208
|
const page = await context.newPage()
|
|
209
|
+
let closing = false
|
|
210
|
+
const shutdown = async () => {
|
|
211
|
+
if (closing) return
|
|
212
|
+
closing = true
|
|
213
|
+
stopDevServer()
|
|
214
|
+
await browser.close().catch(() => undefined)
|
|
215
|
+
process.exit(0)
|
|
216
|
+
}
|
|
217
|
+
process.on('SIGINT', shutdown)
|
|
218
|
+
process.on('SIGTERM', shutdown)
|
|
168
219
|
await page.goto(config.loginUrl, { waitUntil: 'domcontentloaded' })
|
|
169
220
|
|
|
170
221
|
const visibleLocator = async (selectors) => {
|
|
@@ -188,6 +239,42 @@ const clickText = async (text) => {
|
|
|
188
239
|
return false
|
|
189
240
|
}
|
|
190
241
|
|
|
242
|
+
const waitForPortalReady = async () => {
|
|
243
|
+
console.log('正在等待门户页面加载完成。')
|
|
244
|
+
let stableCount = 0
|
|
245
|
+
for (let index = 0; index < 120; index += 1) {
|
|
246
|
+
const ready = await page
|
|
247
|
+
.evaluate(() => {
|
|
248
|
+
const visible = (element) => {
|
|
249
|
+
const style = getComputedStyle(element)
|
|
250
|
+
const rect = element.getBoundingClientRect()
|
|
251
|
+
return (
|
|
252
|
+
style.display !== 'none' &&
|
|
253
|
+
style.visibility !== 'hidden' &&
|
|
254
|
+
style.opacity !== '0' &&
|
|
255
|
+
rect.width > 0 &&
|
|
256
|
+
rect.height > 0
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
const loading = Array.from(
|
|
260
|
+
document.querySelectorAll(
|
|
261
|
+
'.el-loading-mask, .ant-spin-spinning, .n-spin-body, .loading-mask, .loading-overlay, .v-loading-mask, [aria-busy="true"]',
|
|
262
|
+
),
|
|
263
|
+
).some(visible)
|
|
264
|
+
return document.readyState === 'complete' && !loading
|
|
265
|
+
})
|
|
266
|
+
.catch(() => false)
|
|
267
|
+
stableCount = ready ? stableCount + 1 : 0
|
|
268
|
+
if (stableCount >= 2) {
|
|
269
|
+
await sleep(800)
|
|
270
|
+
console.log('门户页面已加载,正在进入智能体样板间。')
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
await sleep(500)
|
|
274
|
+
}
|
|
275
|
+
throw new Error('门户页面 loading 等待超时')
|
|
276
|
+
}
|
|
277
|
+
|
|
191
278
|
const login = async () => {
|
|
192
279
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
193
280
|
let captcha
|
|
@@ -209,6 +296,7 @@ const login = async () => {
|
|
|
209
296
|
'input[name="username"]',
|
|
210
297
|
'input[placeholder*="用户名"]',
|
|
211
298
|
'input[placeholder*="账号"]',
|
|
299
|
+
'input[autocomplete="off"]:not([type="password"]):not([placeholder*="验证码"])',
|
|
212
300
|
])
|
|
213
301
|
const passwordInput = await visibleLocator([
|
|
214
302
|
'input[autocomplete="current-password"]',
|
|
@@ -301,6 +389,7 @@ if (!devMode) {
|
|
|
301
389
|
await new Promise(() => undefined)
|
|
302
390
|
}
|
|
303
391
|
|
|
392
|
+
await waitForPortalReady()
|
|
304
393
|
let sampleRoomClicked = false
|
|
305
394
|
let searched = false
|
|
306
395
|
let targetUrl
|
|
@@ -341,11 +430,4 @@ await context.newPage().then((localPage) => localPage.goto(destination.href))
|
|
|
341
430
|
console.log(`门户本地调试已就绪:${localOrigin}${targetUrl.pathname}`)
|
|
342
431
|
console.log('Token 未打印、未写入文件。按 Ctrl+C 结束。')
|
|
343
432
|
|
|
344
|
-
const shutdown = async () => {
|
|
345
|
-
devServer?.kill('SIGTERM')
|
|
346
|
-
await browser.close().catch(() => undefined)
|
|
347
|
-
process.exit(0)
|
|
348
|
-
}
|
|
349
|
-
process.on('SIGINT', shutdown)
|
|
350
|
-
process.on('SIGTERM', shutdown)
|
|
351
433
|
await new Promise(() => undefined)
|