@sybz-components/portal-dev 1.0.3 → 1.0.9
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 -10
- package/bin/portal-dev.mjs +6 -4
- package/package.json +3 -3
- package/skills/portal-dev/SKILL.md +23 -6
- package/src/config-file.mjs +45 -6
- package/src/configure.mjs +81 -9
- package/src/macos-chrome.mjs +169 -0
- package/src/portal-dev.mjs +136 -26
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# @sybz-components/portal-dev
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
成华、石景山门户自动登录 CLI,也可登录只需用户名和密码的自定义网站,并按需启动石景山门户本地联调。
|
|
4
4
|
|
|
5
5
|
## 环境要求
|
|
6
6
|
|
|
7
7
|
- Node.js 18+
|
|
8
8
|
- Google Chrome 或 Microsoft Edge
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
macOS 会直接复用当前打开的 Google Chrome,在现有窗口中新建标签页完成登录和本地联调,不会启动独立的 Chrome 实例。Windows / Linux 使用独立浏览器实例作为兼容方案。
|
|
10
11
|
|
|
11
12
|
## 安装
|
|
12
13
|
|
|
@@ -24,7 +25,7 @@ pnpm add -g @sybz-components/portal-dev
|
|
|
24
25
|
portal-dev config
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
根据中文提示输入账号名称、用户名和密码即可。密码输入时会显示为 `*`。同一个门户可以重复执行该命令保存多个账号;账号名称相同时会更新原账号。
|
|
28
29
|
|
|
29
30
|
默认配置文件位置:
|
|
30
31
|
|
|
@@ -33,7 +34,7 @@ portal-dev config
|
|
|
33
34
|
|
|
34
35
|
程序会自动创建目录,并尽可能将配置文件权限设为仅当前用户可读写。
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
配置完成后可以在任意目录直接运行:
|
|
37
38
|
|
|
38
39
|
```bash
|
|
39
40
|
portal-dev
|
|
@@ -44,27 +45,94 @@ portal-dev
|
|
|
44
45
|
```bash
|
|
45
46
|
portal-dev config --portal sjs
|
|
46
47
|
portal-dev config --portal chenghua
|
|
48
|
+
portal-dev config --portal custom
|
|
47
49
|
```
|
|
48
50
|
|
|
49
51
|
## 使用
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
只登录石景山门户,不读取或启动任何前端项目:
|
|
52
54
|
|
|
53
55
|
```bash
|
|
54
56
|
portal-dev
|
|
55
57
|
```
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
只登录成华门户并进入智能体广场,不读取或启动任何前端项目:
|
|
58
60
|
|
|
59
61
|
```bash
|
|
60
62
|
portal-dev --portal chenghua
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
配置多个账号后,不写账号名称会直接使用数组中的第一个账号:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
portal-dev --portal sjs
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
在命令末尾写账号名称,可以使用指定账号:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
portal-dev --portal sjs 测试账号
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
默认快捷命令:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
portal-dev ds # 登录石景山
|
|
81
|
+
portal-dev sc # 登录成华
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
配置并登录普通网站:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
portal-dev config --portal custom
|
|
88
|
+
portal-dev --portal custom 我的账号
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
配置时依次输入登录页 URL、用户名和密码。登录时会自动查找常见的用户名、邮箱、密码输入框,以及“登录”、“Login”或“Sign in”按钮;该模式不会执行验证码识别、样板间搜索或 Token 跳转。
|
|
92
|
+
|
|
93
|
+
别名保存在 JSON 中,可以通过向导新增登录或联调命令:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
portal-dev config command
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
例如配置一个别名为 `sjs-dev` 的联调命令后,只需运行:
|
|
64
100
|
|
|
65
101
|
```bash
|
|
66
|
-
portal-dev
|
|
67
|
-
|
|
102
|
+
portal-dev sjs-dev
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
执行时会登录指定账号、启动本地项目、进入样板间搜索智能体,从匹配的 iframe 地址取得 Token,然后保留查询参数打开本地页面。
|
|
106
|
+
|
|
107
|
+
配置文件中的账号和快捷命令格式:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"version": 4,
|
|
112
|
+
"profiles": {
|
|
113
|
+
"sjs": [{ "name": "我的账号", "username": "user1", "password": "******" }],
|
|
114
|
+
"chenghua": [{ "name": "测试账号", "username": "user2", "password": "******" }],
|
|
115
|
+
"custom": [
|
|
116
|
+
{ "name": "内部系统", "loginUrl": "https://example.com/login", "username": "user3", "password": "******" }
|
|
117
|
+
]
|
|
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
|
+
]
|
|
135
|
+
}
|
|
68
136
|
```
|
|
69
137
|
|
|
70
138
|
查看命令帮助:
|
|
@@ -110,4 +178,4 @@ pnpm portal:skill:install
|
|
|
110
178
|
pnpm portal:release
|
|
111
179
|
```
|
|
112
180
|
|
|
113
|
-
`portal:release` 会执行检查、升级 patch
|
|
181
|
+
`portal:release` 会执行检查、升级 patch 版本、创建版本提交和 Git tag、发布 npm 包,最后将当前分支及 tags 推送到 Git 远端。执行前请确认当前分支、远端和工作区状态。
|
package/bin/portal-dev.mjs
CHANGED
|
@@ -34,13 +34,15 @@ if (args[0] === 'config') {
|
|
|
34
34
|
} else if (args.includes('--help') || args.includes('-h')) {
|
|
35
35
|
console.log(`用法:
|
|
36
36
|
portal-dev
|
|
37
|
-
portal-dev config [--portal sjs|chenghua]
|
|
38
|
-
portal-dev
|
|
39
|
-
portal-dev
|
|
37
|
+
portal-dev config [--portal sjs|chenghua|custom]
|
|
38
|
+
portal-dev config command
|
|
39
|
+
portal-dev <快捷命令别名>
|
|
40
|
+
portal-dev --portal sjs|chenghua|custom [账号名称]
|
|
40
41
|
portal-dev skill install [Codex skills 目录]
|
|
41
42
|
|
|
42
43
|
首次使用只需运行一次 portal-dev config。
|
|
43
|
-
|
|
44
|
+
默认快捷命令:portal-dev ds 登录石景山,portal-dev sc 登录成华。
|
|
45
|
+
运行 portal-dev config command 可新增或修改快捷命令。`)
|
|
44
46
|
process.exit(0)
|
|
45
47
|
} else {
|
|
46
48
|
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.9",
|
|
4
4
|
"description": "成华和石景山门户自动登录与本地前端联调 CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"skill:install": "node ./bin/portal-dev.mjs skill install",
|
|
24
|
-
"check": "node --check bin/portal-dev.mjs && node --check src/config-file.mjs && node --check src/configure.mjs && node --check src/portal-dev.mjs && node --check src/recognize-captcha.mjs",
|
|
25
|
-
"release": "pnpm check && npm version patch && npm publish"
|
|
24
|
+
"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": "pnpm check && npm version patch && npm publish && git push --follow-tags"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@napi-rs/canvas": "^1.0.3",
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: portal-dev
|
|
3
|
-
description:
|
|
3
|
+
description: 启动前端项目的石景山门户本地联调,自动登录成华门户,或使用已配置的 URL 和账号密码登录普通网站。用户提到 dev:portal、门户联调、成华登录、石景山登录、自定义登录或从任意项目启动门户调试时使用。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 门户本地调试
|
|
7
7
|
|
|
8
8
|
调用已安装的 `portal-dev` CLI,不要复制或改写登录自动化逻辑。
|
|
9
9
|
|
|
10
|
+
macOS 会复用用户当前打开的 Google Chrome,并在现有窗口的标签页中完成登录;不要另外启动 Chrome。Windows / Linux 使用 CLI 内置的浏览器兼容方案。
|
|
11
|
+
|
|
10
12
|
## 执行
|
|
11
13
|
|
|
12
14
|
缺少账号配置时先执行:
|
|
@@ -15,20 +17,35 @@ description: 启动前端项目的石景山门户本地联调,或自动登录
|
|
|
15
17
|
portal-dev config --portal sjs
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
只登录石景山门户(不依赖前端项目):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
portal-dev --portal sjs
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
只登录成华门户(不依赖前端项目):
|
|
19
27
|
|
|
20
28
|
```bash
|
|
21
|
-
portal-dev --portal
|
|
29
|
+
portal-dev --portal chenghua
|
|
22
30
|
```
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
登录已配置的普通网站(不走验证码和 Token 等特殊流程):
|
|
25
33
|
|
|
26
34
|
```bash
|
|
27
|
-
portal-dev --portal
|
|
35
|
+
portal-dev --portal custom <账号名称>
|
|
28
36
|
```
|
|
29
37
|
|
|
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
|
+
|
|
30
47
|
保持进程运行并告诉用户当前状态;只有用户明确要求停止时才终止。不得输出用户名、密码或门户 Token。
|
|
31
48
|
|
|
32
49
|
## 配置
|
|
33
50
|
|
|
34
|
-
|
|
51
|
+
账号密码只从当前用户的专属配置文件读取,每个门户可保存多个账号。`portal-dev config` 会新增账号或按账号名称更新已有账号;登录命令不写账号名称时使用数组第一个账号,在命令末尾追加账号名称时使用指定账号。普通网站使用 `portal-dev config --portal custom` 配置登录 URL、账号名称、用户名和密码。缺少凭据时,引导用户在本机终端运行 `portal-dev config`,不索要或展示具体密码。
|
package/src/config-file.mjs
CHANGED
|
@@ -2,6 +2,11 @@ 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
|
+
|
|
5
10
|
export const getPortalConfigPath = () => {
|
|
6
11
|
const configRoot =
|
|
7
12
|
process.platform === 'win32' ? resolve(homedir(), 'AppData', 'Roaming') : resolve(homedir(), '.config')
|
|
@@ -10,23 +15,39 @@ export const getPortalConfigPath = () => {
|
|
|
10
15
|
|
|
11
16
|
export const readPortalConfig = () => {
|
|
12
17
|
const configPath = getPortalConfigPath()
|
|
13
|
-
if (!existsSync(configPath)) return { version:
|
|
18
|
+
if (!existsSync(configPath)) return { version: 4, profiles: {}, commands: defaultCommands() }
|
|
14
19
|
try {
|
|
15
20
|
const config = JSON.parse(readFileSync(configPath, 'utf8'))
|
|
16
21
|
if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('根节点必须是对象')
|
|
17
|
-
return {
|
|
22
|
+
return {
|
|
23
|
+
...config,
|
|
24
|
+
profiles: config.profiles && typeof config.profiles === 'object' ? config.profiles : {},
|
|
25
|
+
commands: Array.isArray(config.commands) ? config.commands : defaultCommands(),
|
|
26
|
+
}
|
|
18
27
|
} catch (error) {
|
|
19
28
|
throw new Error(`门户配置文件格式错误:${configPath}\n${error.message}`)
|
|
20
29
|
}
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
export const
|
|
32
|
+
export const writePortalAccount = (portal, account) => {
|
|
24
33
|
const configPath = getPortalConfigPath()
|
|
25
34
|
const config = readPortalConfig()
|
|
35
|
+
const normalizedProfiles = {
|
|
36
|
+
...config.profiles,
|
|
37
|
+
sjs: Array.isArray(config.profiles.sjs) ? config.profiles.sjs : [],
|
|
38
|
+
chenghua: Array.isArray(config.profiles.chenghua) ? config.profiles.chenghua : [],
|
|
39
|
+
custom: Array.isArray(config.profiles.custom) ? config.profiles.custom : [],
|
|
40
|
+
}
|
|
41
|
+
const accounts = normalizedProfiles[portal]
|
|
42
|
+
const accountIndex = accounts.findIndex((item) => item.name === account.name)
|
|
43
|
+
const nextAccounts = [...accounts]
|
|
44
|
+
if (accountIndex >= 0) nextAccounts[accountIndex] = account
|
|
45
|
+
else nextAccounts.push(account)
|
|
26
46
|
const nextConfig = {
|
|
27
47
|
...config,
|
|
28
|
-
version:
|
|
29
|
-
profiles: { ...
|
|
48
|
+
version: 4,
|
|
49
|
+
profiles: { ...normalizedProfiles, [portal]: nextAccounts },
|
|
50
|
+
commands: config.commands,
|
|
30
51
|
}
|
|
31
52
|
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 })
|
|
32
53
|
writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, { mode: 0o600 })
|
|
@@ -35,5 +56,23 @@ export const writePortalProfile = (portal, profile) => {
|
|
|
35
56
|
} catch {
|
|
36
57
|
// Windows 等不支持 POSIX 权限的环境由系统用户目录权限保护。
|
|
37
58
|
}
|
|
38
|
-
return configPath
|
|
59
|
+
return { configPath, accountCount: nextAccounts.length }
|
|
60
|
+
}
|
|
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 }
|
|
39
78
|
}
|
package/src/configure.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline/promises'
|
|
2
|
-
import {
|
|
2
|
+
import { writePortalAccount, writePortalCommand } from './config-file.mjs'
|
|
3
3
|
|
|
4
4
|
const args = process.argv.slice(2)
|
|
5
5
|
const readArg = (name) => {
|
|
@@ -52,18 +52,90 @@ 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
|
+
|
|
55
101
|
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
56
102
|
const portalArg = readArg('--portal')
|
|
57
|
-
const portalAnswer = portalArg || (await rl.question('请选择门户(1=石景山,2
|
|
58
|
-
const portal =
|
|
59
|
-
|
|
60
|
-
|
|
103
|
+
const portalAnswer = portalArg || (await rl.question('请选择门户(1=石景山,2=成华,3=自定义,直接回车默认石景山):'))
|
|
104
|
+
const portal =
|
|
105
|
+
portalAnswer === '3' || portalAnswer === 'custom'
|
|
106
|
+
? 'custom'
|
|
107
|
+
: portalAnswer === '2' || portalAnswer === 'chenghua'
|
|
108
|
+
? 'chenghua'
|
|
109
|
+
: 'sjs'
|
|
110
|
+
const portalName = portal === 'custom' ? '自定义网站' : portal === 'chenghua' ? '成华' : '石景山'
|
|
111
|
+
const name = (await rl.question('请输入账号名称(例如:我的账号、测试账号):')).trim()
|
|
112
|
+
if (!name) {
|
|
113
|
+
rl.close()
|
|
114
|
+
throw new Error('账号名称不能为空')
|
|
115
|
+
}
|
|
116
|
+
let loginUrl
|
|
117
|
+
if (portal === 'custom') {
|
|
118
|
+
loginUrl = (await rl.question('请输入登录页 URL(例如:https://example.com/login):')).trim()
|
|
119
|
+
try {
|
|
120
|
+
if (!['http:', 'https:'].includes(new URL(loginUrl).protocol)) throw new Error()
|
|
121
|
+
} catch {
|
|
122
|
+
rl.close()
|
|
123
|
+
throw new Error('登录页 URL 必须是有效的 http 或 https 地址')
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const username = (await rl.question(`请输入${portalName}账号:`)).trim()
|
|
61
127
|
rl.close()
|
|
62
128
|
if (!username) throw new Error('账号不能为空')
|
|
63
|
-
const password = await askPassword(`请输入${portalName}
|
|
129
|
+
const password = await askPassword(`请输入${portalName}密码(输入内容会隐藏):`)
|
|
64
130
|
if (!password) throw new Error('密码不能为空')
|
|
65
131
|
|
|
66
|
-
const configPath =
|
|
132
|
+
const { configPath, accountCount } = writePortalAccount(portal, {
|
|
133
|
+
name,
|
|
134
|
+
username,
|
|
135
|
+
password,
|
|
136
|
+
...(loginUrl ? { loginUrl } : {}),
|
|
137
|
+
})
|
|
67
138
|
|
|
68
|
-
console.log(`配置完成:${
|
|
69
|
-
console.log(
|
|
139
|
+
console.log(`配置完成:${portalName}现有 ${accountCount} 个账号`)
|
|
140
|
+
console.log(`配置文件:${configPath}`)
|
|
141
|
+
console.log(`现在可以在任意目录运行:portal-dev --portal ${portal}`)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { promisify } from 'node:util'
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile)
|
|
5
|
+
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
|
|
6
|
+
const escapeAppleScript = (value) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\r?\n/g, ' ')
|
|
7
|
+
|
|
8
|
+
const runAppleScript = async (script) => {
|
|
9
|
+
const { stdout } = await execFileAsync('/usr/bin/osascript', ['-e', script])
|
|
10
|
+
return stdout.trim()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const openTab = async (url) => {
|
|
14
|
+
const result = await runAppleScript(`
|
|
15
|
+
tell application "Google Chrome"
|
|
16
|
+
activate
|
|
17
|
+
if (count windows) = 0 then make new window
|
|
18
|
+
set currentWindow to front window
|
|
19
|
+
tell currentWindow
|
|
20
|
+
set newTab to make new tab with properties {URL:"${escapeAppleScript(url)}"}
|
|
21
|
+
set active tab index to (count tabs)
|
|
22
|
+
end tell
|
|
23
|
+
return "tab id " & (id of newTab) & " of window id " & (id of currentWindow)
|
|
24
|
+
end tell
|
|
25
|
+
`)
|
|
26
|
+
const match = result.match(/tab id (\d+) of window id (\d+)/)
|
|
27
|
+
if (!match) throw new Error(`无法识别 Chrome 新标签页:${result}`)
|
|
28
|
+
return { tabId: Number(match[1]), windowId: Number(match[2]) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const execute = (tab, javascript) =>
|
|
32
|
+
runAppleScript(
|
|
33
|
+
`tell application "Google Chrome" to return (execute tab id ${tab.tabId} of window id ${tab.windowId} javascript "${escapeAppleScript(javascript)}") as text`,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const pageState = async (tab) =>
|
|
37
|
+
JSON.parse(
|
|
38
|
+
await execute(
|
|
39
|
+
tab,
|
|
40
|
+
`(() => JSON.stringify({url: location.href, frameUrls: Array.from(document.querySelectorAll('iframe')).map((frame) => frame.src).filter(Boolean)}))()`,
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const findTargetUrl = (state, iframeHost, iframePath) =>
|
|
45
|
+
state.frameUrls
|
|
46
|
+
.map((value) => {
|
|
47
|
+
try {
|
|
48
|
+
return new URL(value, state.url)
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
.find((url) => url?.host === iframeHost && url.pathname.startsWith(iframePath) && url.searchParams.has('token'))
|
|
54
|
+
|
|
55
|
+
const clickText = async (tab, text) =>
|
|
56
|
+
(await execute(
|
|
57
|
+
tab,
|
|
58
|
+
`(() => {
|
|
59
|
+
const label = ${JSON.stringify(text)};
|
|
60
|
+
const visible = (element) => { const style = getComputedStyle(element); const rect = element.getBoundingClientRect(); return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0; };
|
|
61
|
+
const candidates = Array.from(document.querySelectorAll('button, a, [role="button"], div, span')).filter((element) => visible(element) && (element.textContent || '').includes(label)).sort((a, b) => (a.textContent || '').length - (b.textContent || '').length);
|
|
62
|
+
if (!candidates[0]) return 'false'; candidates[0].click(); return 'true';
|
|
63
|
+
})()`,
|
|
64
|
+
)) === 'true'
|
|
65
|
+
|
|
66
|
+
const fillSearch = async (tab, roomName) =>
|
|
67
|
+
(await execute(
|
|
68
|
+
tab,
|
|
69
|
+
`(() => {
|
|
70
|
+
const input = document.querySelector('input[placeholder*="搜索"], input[placeholder*="查找"], input[type="search"]');
|
|
71
|
+
if (!input) return 'false';
|
|
72
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
|
|
73
|
+
setter.call(input, ${JSON.stringify(roomName)}); input.dispatchEvent(new Event('input', {bubbles:true})); input.dispatchEvent(new Event('change', {bubbles:true})); return 'true';
|
|
74
|
+
})()`,
|
|
75
|
+
)) === 'true'
|
|
76
|
+
|
|
77
|
+
const fillAndSubmit = async (tab, { username, password, captchaText, custom }) =>
|
|
78
|
+
JSON.parse(
|
|
79
|
+
await execute(
|
|
80
|
+
tab,
|
|
81
|
+
`(() => {
|
|
82
|
+
const values = ${JSON.stringify({ username, password, captchaText })};
|
|
83
|
+
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
|
+
const inputs = Array.from(document.querySelectorAll('input')).filter(visible);
|
|
85
|
+
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 usernameInput = inputs.find((input) => input.autocomplete === 'username' || ['username','account','login','user','email'].includes(input.name) || /用户名|账号|邮箱|手机/.test(input.placeholder));
|
|
87
|
+
const passwordInput = inputs.find((input) => input.autocomplete === 'current-password' || input.name === 'password' || input.type === 'password' || input.placeholder.includes('密码'));
|
|
88
|
+
const captchaInput = inputs.find((input) => /验证码/.test(input.placeholder) || /captcha|code/i.test(input.name));
|
|
89
|
+
if (!usernameInput || !passwordInput || (!${custom} && !captchaInput)) return JSON.stringify({submitted:false});
|
|
90
|
+
setValue(usernameInput, values.username); setValue(passwordInput, values.password); if (captchaInput) setValue(captchaInput, values.captchaText);
|
|
91
|
+
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});
|
|
93
|
+
})()`,
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const login = async (tab, config, recognizeCaptcha, custom) => {
|
|
98
|
+
for (let attempt = 1; attempt <= (custom ? 1 : 3); attempt += 1) {
|
|
99
|
+
let captchaText = ''
|
|
100
|
+
if (!custom) {
|
|
101
|
+
const captchaUrl = await execute(
|
|
102
|
+
tab,
|
|
103
|
+
`(() => { const image = document.querySelector('img.code-img, img[class*="captcha" i], img[alt*="验证码"], img[title*="验证码"]'); return image ? (image.currentSrc || image.src || '') : ''; })()`,
|
|
104
|
+
)
|
|
105
|
+
if (!captchaUrl) throw new Error('未找到图形验证码')
|
|
106
|
+
captchaText = await recognizeCaptcha(captchaUrl)
|
|
107
|
+
console.log(`已识别图形验证码(第 ${attempt}/3 次)`)
|
|
108
|
+
}
|
|
109
|
+
const result = await fillAndSubmit(tab, { ...config, captchaText, custom })
|
|
110
|
+
if (!result.submitted) throw new Error('登录表单字段不完整或未找到登录按钮')
|
|
111
|
+
await sleep(1800)
|
|
112
|
+
const state = await pageState(tab)
|
|
113
|
+
if (custom || !state.url.includes('/passport/login/')) return
|
|
114
|
+
if (attempt < 3)
|
|
115
|
+
await execute(
|
|
116
|
+
tab,
|
|
117
|
+
`(() => { const image = document.querySelector('img.code-img, img[class*="captcha" i]'); if (image) image.click(); return ''; })()`,
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
throw new Error('自动登录失败,已达到最多重试次数')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const runInExistingChrome = async ({
|
|
124
|
+
config,
|
|
125
|
+
devMode,
|
|
126
|
+
localOrigin,
|
|
127
|
+
localPath,
|
|
128
|
+
iframeHost,
|
|
129
|
+
iframePath,
|
|
130
|
+
roomName,
|
|
131
|
+
portal,
|
|
132
|
+
portalName,
|
|
133
|
+
recognizeCaptcha,
|
|
134
|
+
}) => {
|
|
135
|
+
console.log('正在使用当前打开的 Google Chrome 登录门户。')
|
|
136
|
+
const tab = await openTab(config.loginUrl)
|
|
137
|
+
await sleep(800)
|
|
138
|
+
const state = await pageState(tab)
|
|
139
|
+
if (portal === 'custom') await login(tab, config, recognizeCaptcha, true)
|
|
140
|
+
else if (state.url.includes('/passport/login/') || portal === 'chenghua')
|
|
141
|
+
await login(tab, config, recognizeCaptcha, false)
|
|
142
|
+
|
|
143
|
+
if (!devMode) {
|
|
144
|
+
if (portal === 'chenghua') {
|
|
145
|
+
await execute(tab, `location.href = 'https://www.chenghua-ai.com/chat/pages/application'; ''`)
|
|
146
|
+
}
|
|
147
|
+
console.log(`${portalName}登录流程已完成,按 Ctrl+C 结束。`)
|
|
148
|
+
await new Promise(() => undefined)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let sampleRoomClicked = false
|
|
152
|
+
let searched = false
|
|
153
|
+
for (let index = 0; index < 180; index += 1) {
|
|
154
|
+
const currentState = await pageState(tab)
|
|
155
|
+
const targetUrl = findTargetUrl(currentState, iframeHost, iframePath)
|
|
156
|
+
if (targetUrl) {
|
|
157
|
+
const destination = new URL(`${localPath || targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`, localOrigin)
|
|
158
|
+
await openTab(destination.href)
|
|
159
|
+
console.log(`门户本地调试已就绪:${localOrigin}${targetUrl.pathname}`)
|
|
160
|
+
console.log('Token 未打印、未写入文件。按 Ctrl+C 结束。')
|
|
161
|
+
return new Promise(() => undefined)
|
|
162
|
+
}
|
|
163
|
+
if (!sampleRoomClicked) sampleRoomClicked = await clickText(tab, '智能体样板间')
|
|
164
|
+
else if (!searched) searched = await fillSearch(tab, roomName)
|
|
165
|
+
else (await clickText(tab, roomName)) || (await clickText(tab, '3D智能展厅'))
|
|
166
|
+
await sleep(1000)
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`未找到目标智能体入口:${roomName}`)
|
|
169
|
+
}
|
package/src/portal-dev.mjs
CHANGED
|
@@ -4,20 +4,41 @@ import { dirname, resolve } from 'node:path'
|
|
|
4
4
|
import { chromium } from 'playwright-core'
|
|
5
5
|
import { readPortalConfig } from './config-file.mjs'
|
|
6
6
|
import { recognizeCaptcha } from './recognize-captcha.mjs'
|
|
7
|
+
import { runInExistingChrome } from './macos-chrome.mjs'
|
|
7
8
|
|
|
8
9
|
const sleep = (milliseconds) => new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds))
|
|
9
10
|
const args = process.argv.slice(2)
|
|
11
|
+
const portalConfig = readPortalConfig()
|
|
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 创建`)
|
|
10
16
|
const readArg = (name) => {
|
|
11
17
|
const index = args.indexOf(name)
|
|
12
18
|
return index >= 0 ? args[index + 1] : undefined
|
|
13
19
|
}
|
|
20
|
+
const readPositionalAccount = () => {
|
|
21
|
+
const valueOptions = new Set(['--portal', '--project'])
|
|
22
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
23
|
+
const argument = args[index]
|
|
24
|
+
if (valueOptions.has(argument)) {
|
|
25
|
+
index += 1
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
if (argument === 'dev' || argument === commandAlias || argument.startsWith('-')) continue
|
|
29
|
+
return argument
|
|
30
|
+
}
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
14
33
|
|
|
15
|
-
const portal = readArg('--portal') || 'sjs'
|
|
16
|
-
if (!['sjs', 'chenghua'].includes(portal))
|
|
17
|
-
|
|
34
|
+
const portal = savedCommand?.portal || readArg('--portal') || 'sjs'
|
|
35
|
+
if (!['sjs', 'chenghua', 'custom'].includes(portal))
|
|
36
|
+
throw new Error(`不支持的门户:${portal},可选值为 sjs、chenghua、custom`)
|
|
37
|
+
const devMode = savedCommand?.mode === 'dev' || args[0] === 'dev'
|
|
38
|
+
if (devMode && portal !== 'sjs') throw new Error('门户联调目前只支持石景山;成华和自定义命令只负责登录')
|
|
18
39
|
|
|
19
40
|
const findProject = () => {
|
|
20
|
-
const explicit = readArg('--project')
|
|
41
|
+
const explicit = savedCommand?.project || readArg('--project')
|
|
21
42
|
if (explicit) return resolve(explicit)
|
|
22
43
|
let current = process.cwd()
|
|
23
44
|
while (true) {
|
|
@@ -29,34 +50,59 @@ const findProject = () => {
|
|
|
29
50
|
throw new Error('未找到前端项目,请先进入项目目录,或使用 --project <路径> 指定')
|
|
30
51
|
}
|
|
31
52
|
|
|
32
|
-
const projectDir = findProject()
|
|
33
|
-
if (!existsSync(resolve(projectDir, 'package.json')))
|
|
53
|
+
const projectDir = devMode ? findProject() : undefined
|
|
54
|
+
if (projectDir && !existsSync(resolve(projectDir, 'package.json')))
|
|
34
55
|
throw new Error(`目标项目不存在或缺少 package.json:${projectDir}`)
|
|
35
|
-
const
|
|
56
|
+
const configuredAccounts = portalConfig.profiles[portal]
|
|
57
|
+
const portalAccounts = Array.isArray(configuredAccounts)
|
|
58
|
+
? configuredAccounts.filter(
|
|
59
|
+
(account) => account?.name && typeof account.username === 'string' && typeof account.password === 'string',
|
|
60
|
+
)
|
|
61
|
+
: []
|
|
62
|
+
if (!portalAccounts.length) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`尚未配置${portal === 'custom' ? '自定义网站' : portal === 'chenghua' ? '成华' : '石景山'}账号,请先运行 portal-dev config --portal ${portal}`,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const selectAccount = () => {
|
|
69
|
+
const accountName = savedCommand?.account || readPositionalAccount()
|
|
70
|
+
if (accountName) {
|
|
71
|
+
const matched = portalAccounts.find((account) => account.name === accountName)
|
|
72
|
+
if (!matched) throw new Error(`未找到账号“${accountName}”,请检查账号名称或重新配置`)
|
|
73
|
+
return matched
|
|
74
|
+
}
|
|
75
|
+
return portalAccounts[0]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const portalAccount = selectAccount()
|
|
36
79
|
|
|
37
80
|
const configs = {
|
|
38
81
|
sjs: {
|
|
39
82
|
loginUrl: 'http://115.190.54.111:1880/passport/login/userLogin',
|
|
40
|
-
username:
|
|
41
|
-
password:
|
|
83
|
+
username: portalAccount.username,
|
|
84
|
+
password: portalAccount.password,
|
|
42
85
|
},
|
|
43
86
|
chenghua: {
|
|
44
87
|
loginUrl: 'https://www.chenghua-ai.com/passport/login/userLogin',
|
|
45
|
-
username:
|
|
46
|
-
password:
|
|
88
|
+
username: portalAccount.username,
|
|
89
|
+
password: portalAccount.password,
|
|
90
|
+
},
|
|
91
|
+
custom: {
|
|
92
|
+
loginUrl: portalAccount.loginUrl,
|
|
93
|
+
username: portalAccount.username,
|
|
94
|
+
password: portalAccount.password,
|
|
47
95
|
},
|
|
48
96
|
}
|
|
49
97
|
const config = configs[portal]
|
|
50
|
-
if (!config.username || !config.password) {
|
|
51
|
-
|
|
52
|
-
`尚未配置${portal === 'chenghua' ? '成华' : '石景山'}门户账号,请先运行 portal-dev config --portal ${portal}`,
|
|
53
|
-
)
|
|
54
|
-
}
|
|
98
|
+
if (!config.username || !config.password) throw new Error(`账号“${portalAccount.name}”的配置不完整,请重新配置`)
|
|
99
|
+
if (portal === 'custom' && !config.loginUrl) throw new Error(`账号“${portalAccount.name}”缺少登录页 URL,请重新配置`)
|
|
55
100
|
|
|
56
|
-
const localOrigin = 'http://localhost:5173'
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
101
|
+
const localOrigin = (savedCommand?.localOrigin || 'http://localhost:5173').replace(/\/$/, '')
|
|
102
|
+
const localPath = savedCommand?.localPath
|
|
103
|
+
const iframeHost = savedCommand?.iframeHost || 'hia.sjsdoubao.com:31118'
|
|
104
|
+
const iframePath = savedCommand?.iframePath || '/exhibition-hall'
|
|
105
|
+
const roomName = savedCommand?.roomName || '3D智能展厅智能体'
|
|
60
106
|
const chromeCandidates = {
|
|
61
107
|
darwin: [
|
|
62
108
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
@@ -82,7 +128,7 @@ const isReady = async () => {
|
|
|
82
128
|
}
|
|
83
129
|
|
|
84
130
|
let devServer
|
|
85
|
-
if (
|
|
131
|
+
if (devMode && !(await isReady())) {
|
|
86
132
|
const packageJson = JSON.parse(readFileSync(resolve(projectDir, 'package.json'), 'utf8'))
|
|
87
133
|
if (!packageJson.scripts?.dev) throw new Error(`目标项目没有 dev script:${projectDir}`)
|
|
88
134
|
const runner = existsSync(resolve(projectDir, 'bun.lock'))
|
|
@@ -100,6 +146,22 @@ if (!loginOnly && !(await isReady())) {
|
|
|
100
146
|
if (!(await isReady())) throw new Error(`本地开发服务启动超时:${localOrigin}`)
|
|
101
147
|
}
|
|
102
148
|
|
|
149
|
+
if (process.platform === 'darwin') {
|
|
150
|
+
await runInExistingChrome({
|
|
151
|
+
config,
|
|
152
|
+
devMode,
|
|
153
|
+
localOrigin,
|
|
154
|
+
localPath,
|
|
155
|
+
iframeHost,
|
|
156
|
+
iframePath,
|
|
157
|
+
roomName,
|
|
158
|
+
portal,
|
|
159
|
+
portalName:
|
|
160
|
+
portal === 'custom' ? `自定义网站“${portalAccount.name}”` : `${portal === 'chenghua' ? '成华' : '石景山'}门户`,
|
|
161
|
+
recognizeCaptcha,
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
103
165
|
const browser = await chromium.launch({ headless: false, executablePath })
|
|
104
166
|
const context = await browser.newContext()
|
|
105
167
|
const page = await context.newPage()
|
|
@@ -184,10 +246,58 @@ const login = async () => {
|
|
|
184
246
|
throw new Error('自动登录失败,已达到最多重试次数')
|
|
185
247
|
}
|
|
186
248
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
249
|
+
const loginWithoutCaptcha = async () => {
|
|
250
|
+
const usernameInput = await visibleLocator([
|
|
251
|
+
'input[autocomplete="username"]',
|
|
252
|
+
'input[name="username"]',
|
|
253
|
+
'input[name="account"]',
|
|
254
|
+
'input[name="login"]',
|
|
255
|
+
'input[name="user"]',
|
|
256
|
+
'input[name="email"]',
|
|
257
|
+
'input[type="email"]',
|
|
258
|
+
'input[type="tel"]',
|
|
259
|
+
'input[placeholder*="用户名"]',
|
|
260
|
+
'input[placeholder*="账号"]',
|
|
261
|
+
'input[placeholder*="邮箱"]',
|
|
262
|
+
'input[placeholder*="手机"]',
|
|
263
|
+
])
|
|
264
|
+
const passwordInput = await visibleLocator([
|
|
265
|
+
'input[autocomplete="current-password"]',
|
|
266
|
+
'input[name="password"]',
|
|
267
|
+
'input[type="password"]',
|
|
268
|
+
'input[placeholder*="密码"]',
|
|
269
|
+
])
|
|
270
|
+
if (!usernameInput || !passwordInput) throw new Error('未找到常见的用户名或密码输入框')
|
|
271
|
+
await usernameInput.fill(config.username)
|
|
272
|
+
await passwordInput.fill(config.password)
|
|
273
|
+
const button = await visibleLocator([
|
|
274
|
+
'button[type="submit"]',
|
|
275
|
+
'input[type="submit"]',
|
|
276
|
+
'button:has-text("登录")',
|
|
277
|
+
'[role="button"]:has-text("登录")',
|
|
278
|
+
'button:has-text("Login")',
|
|
279
|
+
'[role="button"]:has-text("Login")',
|
|
280
|
+
'button:has-text("Sign in")',
|
|
281
|
+
'[role="button"]:has-text("Sign in")',
|
|
282
|
+
])
|
|
283
|
+
if (!button) throw new Error('未找到常见的登录按钮')
|
|
284
|
+
const originalUrl = page.url()
|
|
285
|
+
await button.click()
|
|
286
|
+
await Promise.race([
|
|
287
|
+
page.waitForURL((url) => url.href !== originalUrl, { timeout: 15000 }),
|
|
288
|
+
passwordInput.waitFor({ state: 'hidden', timeout: 15000 }),
|
|
289
|
+
]).catch(() => undefined)
|
|
290
|
+
console.log(`自定义网站“${portalAccount.name}”已填写账号密码并点击登录。`)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (portal === 'custom') await loginWithoutCaptcha()
|
|
294
|
+
else if (page.url().includes('/passport/login/') || portal === 'chenghua') await login()
|
|
295
|
+
if (!devMode) {
|
|
296
|
+
if (portal === 'chenghua')
|
|
297
|
+
await page.goto('https://www.chenghua-ai.com/chat/pages/application', { waitUntil: 'domcontentloaded' })
|
|
298
|
+
console.log(
|
|
299
|
+
`${portal === 'custom' ? `自定义网站“${portalAccount.name}”` : `${portal === 'chenghua' ? '成华' : '石景山'}门户`}登录流程已完成,按 Ctrl+C 结束。`,
|
|
300
|
+
)
|
|
191
301
|
await new Promise(() => undefined)
|
|
192
302
|
}
|
|
193
303
|
|
|
@@ -226,7 +336,7 @@ for (let index = 0; index < 180; index += 1) {
|
|
|
226
336
|
}
|
|
227
337
|
|
|
228
338
|
if (!targetUrl) throw new Error(`未找到目标智能体入口:${roomName}`)
|
|
229
|
-
const destination = new URL(`${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`, localOrigin)
|
|
339
|
+
const destination = new URL(`${localPath || targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`, localOrigin)
|
|
230
340
|
await context.newPage().then((localPage) => localPage.goto(destination.href))
|
|
231
341
|
console.log(`门户本地调试已就绪:${localOrigin}${targetUrl.pathname}`)
|
|
232
342
|
console.log('Token 未打印、未写入文件。按 Ctrl+C 结束。')
|