jpsclient 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/bin/jpsclient +28 -0
  4. data/lib/jpsclient/api/app_level.rb +138 -0
  5. data/lib/jpsclient/api/app_resource.rb +164 -0
  6. data/lib/jpsclient/api/app_resource_version.rb +52 -0
  7. data/lib/jpsclient/api/apple_account.rb +140 -0
  8. data/lib/jpsclient/api/application.rb +268 -0
  9. data/lib/jpsclient/api/application_category.rb +120 -0
  10. data/lib/jpsclient/api/application_design.rb +118 -0
  11. data/lib/jpsclient/api/application_income.rb +54 -0
  12. data/lib/jpsclient/api/application_sales.rb +52 -0
  13. data/lib/jpsclient/api/application_version.rb +77 -0
  14. data/lib/jpsclient/api/assets_category.rb +120 -0
  15. data/lib/jpsclient/api/bug.rb +140 -0
  16. data/lib/jpsclient/api/cert.rb +27 -0
  17. data/lib/jpsclient/api/client.rb +268 -0
  18. data/lib/jpsclient/api/collect.rb +52 -0
  19. data/lib/jpsclient/api/collection.rb +162 -0
  20. data/lib/jpsclient/api/commit_log.rb +104 -0
  21. data/lib/jpsclient/api/creative.rb +52 -0
  22. data/lib/jpsclient/api/custom_application.rb +230 -0
  23. data/lib/jpsclient/api/custom_application_web.rb +52 -0
  24. data/lib/jpsclient/api/document_text.rb +30 -0
  25. data/lib/jpsclient/api/experience.rb +143 -0
  26. data/lib/jpsclient/api/experience_category.rb +97 -0
  27. data/lib/jpsclient/api/fgui_export.rb +52 -0
  28. data/lib/jpsclient/api/file.rb +84 -0
  29. data/lib/jpsclient/api/game_assets.rb +140 -0
  30. data/lib/jpsclient/api/healthy.rb +30 -0
  31. data/lib/jpsclient/api/icon_and_snapshot.rb +54 -0
  32. data/lib/jpsclient/api/idea.rb +121 -0
  33. data/lib/jpsclient/api/jssdk.rb +30 -0
  34. data/lib/jpsclient/api/lark_bitable.rb +30 -0
  35. data/lib/jpsclient/api/lark_chat_group.rb +30 -0
  36. data/lib/jpsclient/api/lark_comment.rb +118 -0
  37. data/lib/jpsclient/api/lark_task.rb +140 -0
  38. data/lib/jpsclient/api/lark_task_list.rb +118 -0
  39. data/lib/jpsclient/api/lark_task_section.rb +74 -0
  40. data/lib/jpsclient/api/lark_user.rb +30 -0
  41. data/lib/jpsclient/api/lark_wiki_node.rb +30 -0
  42. data/lib/jpsclient/api/lark_wiki_space.rb +30 -0
  43. data/lib/jpsclient/api/lazy_client.rb +290 -0
  44. data/lib/jpsclient/api/login.rb +96 -0
  45. data/lib/jpsclient/api/m3u8.rb +30 -0
  46. data/lib/jpsclient/api/menu.rb +143 -0
  47. data/lib/jpsclient/api/modular_client.rb +228 -0
  48. data/lib/jpsclient/api/project.rb +46 -0
  49. data/lib/jpsclient/api/project_package.rb +249 -0
  50. data/lib/jpsclient/api/publisher.rb +165 -0
  51. data/lib/jpsclient/api/publisher_category.rb +142 -0
  52. data/lib/jpsclient/api/publisher_group.rb +118 -0
  53. data/lib/jpsclient/api/publisher_group_category.rb +120 -0
  54. data/lib/jpsclient/api/requirements.rb +143 -0
  55. data/lib/jpsclient/api/requirements_category.rb +97 -0
  56. data/lib/jpsclient/api/resource_category.rb +120 -0
  57. data/lib/jpsclient/api/role.rb +165 -0
  58. data/lib/jpsclient/api/simple_search.rb +162 -0
  59. data/lib/jpsclient/api/sketch.rb +74 -0
  60. data/lib/jpsclient/api/sketch_category.rb +97 -0
  61. data/lib/jpsclient/api/sov.rb +30 -0
  62. data/lib/jpsclient/api/statistics.rb +30 -0
  63. data/lib/jpsclient/api/store.rb +30 -0
  64. data/lib/jpsclient/api/survey.rb +143 -0
  65. data/lib/jpsclient/api/survey_category.rb +97 -0
  66. data/lib/jpsclient/api/tag.rb +142 -0
  67. data/lib/jpsclient/api/tool.rb +121 -0
  68. data/lib/jpsclient/api/tool_category.rb +120 -0
  69. data/lib/jpsclient/api/trending.rb +30 -0
  70. data/lib/jpsclient/api/ud_id.rb +118 -0
  71. data/lib/jpsclient/api/user.rb +99 -0
  72. data/lib/jpsclient/api/util.rb +30 -0
  73. data/lib/jpsclient/api/video_cover.rb +30 -0
  74. data/lib/jpsclient/api/webhook.rb +118 -0
  75. data/lib/jpsclient/api/workflow.rb +118 -0
  76. data/lib/jpsclient/auth/auth.rb +676 -0
  77. data/lib/jpsclient/auth/token.rb +209 -0
  78. data/lib/jpsclient/base/api_config.rb +225 -0
  79. data/lib/jpsclient/base/exception.rb +5 -0
  80. data/lib/jpsclient/http/http_client.rb +148 -0
  81. data/lib/jpsclient/upload/upload_client.rb +334 -0
  82. data/lib/jpsclient/upload/upload_config.rb +128 -0
  83. data/lib/jpsclient/upload/upload_progress.rb +73 -0
  84. data/lib/jpsclient/utils/aes.rb +49 -0
  85. data/lib/jpsclient/utils/logger.rb +38 -0
  86. data/lib/jpsclient/version.rb +3 -0
  87. data/lib/jpsclient.rb +34 -0
  88. metadata +269 -0
