@sybz-components/portal-dev 1.0.3 → 1.0.6

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 CHANGED
@@ -1,12 +1,11 @@
1
1
  # @sybz-components/portal-dev
2
2
 
3
- 成华、石景山门户自动登录与本地前端联调 CLI。支持自动启动项目开发服务、识别图形验证码、登录门户,以及携带门户 Token 打开本地页面。
3
+ 成华、石景山门户自动登录 CLI,也可登录只需用户名和密码的自定义网站,并按需启动石景山门户本地联调。
4
4
 
5
5
  ## 环境要求
6
6
 
7
7
  - Node.js 18+
8
8
  - Google Chrome 或 Microsoft Edge
9
- - 目标项目已安装依赖,并提供 `dev` script
10
9
 
11
10
  ## 安装
12
11
 
@@ -24,7 +23,7 @@ pnpm add -g @sybz-components/portal-dev
24
23
  portal-dev config
25
24
  ```
26
25
 
27
- 根据中文提示选择门户、输入账号和密码即可。密码输入时会显示为 `*`。账号配置属于当前电脑用户,与具体项目无关,只需配置一次。
26
+ 根据中文提示输入账号名称、用户名和密码即可。密码输入时会显示为 `*`。同一个门户可以重复执行该命令保存多个账号;账号名称相同时会更新原账号。
28
27
 
29
28
  默认配置文件位置:
30
29
 
@@ -33,7 +32,7 @@ portal-dev config
33
32
 
34
33
  程序会自动创建目录,并尽可能将配置文件权限设为仅当前用户可读写。
35
34
 
36
- 配置完成后直接运行:
35
+ 配置完成后可以在任意目录直接运行:
37
36
 
38
37
  ```bash
39
38
  portal-dev
@@ -44,27 +43,94 @@ portal-dev
44
43
  ```bash
45
44
  portal-dev config --portal sjs
46
45
  portal-dev config --portal chenghua
46
+ portal-dev config --portal custom
47
47
  ```
48
48
 
49
49
  ## 使用
50
50
 
51
- 石景山门户会启动本地开发服务、进入智能体样板间,并携带门户 Token 打开本地页面:
51
+ 只登录石景山门户,不读取或启动任何前端项目:
52
52
 
53
53
  ```bash
54
54
  portal-dev
55
55
  ```
56
56
 
57
- 成华门户完成自动登录后进入智能体广场:
57
+ 只登录成华门户并进入智能体广场,不读取或启动任何前端项目:
58
58
 
59
59
  ```bash
60
60
  portal-dev --portal chenghua
61
61
  ```
62
62
 
63
- 不在项目目录时可以指定项目路径:
63
+ 配置多个账号后,不写账号名称会直接使用数组中的第一个账号:
64
64
 
65
65
  ```bash
66
- portal-dev --portal sjs --project .
67
- portal-dev --portal chenghua --project .
66
+ portal-dev --portal sjs
67
+ ```
68
+
69
+ 在命令末尾写账号名称,可以使用指定账号:
70
+
71
+ ```bash
72
+ portal-dev --portal sjs 测试账号
73
+ ```
74
+
75
+ 默认快捷命令:
76
+
77
+ ```bash
78
+ portal-dev ds # 登录石景山
79
+ portal-dev sc # 登录成华
80
+ ```
81
+
82
+ 配置并登录普通网站:
83
+
84
+ ```bash
85
+ portal-dev config --portal custom
86
+ portal-dev --portal custom 我的账号
87
+ ```
88
+
89
+ 配置时依次输入登录页 URL、用户名和密码。登录时会自动查找常见的用户名、邮箱、密码输入框,以及“登录”、“Login”或“Sign in”按钮;该模式不会执行验证码识别、样板间搜索或 Token 跳转。
90
+
91
+ 别名保存在 JSON 中,可以通过向导新增登录或联调命令:
92
+
93
+ ```bash
94
+ portal-dev config command
95
+ ```
96
+
97
+ 例如配置一个别名为 `sjs-dev` 的联调命令后,只需运行:
98
+
99
+ ```bash
100
+ portal-dev sjs-dev
101
+ ```
102
+
103
+ 执行时会登录指定账号、启动本地项目、进入样板间搜索智能体,从匹配的 iframe 地址取得 Token,然后保留查询参数打开本地页面。
104
+
105
+ 配置文件中的账号和快捷命令格式:
106
+
107
+ ```json
108
+ {
109
+ "version": 4,
110
+ "profiles": {
111
+ "sjs": [{ "name": "我的账号", "username": "user1", "password": "******" }],
112
+ "chenghua": [{ "name": "测试账号", "username": "user2", "password": "******" }],
113
+ "custom": [
114
+ { "name": "内部系统", "loginUrl": "https://example.com/login", "username": "user3", "password": "******" }
115
+ ]
116
+ },
117
+ "commands": [
118
+ { "alias": "ds", "mode": "login", "portal": "sjs" },
119
+ { "alias": "sc", "mode": "login", "portal": "chenghua" },
120
+ {
121
+ "alias": "sjs-dev",
122
+ "mode": "dev",
123
+ "portal": "sjs",
124
+ "account": "测试账号",
125
+ "project": "/Users/你的名字/projects/agent-web",
126
+ "localOrigin": "http://localhost:5173",
127
+ "localPath": "/exhibition-hall",
128
+ "roomName": "3D智能展厅智能体",
129
+ "iframeHost": "hia.sjsdoubao.com:31118",
130
+ "iframePath": "/exhibition-hall"
131
+ }
132
+ ]
133
+ }
68
134
  ```
69
135
 
70
136
  查看命令帮助:
@@ -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 --portal sjs [--project <目录>]
39
- portal-dev --portal chenghua [--project <目录>]
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
- 之后在项目目录运行 portal-dev 即可,默认使用石景山门户。`)
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",
3
+ "version": "1.0.6",
4
4
  "description": "成华和石景山门户自动登录与本地前端联调 CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: portal-dev
3
- description: 启动前端项目的石景山门户本地联调,或自动登录成华门户。用户提到 dev:portal、门户联调、成华登录、石景山登录或从任意项目启动门户调试时使用。
3
+ description: 启动前端项目的石景山门户本地联调,自动登录成华门户,或使用已配置的 URL 和账号密码登录普通网站。用户提到 dev:portal、门户联调、成华登录、石景山登录、自定义登录或从任意项目启动门户调试时使用。
4
4
  ---
5
5
 
6
6
  # 门户本地调试
@@ -15,20 +15,35 @@ description: 启动前端项目的石景山门户本地联调,或自动登录
15
15
  portal-dev config --portal sjs
16
16
  ```
17
17
 
18
- 石景山门户联调:
18
+ 只登录石景山门户(不依赖前端项目):
19
19
 
20
20
  ```bash
21
- portal-dev --portal sjs --project <project-directory>
21
+ portal-dev --portal sjs
22
22
  ```
23
23
 
24
- 成华门户登录:
24
+ 只登录成华门户(不依赖前端项目):
25
25
 
26
26
  ```bash
27
- portal-dev --portal chenghua --project <project-directory>
27
+ portal-dev --portal chenghua
28
28
  ```
29
29
 
30
+ 登录已配置的普通网站(不走验证码和 Token 等特殊流程):
31
+
32
+ ```bash
33
+ portal-dev --portal custom <账号名称>
34
+ ```
35
+
36
+ 默认快捷命令:
37
+
38
+ ```bash
39
+ portal-dev ds
40
+ portal-dev sc
41
+ ```
42
+
43
+ 只有用户明确要求门户本地联调时才执行 JSON 中 `mode: "dev"` 的快捷命令,例如 `portal-dev sjs-dev`。项目、本地地址、路由、智能体名称和 iframe 识别参数均从该命令配置读取。脚本从门户 iframe URL 取得 Token 后,必须保留查询参数打开本地地址,且不得输出 Token。
44
+
30
45
  保持进程运行并告诉用户当前状态;只有用户明确要求停止时才终止。不得输出用户名、密码或门户 Token。
31
46
 
32
47
  ## 配置
33
48
 