@@ -0,0 +1,676 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'webrick'
4
+ require 'json'
5
+ require 'net/http'
6
+ require 'uri'
7
+ require 'securerandom'
8
+ require 'digest'
9
+ require 'base64'
10
+ require 'fileutils'
11
+ require 'jpsclient/utils/aes'
12
+ require 'jpsclient/base/exception'
13
+ require 'jpsclient/utils/logger'
14
+
15
+ module JPSClient
16
+
17
+ # JPS 登录配置类
18
+ class LoginConfig
19
+ attr_accessor :client_id, :feishu_auth_url, :redirect_uri, :api_endpoint, :state, :server_port
20
+ attr_accessor :aes_key, :token_verify_endpoint
21
+
22
+ def initialize
23
+ # 所有配置必须从外部配置文件加载,不提供硬编码的默认值
24
+ @client_id = nil
25
+ @feishu_auth_url = nil
26
+ @redirect_uri = nil
27
+ @api_endpoint = nil
28
+ @state = 'client_login' # 仅此项可有默认值
29
+ @server_port = 8898 # 仅此项可有默认值
30
+ @aes_key = nil # AES 加密密钥,必需配置
31
+ @token_verify_endpoint = nil # Token 验证端点,必需配置
32
+ end
33
+
34
+ # 从 JSON 加载配置
35
+ def self.from_json(json_data)
36
+ config = new
37
+ return config if json_data.nil?
38
+
39
+ data = json_data.is_a?(String) ? JSON.parse(json_data) : json_data
40
+
41
+ # 加载所有配置字段
42
+ config.client_id = data['client_id'] if data['client_id']
43
+ config.feishu_auth_url = data['feishu_auth_url'] if data['feishu_auth_url']
44
+ config.redirect_uri = data['redirect_uri'] if data['redirect_uri']
45
+ config.api_endpoint = data['api_endpoint'] if data['api_endpoint']
46
+ config.state = data['state'] if data['state']
47
+ config.server_port = data['server_port'] if data['server_port']
48
+ config.token_verify_endpoint = data['token_verify_endpoint'] if data['token_verify_endpoint']
49
+ config.aes_key = data['aes_key'] if data['aes_key']
50
+
51
+ config
52
+ end
53
+
54
+ # 从配置文件加载
55
+ def self.from_file(file_path)
56
+ return new unless File.exist?(file_path)
57
+ from_json(File.read(file_path))
58
+ end
59
+ end
60
+
61
+ class Auth
62
+ attr_reader :access_token, :username, :expires_at
63
+
64
+ def initialize(config = nil)
65
+ # 只接受 LoginConfig 对象或默认配置
66
+ @config = config || LoginConfig.new
67
+
68
+ # 从配置对象获取值
69
+ @client_id = @config.client_id
70
+ @feishu_auth_url = @config.feishu_auth_url
71
+ @redirect_uri = @config.redirect_uri
72
+ @api_endpoint = @config.api_endpoint
73
+ @state = @config.state
74
+ @server_port = @config.server_port
75
+
76
+ @scope_list = [
77
+ 'task:task:write',
78
+ 'task:section:write',
79
+ 'task:custom_field:write',
80
+ 'task:tasklist:write'
81
+ ]
82
+
83
+ @access_token = nil
84
+ @username = nil
85
+ @expires_at = nil
86
+
87
+ # 调试模式,通过环境变量控制
88
+ @verbose = ENV['PINDO_DEBUG'] == 'true'
89
+ end
90
+
91
+ # 主登录入口
92
+ def login
93
+ puts "🔐 需要登录 JPS..."
94
+ result = authorize_and_login
95
+
96
+ # 如果用户主动取消,返回特殊标识
97
+ return :user_cancelled if result == :user_cancelled
98
+
99
+ return result
100
+ end
101
+
102
+ # 启动授权流程(保留兼容性)
103
+ def authorize
104
+ login
105
+ end
106
+
107
+ # 获取用户名(登录后可用)
108
+ def get_username
109
+ @username
110
+ end
111
+
112
+ # 获取 token 数据(供 Client 使用)
113
+ def get_token_data
114
+ return nil unless @access_token
115
+
116
+ {
117
+ 'token' => @access_token,
118
+ 'username' => @username,
119
+ 'expires_at' => @expires_at
120
+ }
121
+ end
122
+
123
+ # 完整的授权和登录流程
124
+ def authorize_and_login
125
+ # 构建授权 URL
126
+ authorization_uri = build_authorization_uri
127
+ puts "正在打开浏览器进行飞书 OAuth 授权..."
128
+ puts "\n授权 URL(如自动打开失败,请手动复制下面的链接到浏览器):"
129
+ puts "=" * 80
130
+ puts authorization_uri
131
+ puts "=" * 80
132
+ puts ""
133
+
134
+ # 在浏览器中打开授权 URL
135
+ open_browser(authorization_uri)
136
+
137
+ # 启动本地服务器处理回调
138
+ code = start_callback_server
139
+
140
+ # 如果自动获取失败,提示用户手动输入
141
+ if code.nil?
142
+ loop do
143
+ puts "\n自动获取授权码失败,请选择:"
144
+ puts "1. 输入授权码 (直接复制 'code=' 后面的内容)"
145
+ puts "2. 输入完整回调 URL"
146
+ puts "3. 重新打开授权网页"
147
+ puts "4. 退出"
148
+ print "> "
149
+
150
+ begin
151
+ choice = STDIN.gets&.chomp
152
+
153
+ # 处理 Ctrl+C 中断
154
+ if choice.nil?
155
+ puts "\n用户中断操作"
156
+ return :user_cancelled
157
+ end
158
+
159
+ case choice
160
+ when "1"
161
+ puts "请输入授权码:"
162
+ print "> "
163
+ code_input = STDIN.gets&.chomp
164
+ if code_input.nil?
165
+ puts "用户中断操作"
166
+ return :user_cancelled
167
+ elsif !code_input.empty?
168
+ code = code_input
169
+ break
170
+ else
171
+ puts "授权码不能为空,请重新选择"
172
+ end
173
+ when "2"
174
+ puts "请输入完整回调 URL:"
175
+ print "> "
176
+ url_input = STDIN.gets&.chomp
177
+ if url_input.nil?
178
+ puts "用户中断操作"
179
+ return :user_cancelled
180
+ elsif url_input.start_with?("http")
181
+ # 尝试从 URL 中提取 code
182
+ begin
183
+ uri = URI(url_input)
184
+ query_params = URI.decode_www_form(uri.query || '').to_h
185
+ code = query_params['code']
186
+ if code
187
+ puts "✓ 从 URL 中成功提取授权码"
188
+ break
189
+ else
190
+ puts "✗ URL 中没有找到授权码,请重新选择"
191
+ end
192
+ rescue => e
193
+ puts "✗ 无法从 URL 中提取授权码: #{e.message}"
194
+ end
195
+ else
196
+ puts "✗ 无效的 URL 格式,请重新选择"
197
+ end
198
+ when "3"
199
+ # 重新打开授权网页
200
+ puts "正在重新打开授权网页..."
201
+ open_browser(authorization_uri)
202
+ # 重新启动服务器尝试获取授权码
203
+ code = start_callback_server
204
+ if code
205
+ break
206
+ end
207
+ # 如果还是失败,继续循环让用户选择
208
+ when "4"
209
+ puts "已退出授权流程"
210
+ return :user_cancelled
211
+ else
212
+ puts "无效的选择,请输入 1-4 之间的数字"
213
+ end
214
+ rescue Interrupt
215
+ puts "\n\n用户中断操作"
216
+ return :user_cancelled
217
+ end
218
+ end
219
+ end
220
+
221
+ if code
222
+ if exchange_code_for_token(code)
223
+ puts "✓ JPS 登录成功!用户名: #{@username}"
224
+ return true
225
+ end
226
+ return false
227
+ else
228
+ puts "✗ 授权失败"
229
+ return false
230
+ end
231
+ end
232
+
233
+ # 检查 token 是否有效
234
+ def validate_token(token = nil)
235
+ token_to_check = token || @access_token
236
+ return false unless token_to_check
237
+
238
+ # 检查过期时间
239
+ if @expires_at && Time.now.to_i > @expires_at
240
+ puts "Token 已过期,需要重新登录"
241
+ return false
242
+ end
243
+
244
+ # 简单验证(实际项目中可以调用 API 验证)
245
+ return true
246
+ end
247
+
248
+ # 验证Pgyer令牌(保留兼容性)
249
+ def validate_pgyer_token(token = nil, expires_at = nil)
250
+ validate_token(token)
251
+ end
252
+
253
+ # 使用授权码换取 token
254
+ def exchange_code_for_token(code)
255
+ begin
256
+ request_data = {
257
+ 'code' => code,
258
+ 'redirectUri' => @redirect_uri,
259
+ 'scope' => @scope_list.join(' ')
260
+ }
261
+
262
+ uri = URI(@api_endpoint)
263
+ http = Net::HTTP.new(uri.host, uri.port)
264
+ http.use_ssl = (uri.scheme == 'https')
265
+
266
+ request = Net::HTTP::Post.new(uri)
267
+ request['Content-Type'] = 'application/json'
268
+ request.body = request_data.to_json
269
+
270
+ puts "正在请求 JPS API: #{@api_endpoint}" if @verbose
271
+ response = http.request(request)
272
+
273
+ puts "API 响应状态码: #{response.code}" if @verbose
274
+
275
+ if response.body
276
+ result = JSON.parse(response.body)
277
+ puts "API 响应: #{result.inspect}" if @verbose
278
+
279
+ if result['meta'] && result['meta']['code'] == 200 && result['data']
280
+ data = result['data']
281
+ @access_token = data['token'] if data['token']
282
+ @username = data['username'] if data['username']
283
+ # 设置过期时间为 6 天后
284
+ @expires_at = Time.now.to_i + 6 * 24 * 60 * 60
285
+
286
+ return true if @access_token
287
+ else
288
+ error_msg = result['meta'] && result['meta']['message'] ? result['meta']['message'] : '未知错误'
289
+ puts "JPS 登录失败: #{error_msg}"
290
+ end
291
+ else
292
+ puts "API 返回空响应" if @verbose
293
+ end
294
+
295
+ return false
296
+ rescue => e
297
+ puts "请求 JPS API 失败: #{e.class} - #{e.message}"
298
+ return false
299
+ end
300
+ end
301
+
302
+
303
+ private
304
+
305
+ # 获取跨平台的 null 设备路径
306
+ def get_null_device
307
+ case RbConfig::CONFIG['host_os']
308
+ when /mswin|mingw|cygwin/
309
+ 'NUL'
310
+ else
311
+ '/dev/null'
312
+ end
313
+ end
314
+
315
+
316
+ # 构建授权URI
317
+ def build_authorization_uri
318
+ uri = URI(@feishu_auth_url)
319
+
320
+ params = {
321
+ 'client_id' => @client_id,
322
+ 'redirect_uri' => @redirect_uri,
323
+ 'response_type' => 'code',
324
+ 'state' => @state,
325
+ 'scope' => @scope_list.join(' ')
326
+ }
327
+
328
+ uri.query = URI.encode_www_form(params)
329
+ uri.to_s
330
+ end
331
+
332
+ # 在浏览器中打开URL
333
+ def open_browser(url)
334
+ case RbConfig::CONFIG['host_os']
335
+ when /mswin|mingw|cygwin/
336
+ # Windows: 使用双引号包围URL避免参数解析问题
337
+ system("start \"\" \"#{url}\"")
338
+ when /darwin/
339
+ system("open", url)
340
+ when /linux|bsd/
341
+ system("xdg-open", url)
342
+ else
343
+ puts "无法自动打开浏览器,请手动访问: #{url}"
344
+ end
345
+ end
346
+
347
+ # 启动本地服务器处理回调
348
+ def start_callback_server
349
+ code = nil
350
+
351
+ # 检查并处理端口占用
352
+ unless ensure_port_available(@server_port)
353
+ puts "✗ 无法使用端口 #{@server_port},登录失败"
354
+ return nil
355
+ end
356
+
357
+ puts "启动本地服务器,监听端口 #{@server_port}..."
358
+ puts "提示:按 Ctrl+C 可以中断并获得更多选择"
359
+ puts "🔄 正在使用飞书身份登录 JPS..."
360
+
361
+ begin
362
+ server = WEBrick::HTTPServer.new(
363
+ Port: @server_port,
364
+ BindAddress: '127.0.0.1',
365
+ Logger: WEBrick::Log.new(get_null_device),
366
+ AccessLog: []
367
+ )
368
+ rescue Errno::EADDRINUSE
369
+ puts "✗ 端口 #{@server_port} 仍被占用,无法启动服务器"
370
+ return nil
371
+ rescue Errno::ENOENT
372
+ puts "✗ 启动服务器失败: 系统找不到指定的路径或文件"
373
+ return nil
374
+ rescue => e
375
+ puts "✗ 启动服务器失败: #{e.message}"
376
+ if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
377
+ puts " 提示: 如果是Windows系统,请确保防火墙允许Ruby访问网络"
378
+ end
379
+ return nil
380
+ end
381
+
382
+ # 处理根路径的请求
383
+ server.mount_proc '/' do |req, res|
384
+ begin
385
+ # 解析请求参数
386
+ query_params = URI.decode_www_form(req.query_string || '').to_h
387
+ puts "接收到回调请求,参数: #{query_params.inspect}" if @verbose
388
+
389
+ if query_params['error']
390
+ puts "授权错误: #{query_params['error']}" if @verbose
391
+ res.content_type = "text/html; charset=UTF-8"
392
+ res.body = build_error_page(query_params['error'])
393
+ elsif query_params['code']
394
+ code = query_params['code']
395
+ # 静默处理,避免重复输出
396
+ res.content_type = "text/html; charset=UTF-8"
397
+ res.body = build_success_page
398
+ else
399
+ # 忽略没有参数的请求(如 favicon.ico 等)
400
+ res.content_type = "text/html; charset=UTF-8"
401
+ res.body = "等待授权中..."
402
+ end
403
+
404
+ # 4秒后关闭服务器
405
+ Thread.new do
406
+ sleep 4
407
+ server.shutdown
408
+ end
409
+ rescue => e
410
+ puts "处理请求时出错: #{e.class} - #{e.message}" if @verbose
411
+ res.content_type = "text/html; charset=UTF-8"
412
+ res.body = build_error_page("处理请求时出错")
413
+
414
+ Thread.new do
415
+ sleep 4
416
+ server.shutdown
417
+ end
418
+ end
419
+ end
420
+
421
+ # 捕获 Ctrl+C
422
+ trap('INT') { server.shutdown }
423
+
424
+ # 在线程中运行服务器,最多等待 3 分钟
425
+ thread = Thread.new { server.start }
426
+ begin
427
+ thread.join(180) # 最多等待 3 分钟
428
+ rescue => e
429
+ puts "服务器等待超时"
430
+ server.shutdown
431
+ end
432
+
433
+ code
434
+ end
435
+
436
+ # 确保端口可用,处理端口占用问题
437
+ def ensure_port_available(port)
438
+ return true unless port_occupied?(port)
439
+
440
+ puts "⚠️ 端口 #{port} 被占用,正在检查占用进程..."
441
+
442
+ # 获取占用端口的进程信息
443
+ process_info = get_port_process_info(port)
444
+
445
+ if process_info.nil?
446
+ puts "✗ 无法获取端口占用信息"
447
+ return false
448
+ end
449
+
450
+ puts "占用进程信息:"
451
+ puts " PID: #{process_info[:pid]}"
452
+ puts " 进程名: #{process_info[:name]}"
453
+ puts " 命令: #{process_info[:command]}"
454
+
455
+ # 检查是否是自己的进程(可能是之前未正常关闭的实例)
456
+ if process_info[:name].include?('ruby') && (process_info[:command].include?('pindo') || process_info[:command].include?('jps'))
457
+ puts "检测到可能是 Pindo/JPS 之前的遗留进程"
458
+ if ask_user_kill_process?
459
+ return kill_process_by_pid(process_info[:pid])
460
+ end
461
+ else
462
+ puts "端口被其他应用程序占用"
463
+ if ask_user_kill_process?
464
+ return kill_process_by_pid(process_info[:pid])
465
+ end
466
+ end
467
+
468
+ return false
469
+ end
470
+
471
+ # 检查端口是否被占用
472
+ def port_occupied?(port)
473
+ require 'socket'
474
+
475
+ begin
476
+ server = TCPServer.new('127.0.0.1', port)
477
+ server.close
478
+ false # 端口未被占用
479
+ rescue Errno::EADDRINUSE
480
+ true # 端口被占用
481
+ rescue => e
482
+ puts "⚠️ 检查端口时出错: #{e.message}"
483
+ true # 出错时假设端口被占用,避免冲突
484
+ end
485
+ end
486
+
487
+ # 获取占用指定端口的进程信息
488
+ def get_port_process_info(port)
489
+ begin
490
+ # macOS/Linux 使用 lsof 命令
491
+ if system('which lsof > /dev/null 2>&1')
492
+ output = `lsof -ti :#{port} 2>/dev/null`.strip
493
+ return nil if output.empty?
494
+
495
+ pid = output.split("\n").first
496
+ return nil if pid.nil? || pid.empty?
497
+
498
+ # 获取进程详细信息
499
+ ps_output = `ps -p #{pid} -o pid,comm,args 2>/dev/null`.lines
500
+ return nil if ps_output.length < 2
501
+
502
+ process_line = ps_output[1].strip
503
+ parts = process_line.split(/\s+/, 3)
504
+
505
+ return {
506
+ pid: parts[0],
507
+ name: parts[1] || 'unknown',
508
+ command: parts[2] || 'unknown'
509
+ }
510
+ end
511
+
512
+ # Windows 使用 netstat 命令
513
+ if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
514
+ output = `netstat -ano | findstr :#{port}`.strip
515
+ return nil if output.empty?
516
+
517
+ lines = output.split("\n")
518
+ listening_line = lines.find { |line| line.include?('LISTENING') }
519
+ return nil unless listening_line
520
+
521
+ pid = listening_line.split.last
522
+ return nil if pid.nil? || pid.empty?
523
+
524
+ # 获取进程名称
525
+ task_output = `tasklist /FI "PID eq #{pid}" /FO CSV /NH 2>nul`.strip
526
+ if !task_output.empty?
527
+ process_name = task_output.split(',').first.gsub('"', '')
528
+ return {
529
+ pid: pid,
530
+ name: process_name,
531
+ command: process_name
532
+ }
533
+ end
534
+ end
535
+
536
+ return nil
537
+ rescue => e
538
+ puts "获取进程信息时出错: #{e.message}"
539
+ return nil
540
+ end
541
+ end
542
+
543
+ # 询问用户是否终止占用端口的进程
544
+ def ask_user_kill_process?
545
+ puts "\n是否终止占用端口的进程?"
546
+ puts " y/yes - 终止进程并继续 (可能影响其他应用)"
547
+ puts " n/no - 取消登录 (默认)"
548
+ print "> "
549
+
550
+ begin
551
+ response = STDIN.gets&.chomp&.downcase
552
+ return ['y', 'yes'].include?(response)
553
+ rescue Interrupt
554
+ puts "\n\n用户中断操作"
555
+ return false
556
+ rescue => e
557
+ puts "无法读取用户输入: #{e.message}"
558
+ return false
559
+ end
560
+ end
561
+
562
+ # 根据 PID 终止进程
563
+ def kill_process_by_pid(pid)
564
+ return false if pid.nil? || pid.empty?
565
+
566
+ begin
567
+ puts "正在终止进程 #{pid}..."
568
+
569
+ # 跨平台的进程终止命令
570
+ if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
571
+ # Windows
572
+ success = system("taskkill /PID #{pid} /F >nul 2>&1")
573
+ else
574
+ # macOS/Linux - 先尝试优雅终止,再强制终止
575
+ success = system("kill -TERM #{pid} 2>/dev/null")
576
+ sleep(2)
577
+ # 检查进程是否还存在
578
+ if system("kill -0 #{pid} 2>/dev/null")
579
+ puts "优雅终止失败,使用强制终止..."
580
+ success = system("kill -KILL #{pid} 2>/dev/null")
581
+ end
582
+ end
583
+
584
+ if success
585
+ puts "✓ 进程 #{pid} 已终止"
586
+ sleep(1) # 等待端口释放
587
+
588
+ # 再次检查端口是否可用
589
+ unless port_occupied?(@server_port)
590
+ puts "✓ 端口 #{@server_port} 现在可用"
591
+ return true
592
+ else
593
+ puts "⚠️ 端口仍被占用,可能需要等待更长时间"
594
+ return false
595
+ end
596
+ else
597
+ puts "✗ 终止进程失败,可能需要管理员权限"
598
+ return false
599
+ end
600
+ rescue => e
601
+ puts "终止进程时出错: #{e.message}"
602
+ return false
603
+ end
604
+ end
605
+
606
+ # 构建成功页面
607
+ def build_success_page
608
+ <<~HTML
609
+ <!DOCTYPE html>
610
+ <html>
611
+ <head>
612
+ <meta charset="UTF-8">
613
+ <title>JPS 授权成功</title>
614
+ <style>
615
+ body { font-family: Arial, sans-serif; text-align: center; padding: 40px; background-color: #f5f5f5; }
616
+ .container { max-width: 600px; margin: 0 auto; background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
617
+ .success { color: #4CAF50; margin-bottom: 20px; }
618
+ .countdown { font-weight: bold; color: #FF5722; }
619
+ .close-btn { background: #4CAF50; color: white; border: none; padding: 10px 20px; cursor: pointer; border-radius: 4px; font-size: 16px; }
620
+ </style>
621
+ <script>
622
+ var secondsLeft = 3;
623
+ function updateCountdown() {
624
+ document.getElementById('countdown').innerText = secondsLeft;
625
+ if (secondsLeft <= 0) {
626
+ window.close();
627
+ } else {
628
+ secondsLeft -= 1;
629
+ setTimeout(updateCountdown, 1000);
630
+ }
631
+ }
632
+ window.onload = function() { updateCountdown(); }
633
+ </script>
634
+ </head>
635
+ <body>
636
+ <div class="container">
637
+ <h1 class="success">✓ JPS 授权成功!</h1>
638
+ <p>已获取授权码,正在返回命令行...</p>
639
+ <p>此窗口将在 <span id="countdown" class="countdown">3</span> 秒后自动关闭</p>
640
+ <button class="close-btn" onclick="window.close()">立即关闭</button>
641
+ </div>
642
+ </body>
643
+ </html>
644
+ HTML
645
+ end
646
+
647
+ # 构建错误页面
648
+ def build_error_page(error_msg)
649
+ <<~HTML
650
+ <!DOCTYPE html>
651
+ <html>
652
+ <head>
653
+ <meta charset="UTF-8">
654
+ <title>JPS 授权失败</title>
655
+ <style>
656
+ body { font-family: Arial, sans-serif; text-align: center; padding: 40px; background-color: #f5f5f5; }
657
+ .container { max-width: 600px; margin: 0 auto; background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
658
+ .error { color: #f44336; margin-bottom: 20px; }
659
+ .close-btn { background: #f44336; color: white; border: none; padding: 10px 20px; cursor: pointer; border-radius: 4px; font-size: 16px; }
660
+ </style>
661
+ </head>
662
+ <body>
663
+ <div class="container">
664
+ <h1 class="error">✗ JPS 授权失败</h1>
665
+ <p>错误信息: #{error_msg}</p>
666
+ <p>请返回命令行重试</p>
667
+ <button class="close-btn" onclick="window.close()">关闭窗口</button>
668
+ </div>
669
+ </body>
670
+ </html>
671
+ HTML
672
+ end
673
+
674
+ end
675
+
676
+ end