34
- 账号密码只从当前用户的专属配置文件读取,`portal-dev config` 会交互式创建该文件。缺少凭据时,引导用户在本机终端运行 `portal-dev config`,不索要或展示具体密码。
49
+ 账号密码只从当前用户的专属配置文件读取,每个门户可保存多个账号。`portal-dev config` 会新增账号或按账号名称更新已有账号;登录命令不写账号名称时使用数组第一个账号,在命令末尾追加账号名称时使用指定账号。普通网站使用 `portal-dev config --portal custom` 配置登录 URL、账号名称、用户名和密码。缺少凭据时,引导用户在本机终端运行 `portal-dev config`,不索要或展示具体密码。
@@ -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: 1, profiles: {} }
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 { ...config, profiles: config.profiles && typeof config.profiles === 'object' ? config.profiles : {} }
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 writePortalProfile = (portal, profile) => {
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: 1,
29
- profiles: { ...config.profiles, [portal]: profile },
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 { writePortalProfile } from './config-file.mjs'
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 = portalAnswer === '2' || portalAnswer === 'chenghua' ? 'chenghua' : 'sjs'
59
- const portalName = portal === 'chenghua' ? '成华' : '石景山'
60
- const username = (await rl.question(`请输入${portalName}门户账号:`)).trim()
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 = writePortalProfile(portal, { username, password })
132
+ const { configPath, accountCount } = writePortalAccount(portal, {
133
+ name,
134
+ username,
135
+ password,
136
+ ...(loginUrl ? { loginUrl } : {}),
137
+ })
67
138
 
68
- console.log(`配置完成:${configPath}`)
69
- console.log(`现在进入前端项目目录,运行:${portal === 'sjs' ? 'portal-dev' : 'portal-dev --portal chenghua'}`)
139
+ console.log(`配置完成:${portalName}现有 ${accountCount} 个账号`)
140
+ console.log(`配置文件:${configPath}`)
141
+ console.log(`现在可以在任意目录运行:portal-dev --portal ${portal}`)
@@ -7,17 +7,37 @@ import { recognizeCaptcha } from './recognize-captcha.mjs'
7
7
 
8
8
  const sleep = (milliseconds) => new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds))
9
9
  const args = process.argv.slice(2)
10
+ const portalConfig = readPortalConfig()
11
+ const commandAlias = args[0] && args[0] !== 'dev' && !args[0].startsWith('-') ? args[0] : undefined
12
+ const savedCommand = commandAlias ? portalConfig.commands.find((command) => command?.alias === commandAlias) : undefined
13
+ if (commandAlias && !savedCommand)
14
+ throw new Error(`未找到快捷命令“${commandAlias}”,请运行 portal-dev config command 创建`)
10
15
  const readArg = (name) => {
11
16
  const index = args.indexOf(name)
12
17
  return index >= 0 ? args[index + 1] : undefined
13
18
  }
19
+ const readPositionalAccount = () => {
20
+ const valueOptions = new Set(['--portal', '--project'])
21
+ for (let index = 0; index < args.length; index += 1) {
22
+ const argument = args[index]
23
+ if (valueOptions.has(argument)) {
24
+ index += 1
25
+ continue
26
+ }
27
+ if (argument === 'dev' || argument === commandAlias || argument.startsWith('-')) continue
28
+ return argument
29
+ }
30
+ return undefined
31
+ }
14
32
 
15
- const portal = readArg('--portal') || 'sjs'
16
- if (!['sjs', 'chenghua'].includes(portal)) throw new Error(`不支持的门户:${portal},可选值为 sjs、chenghua`)
17
- const loginOnly = args.includes('--login-only') || portal === 'chenghua'
33
+ const portal = savedCommand?.portal || readArg('--portal') || 'sjs'
34
+ if (!['sjs', 'chenghua', 'custom'].includes(portal))
35
+ throw new Error(`不支持的门户:${portal},可选值为 sjs、chenghua、custom`)
36
+ const devMode = savedCommand?.mode === 'dev' || args[0] === 'dev'
37
+ if (devMode && portal !== 'sjs') throw new Error('门户联调目前只支持石景山;成华和自定义命令只负责登录')
18
38
 
19
39
  const findProject = () => {
20
- const explicit = readArg('--project')
40
+ const explicit = savedCommand?.project || readArg('--project')
21
41
  if (explicit) return resolve(explicit)
22
42
  let current = process.cwd()
23
43
  while (true) {
@@ -29,34 +49,59 @@ const findProject = () => {
29
49
  throw new Error('未找到前端项目,请先进入项目目录,或使用 --project <路径> 指定')
30
50
  }
31
51
 
32
- const projectDir = findProject()
33
- if (!existsSync(resolve(projectDir, 'package.json')))
52
+ const projectDir = devMode ? findProject() : undefined
53
+ if (projectDir && !existsSync(resolve(projectDir, 'package.json')))
34
54
  throw new Error(`目标项目不存在或缺少 package.json:${projectDir}`)
35
- const portalProfile = readPortalConfig().profiles[portal] || {}
55
+ const configuredAccounts = portalConfig.profiles[portal]
56
+ const portalAccounts = Array.isArray(configuredAccounts)
57
+ ? configuredAccounts.filter(
58
+ (account) => account?.name && typeof account.username === 'string' && typeof account.password === 'string',
59
+ )
60
+ : []
61
+ if (!portalAccounts.length) {
62
+ throw new Error(
63
+ `尚未配置${portal === 'custom' ? '自定义网站' : portal === 'chenghua' ? '成华' : '石景山'}账号,请先运行 portal-dev config --portal ${portal}`,
64
+ )
65
+ }
66
+
67
+ const selectAccount = () => {
68
+ const accountName = savedCommand?.account || readPositionalAccount()
69
+ if (accountName) {
70
+ const matched = portalAccounts.find((account) => account.name === accountName)
71
+ if (!matched) throw new Error(`未找到账号“${accountName}”,请检查账号名称或重新配置`)
72
+ return matched
73
+ }
74
+ return portalAccounts[0]
75
+ }
76
+
77
+ const portalAccount = selectAccount()
36
78
 
37
79
  const configs = {
38
80
  sjs: {
39
81
  loginUrl: 'http://115.190.54.111:1880/passport/login/userLogin',
40
- username: portalProfile.username,
41
- password: portalProfile.password,
82
+ username: portalAccount.username,
83
+ password: portalAccount.password,
42
84
  },
43
85
  chenghua: {
44
86
  loginUrl: 'https://www.chenghua-ai.com/passport/login/userLogin',
45
- username: portalProfile.username,
46
- password: portalProfile.password,
87
+ username: portalAccount.username,
88
+ password: portalAccount.password,
89
+ },
90
+ custom: {
91
+ loginUrl: portalAccount.loginUrl,
92
+ username: portalAccount.username,
93
+ password: portalAccount.password,
47
94
  },
48
95
  }
49
96
  const config = configs[portal]
50
- if (!config.username || !config.password) {
51
- throw new Error(
52
- `尚未配置${portal === 'chenghua' ? '成华' : '石景山'}门户账号,请先运行 portal-dev config --portal ${portal}`,
53
- )
54
- }
97
+ if (!config.username || !config.password) throw new Error(`账号“${portalAccount.name}”的配置不完整,请重新配置`)
98
+ if (portal === 'custom' && !config.loginUrl) throw new Error(`账号“${portalAccount.name}”缺少登录页 URL,请重新配置`)
55
99
 
56
- const localOrigin = 'http://localhost:5173'
57
- const iframeHost = 'hia.sjsdoubao.com:31118'
58
- const iframePath = '/exhibition-hall'
59
- const roomName = '3D智能展厅智能体'
100
+ const localOrigin = (savedCommand?.localOrigin || 'http://localhost:5173').replace(/\/$/, '')
101
+ const localPath = savedCommand?.localPath
102
+ const iframeHost = savedCommand?.iframeHost || 'hia.sjsdoubao.com:31118'
103
+ const iframePath = savedCommand?.iframePath || '/exhibition-hall'
104
+ const roomName = savedCommand?.roomName || '3D智能展厅智能体'
60
105
  const chromeCandidates = {
61
106
  darwin: [
62
107
  '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
@@ -82,7 +127,7 @@ const isReady = async () => {
82
127
  }
83
128
 
84
129
  let devServer
85
- if (!loginOnly && !(await isReady())) {
130
+ if (devMode && !(await isReady())) {
86
131
  const packageJson = JSON.parse(readFileSync(resolve(projectDir, 'package.json'), 'utf8'))
87
132
  if (!packageJson.scripts?.dev) throw new Error(`目标项目没有 dev script:${projectDir}`)
88
133
  const runner = existsSync(resolve(projectDir, 'bun.lock'))
@@ -184,10 +229,58 @@ const login = async () => {
184
229
  throw new Error('自动登录失败,已达到最多重试次数')
185
230
  }
186
231
 
187
- if (page.url().includes('/passport/login/') || portal === 'chenghua') await login()
188
- if (loginOnly) {
189
- await page.goto('https://www.chenghua-ai.com/chat/pages/application', { waitUntil: 'domcontentloaded' })
190
- console.log('成华门户登录已完成,按 Ctrl+C 结束。')
232
+ const loginWithoutCaptcha = async () => {
233
+ const usernameInput = await visibleLocator([
234
+ 'input[autocomplete="username"]',
235
+ 'input[name="username"]',
236
+ 'input[name="account"]',
237
+ 'input[name="login"]',
238
+ 'input[name="user"]',
239
+ 'input[name="email"]',
240
+ 'input[type="email"]',
241
+ 'input[type="tel"]',
242
+ 'input[placeholder*="用户名"]',
243
+ 'input[placeholder*="账号"]',
244
+ 'input[placeholder*="邮箱"]',
245
+ 'input[placeholder*="手机"]',
246
+ ])
247
+ const passwordInput = await visibleLocator([
248
+ 'input[autocomplete="current-password"]',
249
+ 'input[name="password"]',
250
+ 'input[type="password"]',
251
+ 'input[placeholder*="密码"]',
252
+ ])
253
+ if (!usernameInput || !passwordInput) throw new Error('未找到常见的用户名或密码输入框')
254
+ await usernameInput.fill(config.username)
255
+ await passwordInput.fill(config.password)
256
+ const button = await visibleLocator([
257
+ 'button[type="submit"]',
258
+ 'input[type="submit"]',
259
+ 'button:has-text("登录")',
260
+ '[role="button"]:has-text("登录")',
261
+ 'button:has-text("Login")',
262
+ '[role="button"]:has-text("Login")',
263
+ 'button:has-text("Sign in")',
264
+ '[role="button"]:has-text("Sign in")',
265
+ ])
266
+ if (!button) throw new Error('未找到常见的登录按钮')
267
+ const originalUrl = page.url()
268
+ await button.click()
269
+ await Promise.race([
270
+ page.waitForURL((url) => url.href !== originalUrl, { timeout: 15000 }),
271
+ passwordInput.waitFor({ state: 'hidden', timeout: 15000 }),
272
+ ]).catch(() => undefined)
273
+ console.log(`自定义网站“${portalAccount.name}”已填写账号密码并点击登录。`)
274
+ }
275
+
276
+ if (portal === 'custom') await loginWithoutCaptcha()
277
+ else if (page.url().includes('/passport/login/') || portal === 'chenghua') await login()
278
+ if (!devMode) {
279
+ if (portal === 'chenghua')
280
+ await page.goto('https://www.chenghua-ai.com/chat/pages/application', { waitUntil: 'domcontentloaded' })
281
+ console.log(
282
+ `${portal === 'custom' ? `自定义网站“${portalAccount.name}”` : `${portal === 'chenghua' ? '成华' : '石景山'}门户`}登录流程已完成,按 Ctrl+C 结束。`,
283
+ )
191
284
  await new Promise(() => undefined)
192
285
  }
193
286
 
@@ -226,7 +319,7 @@ for (let index = 0; index < 180; index += 1) {
226
319
  }
227
320
 
228
321
  if (!targetUrl) throw new Error(`未找到目标智能体入口:${roomName}`)
229
- const destination = new URL(`${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`, localOrigin)
322
+ const destination = new URL(`${localPath || targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`, localOrigin)
230
323
  await context.newPage().then((localPage) => localPage.goto(destination.href))
231
324
  console.log(`门户本地调试已就绪:${localOrigin}${targetUrl.pathname}`)
232
325
  console.log('Token 未打印、未写入文件。按 Ctrl+C 结束。')