pindo 5.4.0 → 5.5.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.
- checksums.yaml +4 -4
- data/lib/pindo/base/pindocontext.rb +42 -31
- data/lib/pindo/client/aws3sclient.rb +2 -2
- data/lib/pindo/client/httpclient.rb +1 -2
- data/lib/pindo/command/android/autobuild.rb +40 -18
- data/lib/pindo/command/android/build.rb +37 -16
- data/lib/pindo/command/android/debug.rb +34 -20
- data/lib/pindo/command/appstore/iap.rb +15 -1
- data/lib/pindo/command/appstore/itcapp.rb +15 -1
- data/lib/pindo/command/appstore/metadata.rb +15 -1
- data/lib/pindo/command/appstore/screenshots.rb +15 -1
- data/lib/pindo/command/appstore/upload.rb +15 -1
- data/lib/pindo/command/deploy/build.rb +35 -20
- data/lib/pindo/command/deploy/bundleid.rb +15 -1
- data/lib/pindo/command/deploy/cert.rb +19 -2
- data/lib/pindo/command/deploy/check.rb +15 -1
- data/lib/pindo/command/deploy/configproj.rb +15 -1
- data/lib/pindo/command/deploy/confusecode.rb +15 -1
- data/lib/pindo/command/deploy/confuseproj.rb +11 -1
- data/lib/pindo/command/deploy/fabric.rb +10 -1
- data/lib/pindo/command/deploy/getitcinfo.rb +11 -1
- data/lib/pindo/command/deploy/iap.rb +12 -1
- data/lib/pindo/command/deploy/initconfig.rb +11 -1
- data/lib/pindo/command/deploy/itcinfo.rb +12 -1
- data/lib/pindo/command/deploy/pullconfig.rb +11 -1
- data/lib/pindo/command/deploy/pushconfig.rb +11 -1
- data/lib/pindo/command/deploy/quswark.rb +12 -1
- data/lib/pindo/command/deploy/quswauth.rb +10 -1
- data/lib/pindo/command/deploy/reportbug.rb +11 -1
- data/lib/pindo/command/deploy/resign.rb +12 -1
- data/lib/pindo/command/deploy/updateconfig.rb +11 -1
- data/lib/pindo/command/deploy/uploadipa.rb +11 -1
- data/lib/pindo/command/env/dreamstudio.rb +13 -1
- data/lib/pindo/command/env/quarkenv.rb +13 -1
- data/lib/pindo/command/env/swarkenv.rb +13 -1
- data/lib/pindo/command/env/workhard.rb +13 -1
- data/lib/pindo/command/gplay/iap.rb +21 -5
- data/lib/pindo/command/gplay/itcapp.rb +19 -5
- data/lib/pindo/command/gplay/metadata.rb +23 -5
- data/lib/pindo/command/gplay/screenshots.rb +23 -5
- data/lib/pindo/command/gplay/upload.rb +21 -5
- data/lib/pindo/command/gplay.rb +8 -8
- data/lib/pindo/command/ios/adhoc.rb +18 -3
- data/lib/pindo/command/ios/autobuild.rb +22 -7
- data/lib/pindo/command/ios/autoresign.rb +18 -3
- data/lib/pindo/command/ios/build.rb +18 -4
- data/lib/pindo/command/ipa/autoresign.rb +35 -4
- data/lib/pindo/command/ipa/import.rb +19 -1
- data/lib/pindo/command/ipa/output.rb +39 -4
- data/lib/pindo/command/{pgyer → jps}/apptest.rb +35 -24
- data/lib/pindo/command/jps/bind.rb +191 -0
- data/lib/pindo/command/{pgyer → jps}/comment.rb +19 -19
- data/lib/pindo/command/{pgyer → jps}/download.rb +20 -20
- data/lib/pindo/command/{pgyer → jps}/login.rb +9 -9
- data/lib/pindo/command/{pgyer → jps}/resign.rb +40 -25
- data/lib/pindo/command/{pgyer → jps}/upload.rb +60 -43
- data/lib/pindo/command/jps.rb +18 -0
- data/lib/pindo/command/lib/forcepush.rb +15 -1
- data/lib/pindo/command/lib/push.rb +15 -1
- data/lib/pindo/command/lib/update.rb +15 -1
- data/lib/pindo/command/repo/clone.rb +11 -1
- data/lib/pindo/command/repo/create.rb +13 -1
- data/lib/pindo/command/repo/login.rb +14 -3
- data/lib/pindo/command/repo/search.rb +14 -2
- data/lib/pindo/command/unity/apk.rb +14 -28
- data/lib/pindo/command/unity/autobuild.rb +13 -13
- data/lib/pindo/command/unity/ipa.rb +15 -29
- data/lib/pindo/command/unity/web.rb +15 -15
- data/lib/pindo/command/utils/boss.rb +19 -2
- data/lib/pindo/command/utils/clearcert.rb +18 -2
- data/lib/pindo/command/utils/device.rb +19 -2
- data/lib/pindo/command/utils/icon.rb +20 -2
- data/lib/pindo/command/utils/renewcert.rb +28 -3
- data/lib/pindo/command/utils/renewproj.rb +25 -2
- data/lib/pindo/command/utils/tgate.rb +28 -3
- data/lib/pindo/command/utils/xcassets.rb +20 -2
- data/lib/pindo/command/web/autobuild.rb +18 -3
- data/lib/pindo/command.rb +8 -2
- data/lib/pindo/module/pgyer/pgyerhelper.rb +185 -85
- data/lib/pindo/version.rb +1 -1
- metadata +30 -12
- data/lib/pindo/client/pgyer_feishu_oauth_cli.rb +0 -669
- data/lib/pindo/client/pgyerclient.rb +0 -466
- data/lib/pindo/client/pgyeruploadclient.rb +0 -517
- data/lib/pindo/command/pgyer.rb +0 -18
@@ -1,669 +0,0 @@
|
|
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 'pindo/base/aeshelper'
|
12
|
-
require 'pindo/config/pindouserlocalconfig'
|
13
|
-
require 'pindo/client/httpclient'
|
14
|
-
|
15
|
-
module Pindo
|
16
|
-
|
17
|
-
class PgyerFeishuOAuthCLI
|
18
|
-
attr_reader :access_token, :username, :expires_at
|
19
|
-
|
20
|
-
def initialize(client_id)
|
21
|
-
@client_id = client_id
|
22
|
-
@feishu_auth_url = 'https://passport.feishu.cn/suite/passport/oauth/authorize?'
|
23
|
-
|
24
|
-
# 保持原有的redirect_uri,飞书OAuth流程中仍使用这个URL
|
25
|
-
# 前端UI会检测state=terminal_login并自动跳转到localhost:8899
|
26
|
-
@redirect_uri = 'https://pgyerapps.com/login'
|
27
|
-
@pgyer_api_endpoint = 'https://api.pgyerapps.com/api/user/lark_qr_login'
|
28
|
-
|
29
|
-
@state = 'terminal_login'
|
30
|
-
@server_port = 8899
|
31
|
-
|
32
|
-
@larkScopeList = [
|
33
|
-
'task:task:write',
|
34
|
-
'task:section:write',
|
35
|
-
'task:custom_field:write',
|
36
|
-
'task:tasklist:write'
|
37
|
-
];
|
38
|
-
|
39
|
-
@access_token = nil
|
40
|
-
@username = nil
|
41
|
-
@expires_at = nil
|
42
|
-
end
|
43
|
-
|
44
|
-
# 启动授权流程
|
45
|
-
def authorize
|
46
|
-
# 不再检查已存储的令牌,直接开始授权流程
|
47
|
-
authorization_uri = build_authorization_uri
|
48
|
-
puts "🚀 正在打开浏览器进行飞书 OAuth 授权..."
|
49
|
-
puts "\n📋 授权链接(如浏览器未自动打开,请手动复制访问):"
|
50
|
-
puts "-" * 60
|
51
|
-
puts authorization_uri
|
52
|
-
puts "-" * 60
|
53
|
-
|
54
|
-
# 在浏览器中打开授权URL
|
55
|
-
open_browser(authorization_uri)
|
56
|
-
|
57
|
-
# 启动本地服务器处理回调
|
58
|
-
code = start_callback_server
|
59
|
-
|
60
|
-
# 如果自动获取失败,提示用户手动输入
|
61
|
-
if code.nil?
|
62
|
-
# 停止任何可能的 spinner 动画
|
63
|
-
if defined?(Funlog) && Funlog.instance.instance_variable_get(:@spinner_log)
|
64
|
-
Funlog.instance.instance_variable_set(:@spinner_log, nil)
|
65
|
-
end
|
66
|
-
|
67
|
-
loop do
|
68
|
-
puts "\n⚠️ 自动获取授权码失败,请选择操作:"
|
69
|
-
puts " 1. 输入授权码(复制 code= 后的内容)"
|
70
|
-
puts " 2. 输入完整回调 URL"
|
71
|
-
puts " 3. 重新打开授权网页"
|
72
|
-
puts " 4. 退出登录"
|
73
|
-
print "请选择 (1-4): "
|
74
|
-
STDOUT.flush # 确保提示符立即显示
|
75
|
-
|
76
|
-
begin
|
77
|
-
choice = STDIN.gets&.chomp
|
78
|
-
|
79
|
-
# 处理 Ctrl+C 中断
|
80
|
-
if choice.nil?
|
81
|
-
puts "\n\n🚪 用户中断操作"
|
82
|
-
return :user_cancelled
|
83
|
-
end
|
84
|
-
|
85
|
-
case choice
|
86
|
-
when "1"
|
87
|
-
puts "\n请输入授权码:"
|
88
|
-
print "🔑 "
|
89
|
-
STDOUT.flush # 确保提示符立即显示
|
90
|
-
code_input = STDIN.gets&.chomp
|
91
|
-
if code_input.nil?
|
92
|
-
puts "\n🚪 用户中断操作"
|
93
|
-
return :user_cancelled
|
94
|
-
elsif !code_input.empty?
|
95
|
-
code = code_input
|
96
|
-
break
|
97
|
-
else
|
98
|
-
puts "⚠️ 授权码不能为空,请重新选择"
|
99
|
-
end
|
100
|
-
when "2"
|
101
|
-
puts "\n请输入完整回调 URL:"
|
102
|
-
print "🔗 "
|
103
|
-
STDOUT.flush # 确保提示符立即显示
|
104
|
-
url_input = STDIN.gets&.chomp
|
105
|
-
if url_input.nil?
|
106
|
-
puts "\n🚪 用户中断操作"
|
107
|
-
return :user_cancelled
|
108
|
-
elsif url_input.start_with?("http")
|
109
|
-
# 尝试从 URL 中提取 code
|
110
|
-
begin
|
111
|
-
uri = URI(url_input)
|
112
|
-
query_params = URI.decode_www_form(uri.query || '').to_h
|
113
|
-
code = query_params['code']
|
114
|
-
if code
|
115
|
-
puts "✅ 成功从 URL 提取授权码"
|
116
|
-
break
|
117
|
-
else
|
118
|
-
puts "❌ URL 中未找到授权码,请重新选择"
|
119
|
-
end
|
120
|
-
rescue => e
|
121
|
-
puts "❌ 解析 URL 失败: #{e.message}"
|
122
|
-
end
|
123
|
-
else
|
124
|
-
puts "❌ 无效的 URL 格式,请重新选择"
|
125
|
-
end
|
126
|
-
when "3"
|
127
|
-
# 重新打开授权网页
|
128
|
-
puts "\n🔄 重新打开授权网页..."
|
129
|
-
open_browser(authorization_uri)
|
130
|
-
# 重新启动服务器尝试获取授权码
|
131
|
-
code = start_callback_server
|
132
|
-
if code
|
133
|
-
break
|
134
|
-
end
|
135
|
-
# 如果还是失败,继续循环让用户选择
|
136
|
-
when "4"
|
137
|
-
puts "\n🚪 已取消登录"
|
138
|
-
return :user_cancelled
|
139
|
-
else
|
140
|
-
puts "❌ 无效的选择,请输入 1-4"
|
141
|
-
end
|
142
|
-
rescue Interrupt
|
143
|
-
puts "\n\n🚪 用户中断操作"
|
144
|
-
return :user_cancelled
|
145
|
-
end
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
if code
|
150
|
-
puts "\n✅ 成功获取授权码"
|
151
|
-
print "🔄 正在验证飞书身份..."
|
152
|
-
STDOUT.flush
|
153
|
-
if login_pgyer_with_feishu(code:code)
|
154
|
-
puts " ✅"
|
155
|
-
puts "🎉 登录成功!欢迎使用 Pgyer"
|
156
|
-
# 登录成功后,返回true,外部程序可以通过访问access_token属性获取token
|
157
|
-
return true
|
158
|
-
end
|
159
|
-
return false
|
160
|
-
else
|
161
|
-
puts "\n❌ 授权失败"
|
162
|
-
return false
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
# 验证Pgyer令牌
|
167
|
-
def validate_pgyer_token(token = nil, expires_at = nil)
|
168
|
-
token_to_check = token || @access_token
|
169
|
-
expiration_time = expires_at || @expires_at
|
170
|
-
|
171
|
-
# 首先检查token是否存在
|
172
|
-
return false unless token_to_check
|
173
|
-
|
174
|
-
# 然后检查token是否过期
|
175
|
-
if expiration_time && Time.now.to_i > expiration_time
|
176
|
-
puts "令牌已过期,需要重新登录"
|
177
|
-
return false
|
178
|
-
end
|
179
|
-
|
180
|
-
# 最后验证token是否有效
|
181
|
-
uri = URI("https://www.pgyer.com/api/user/profile")
|
182
|
-
request = Net::HTTP::Get.new(uri)
|
183
|
-
request['Authorization'] = "Bearer #{token_to_check}"
|
184
|
-
|
185
|
-
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
186
|
-
http.request(request)
|
187
|
-
end
|
188
|
-
|
189
|
-
return response.code == '200'
|
190
|
-
end
|
191
|
-
|
192
|
-
# 使用code登录Pgyer, 并且Pgyer返回token
|
193
|
-
def login_pgyer_with_feishu(code: nil)
|
194
|
-
return false unless code
|
195
|
-
|
196
|
-
# 构造请求体
|
197
|
-
body_params = {
|
198
|
-
code: code,
|
199
|
-
redirectUri: @redirect_uri,
|
200
|
-
scope: @larkScopeList.join(' ')
|
201
|
-
}
|
202
|
-
|
203
|
-
# 使用HttpClient发送请求
|
204
|
-
con = HttpClient.create_instance_with_proxy
|
205
|
-
|
206
|
-
begin
|
207
|
-
res = con.post do |req|
|
208
|
-
req.url @pgyer_api_endpoint
|
209
|
-
req.headers['Content-Type'] = 'application/json'
|
210
|
-
req.body = body_params.to_json
|
211
|
-
end
|
212
|
-
|
213
|
-
# 处理响应
|
214
|
-
result = nil
|
215
|
-
if !res.body.nil?
|
216
|
-
begin
|
217
|
-
result = JSON.parse(res.body)
|
218
|
-
# puts "解析后的响应: #{result.inspect}"
|
219
|
-
|
220
|
-
if result['code'] == 200 && !result['data'].nil? && !result['data']['token'].nil?
|
221
|
-
@access_token = result['data']['token']
|
222
|
-
@username = result['data']['username'] if result['data']['username']
|
223
|
-
# 设置token有效期为7天后
|
224
|
-
@expires_at = Time.now.to_i + 6 * 24 * 60 * 60 # 7天的秒数
|
225
|
-
return true
|
226
|
-
else
|
227
|
-
error_msg = result['meta'] && result['meta']['message'] ? result['meta']['message'] : '未知错误'
|
228
|
-
puts " ❌"
|
229
|
-
puts "登录失败: #{error_msg}"
|
230
|
-
return false
|
231
|
-
end
|
232
|
-
rescue => e
|
233
|
-
puts " ❌"
|
234
|
-
puts "响应解析失败: #{e.message}"
|
235
|
-
return false
|
236
|
-
end
|
237
|
-
else
|
238
|
-
puts " ❌"
|
239
|
-
puts "服务器返回空响应"
|
240
|
-
return false
|
241
|
-
end
|
242
|
-
rescue => e
|
243
|
-
puts " ❌"
|
244
|
-
puts "网络请求失败: #{e.message}"
|
245
|
-
return false
|
246
|
-
end
|
247
|
-
end
|
248
|
-
|
249
|
-
private
|
250
|
-
|
251
|
-
# 获取跨平台的 null 设备路径
|
252
|
-
def get_null_device
|
253
|
-
case RbConfig::CONFIG['host_os']
|
254
|
-
when /mswin|mingw|cygwin/
|
255
|
-
'NUL'
|
256
|
-
else
|
257
|
-
'/dev/null'
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
|
-
|
262
|
-
# 构建授权URI
|
263
|
-
def build_authorization_uri
|
264
|
-
uri = URI(@feishu_auth_url)
|
265
|
-
|
266
|
-
params = {
|
267
|
-
'client_id' => @client_id,
|
268
|
-
'redirect_uri' => @redirect_uri,
|
269
|
-
'response_type' => 'code',
|
270
|
-
'state' => @state,
|
271
|
-
'scope' => @larkScopeList.join(' ')
|
272
|
-
}
|
273
|
-
|
274
|
-
uri.query = URI.encode_www_form(params)
|
275
|
-
uri.to_s
|
276
|
-
end
|
277
|
-
|
278
|
-
# 在浏览器中打开URL
|
279
|
-
def open_browser(url)
|
280
|
-
case RbConfig::CONFIG['host_os']
|
281
|
-
when /mswin|mingw|cygwin/
|
282
|
-
# Windows: 使用双引号包围URL避免参数解析问题
|
283
|
-
system("start \"\" \"#{url}\"")
|
284
|
-
when /darwin/
|
285
|
-
system("open", url)
|
286
|
-
when /linux|bsd/
|
287
|
-
system("xdg-open", url)
|
288
|
-
else
|
289
|
-
puts "无法自动打开浏览器,请手动访问: #{url}"
|
290
|
-
end
|
291
|
-
end
|
292
|
-
|
293
|
-
# 启动本地服务器处理回调
|
294
|
-
def start_callback_server
|
295
|
-
code = nil
|
296
|
-
|
297
|
-
# 检查并处理端口占用
|
298
|
-
unless ensure_port_available(@server_port)
|
299
|
-
puts "✗ 无法使用端口 #{@server_port},登录失败"
|
300
|
-
return nil
|
301
|
-
end
|
302
|
-
|
303
|
-
puts "\n🌐 本地服务器已启动(端口: #{@server_port})"
|
304
|
-
puts "💡 提示: 按 Ctrl+C 可中断并选择其他操作"
|
305
|
-
puts "🔄 等待飞书授权回调..."
|
306
|
-
|
307
|
-
begin
|
308
|
-
# 使用本地8899端口处理回调,不管redirect_uri配置如何
|
309
|
-
server = WEBrick::HTTPServer.new(
|
310
|
-
Port: @server_port,
|
311
|
-
BindAddress: '127.0.0.1',
|
312
|
-
Logger: WEBrick::Log.new(get_null_device),
|
313
|
-
AccessLog: []
|
314
|
-
)
|
315
|
-
rescue Errno::EADDRINUSE
|
316
|
-
puts "✗ 端口 #{@server_port} 仍被占用,无法启动服务器"
|
317
|
-
return nil
|
318
|
-
rescue Errno::ENOENT
|
319
|
-
puts "✗ 启动服务器失败: 系统找不到指定的路径或文件"
|
320
|
-
return nil
|
321
|
-
rescue => e
|
322
|
-
puts "✗ 启动服务器失败: #{e.message}"
|
323
|
-
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
|
324
|
-
puts " 提示: 如果是Windows系统,请确保防火墙允许Ruby访问网络"
|
325
|
-
end
|
326
|
-
return nil
|
327
|
-
end
|
328
|
-
|
329
|
-
# 处理根路径的请求
|
330
|
-
server.mount_proc '/' do |req, res|
|
331
|
-
begin
|
332
|
-
# puts "接收到请求: #{req.request_line}"
|
333
|
-
# puts "请求参数: #{req.query_string}"
|
334
|
-
|
335
|
-
# 安全解析请求参数
|
336
|
-
begin
|
337
|
-
query_params = URI.decode_www_form(req.query_string || '').to_h
|
338
|
-
# 只在有code时输出日志,避免多余信息
|
339
|
-
# puts "解析的参数: #{query_params.inspect}" if query_params['code']
|
340
|
-
rescue => e
|
341
|
-
# puts "解析请求参数失败: #{e.message}"
|
342
|
-
query_params = {}
|
343
|
-
end
|
344
|
-
|
345
|
-
# if query_params['state'] != @state
|
346
|
-
# puts "状态不匹配: 接收到 #{query_params['state']},期望 #{@state}"
|
347
|
-
# res.content_type = "text/html; charset=UTF-8"
|
348
|
-
# res.body = "状态不匹配,可能存在CSRF攻击风险"
|
349
|
-
# elsif query_params['error']
|
350
|
-
if query_params['error']
|
351
|
-
# puts "授权错误: #{query_params['error']}"
|
352
|
-
res.content_type = "text/html; charset=UTF-8"
|
353
|
-
res.body = "授权错误: #{query_params['error']}"
|
354
|
-
elsif query_params['code']
|
355
|
-
code = query_params['code']
|
356
|
-
# 成功获取时不输出,避免重复
|
357
|
-
# puts "成功获取授权码: #{code}"
|
358
|
-
res.content_type = "text/html; charset=UTF-8"
|
359
|
-
res.body = <<-HTML
|
360
|
-
<!DOCTYPE html>
|
361
|
-
<html>
|
362
|
-
<head>
|
363
|
-
<meta charset="UTF-8">
|
364
|
-
<title>飞书授权成功</title>
|
365
|
-
<style>
|
366
|
-
body { font-family: Arial, sans-serif; text-align: center; padding: 40px; }
|
367
|
-
.container { max-width: 600px; margin: 0 auto; }
|
368
|
-
.success { color: #4CAF50; }
|
369
|
-
.code { font-family: monospace; background: #f5f5f5; padding: 10px; border-radius: 4px; word-break: break-all; }
|
370
|
-
.countdown { font-weight: bold; color: #FF5722; }
|
371
|
-
.autoclose-banner {
|
372
|
-
background-color: #333; color: white; padding: 10px;
|
373
|
-
position: fixed; top: 0; left: 0; right: 0;
|
374
|
-
display: flex; justify-content: space-between; align-items: center;
|
375
|
-
}
|
376
|
-
.close-btn {
|
377
|
-
background: #f44336; color: white; border: none;
|
378
|
-
padding: 5px 10px; cursor: pointer; border-radius: 3px;
|
379
|
-
}
|
380
|
-
</style>
|
381
|
-
<script>
|
382
|
-
// 尝试多种方法关闭窗口
|
383
|
-
function attemptClose() {
|
384
|
-
try {
|
385
|
-
// 方法1: 最基本的关闭尝试
|
386
|
-
window.close();
|
387
|
-
|
388
|
-
// 方法4: 尝试使用opener关系
|
389
|
-
if (window.opener) {
|
390
|
-
window.opener.focus();
|
391
|
-
window.close();
|
392
|
-
}
|
393
|
-
|
394
|
-
// 如果以上方法都失败了,显示手动关闭提示
|
395
|
-
setTimeout(function() {
|
396
|
-
document.getElementById('close-message').style.display = 'block';
|
397
|
-
document.getElementById('countdown-container').style.display = 'none';
|
398
|
-
}, 1000);
|
399
|
-
} catch (e) {
|
400
|
-
console.error("关闭窗口失败:", e);
|
401
|
-
document.getElementById('close-message').style.display = 'block';
|
402
|
-
document.getElementById('countdown-container').style.display = 'none';
|
403
|
-
}
|
404
|
-
}
|
405
|
-
|
406
|
-
// 倒计时函数
|
407
|
-
var secondsLeft = 3;
|
408
|
-
function updateCountdown() {
|
409
|
-
document.getElementById('countdown').innerText = secondsLeft;
|
410
|
-
if (secondsLeft <= 0) {
|
411
|
-
attemptClose();
|
412
|
-
} else {
|
413
|
-
secondsLeft -= 1;
|
414
|
-
setTimeout(updateCountdown, 1000);
|
415
|
-
}
|
416
|
-
}
|
417
|
-
|
418
|
-
// 初始化
|
419
|
-
window.onload = function() {
|
420
|
-
updateCountdown();
|
421
|
-
}
|
422
|
-
</script>
|
423
|
-
</head>
|
424
|
-
<body>
|
425
|
-
<div class="autoclose-banner">
|
426
|
-
<span>授权成功! 此窗口将自动关闭 (<span id="countdown" class="countdown">3</span>)</span>
|
427
|
-
<button class="close-btn" onclick="attemptClose()">立即关闭</button>
|
428
|
-
</div>
|
429
|
-
|
430
|
-
<div class="container">
|
431
|
-
<h1 class="success">飞书授权成功!</h1>
|
432
|
-
<p>已获取授权码,正在返回命令行...</p>
|
433
|
-
|
434
|
-
<div id="countdown-container">
|
435
|
-
<p>此窗口将在 <span id="countdown-text" class="countdown">3</span> 秒后自动关闭</p>
|
436
|
-
</div>
|
437
|
-
|
438
|
-
<div id="close-message" style="display: none;">
|
439
|
-
<p>自动关闭失败,请手动关闭此窗口</p>
|
440
|
-
<button class="close-btn" onclick="attemptClose()">尝试再次关闭</button>
|
441
|
-
</div>
|
442
|
-
|
443
|
-
<p>授权码:</p>
|
444
|
-
<div class="code">#{code}</div>
|
445
|
-
</div>
|
446
|
-
|
447
|
-
<script>
|
448
|
-
// 同步倒计时显示
|
449
|
-
document.getElementById('countdown-text').innerText = secondsLeft;
|
450
|
-
</script>
|
451
|
-
</body>
|
452
|
-
</html>
|
453
|
-
HTML
|
454
|
-
else
|
455
|
-
# 忽略没有参数的请求(如favicon.ico等)
|
456
|
-
# puts "未获取到授权码"
|
457
|
-
res.content_type = "text/html; charset=UTF-8"
|
458
|
-
res.body = "等待授权中..."
|
459
|
-
end
|
460
|
-
|
461
|
-
# 只在获取到code时关闭服务器
|
462
|
-
if code
|
463
|
-
Thread.new do
|
464
|
-
sleep 4 # 给用户时间看到成功页面
|
465
|
-
server.shutdown
|
466
|
-
end
|
467
|
-
end
|
468
|
-
rescue => e
|
469
|
-
# 静默处理错误,避免干扰用户
|
470
|
-
# puts "处理请求时出错: #{e.class} - #{e.message}"
|
471
|
-
|
472
|
-
# 确保即使出错也返回有效的响应
|
473
|
-
res.content_type = "text/html; charset=UTF-8"
|
474
|
-
res.body = "处理请求时出错"
|
475
|
-
end
|
476
|
-
end
|
477
|
-
|
478
|
-
# 捕获Ctrl+C以允许用户中断
|
479
|
-
trap('INT') { server.shutdown }
|
480
|
-
|
481
|
-
# puts "正在监听http://localhost:8899等待飞书重定向..."
|
482
|
-
# puts "您也可以手动访问: http://localhost:8899/?code=YOUR_CODE&state=terminal_login"
|
483
|
-
|
484
|
-
# 在线程中运行服务器,最多等待3分钟
|
485
|
-
thread = Thread.new { server.start }
|
486
|
-
begin
|
487
|
-
thread.join(180) # 最多等待3分钟
|
488
|
-
rescue Timeout::Error
|
489
|
-
puts "授权超时"
|
490
|
-
server.shutdown
|
491
|
-
end
|
492
|
-
|
493
|
-
# 返回获取到的code(如果有的话)
|
494
|
-
code
|
495
|
-
end
|
496
|
-
|
497
|
-
# 确保端口可用,处理端口占用问题
|
498
|
-
def ensure_port_available(port)
|
499
|
-
return true unless port_occupied?(port)
|
500
|
-
|
501
|
-
puts "⚠️ 端口 #{port} 被占用,正在检查占用进程..."
|
502
|
-
|
503
|
-
# 获取占用端口的进程信息
|
504
|
-
process_info = get_port_process_info(port)
|
505
|
-
|
506
|
-
if process_info.nil?
|
507
|
-
puts "✗ 无法获取端口占用信息"
|
508
|
-
return false
|
509
|
-
end
|
510
|
-
|
511
|
-
puts "占用进程信息:"
|
512
|
-
puts " PID: #{process_info[:pid]}"
|
513
|
-
puts " 进程名: #{process_info[:name]}"
|
514
|
-
puts " 命令: #{process_info[:command]}"
|
515
|
-
|
516
|
-
# 检查是否是自己的进程(可能是之前未正常关闭的实例)
|
517
|
-
if process_info[:name].include?('ruby') && (process_info[:command].include?('pindo') || process_info[:command].include?('pgyer'))
|
518
|
-
puts "检测到可能是 Pindo/Pgyer 之前的遗留进程"
|
519
|
-
if ask_user_kill_process?
|
520
|
-
return kill_process_by_pid(process_info[:pid])
|
521
|
-
end
|
522
|
-
else
|
523
|
-
puts "端口被其他应用程序占用"
|
524
|
-
if ask_user_kill_process?
|
525
|
-
return kill_process_by_pid(process_info[:pid])
|
526
|
-
end
|
527
|
-
end
|
528
|
-
|
529
|
-
return false
|
530
|
-
end
|
531
|
-
|
532
|
-
# 检查端口是否被占用
|
533
|
-
def port_occupied?(port)
|
534
|
-
require 'socket'
|
535
|
-
|
536
|
-
begin
|
537
|
-
server = TCPServer.new('127.0.0.1', port)
|
538
|
-
server.close
|
539
|
-
false # 端口未被占用
|
540
|
-
rescue Errno::EADDRINUSE
|
541
|
-
true # 端口被占用
|
542
|
-
rescue => e
|
543
|
-
puts "⚠️ 检查端口时出错: #{e.message}"
|
544
|
-
true # 出错时假设端口被占用,避免冲突
|
545
|
-
end
|
546
|
-
end
|
547
|
-
|
548
|
-
# 获取占用指定端口的进程信息
|
549
|
-
def get_port_process_info(port)
|
550
|
-
begin
|
551
|
-
# macOS/Linux 使用 lsof 命令
|
552
|
-
if system('which lsof > /dev/null 2>&1')
|
553
|
-
output = `lsof -ti :#{port} 2>/dev/null`.strip
|
554
|
-
return nil if output.empty?
|
555
|
-
|
556
|
-
pid = output.split("\n").first
|
557
|
-
return nil if pid.nil? || pid.empty?
|
558
|
-
|
559
|
-
# 获取进程详细信息
|
560
|
-
ps_output = `ps -p #{pid} -o pid,comm,args 2>/dev/null`.lines
|
561
|
-
return nil if ps_output.length < 2
|
562
|
-
|
563
|
-
process_line = ps_output[1].strip
|
564
|
-
parts = process_line.split(/\s+/, 3)
|
565
|
-
|
566
|
-
return {
|
567
|
-
pid: parts[0],
|
568
|
-
name: parts[1] || 'unknown',
|
569
|
-
command: parts[2] || 'unknown'
|
570
|
-
}
|
571
|
-
end
|
572
|
-
|
573
|
-
# Windows 使用 netstat 命令
|
574
|
-
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
|
575
|
-
output = `netstat -ano | findstr :#{port}`.strip
|
576
|
-
return nil if output.empty?
|
577
|
-
|
578
|
-
lines = output.split("\n")
|
579
|
-
listening_line = lines.find { |line| line.include?('LISTENING') }
|
580
|
-
return nil unless listening_line
|
581
|
-
|
582
|
-
pid = listening_line.split.last
|
583
|
-
return nil if pid.nil? || pid.empty?
|
584
|
-
|
585
|
-
# 获取进程名称
|
586
|
-
task_output = `tasklist /FI "PID eq #{pid}" /FO CSV /NH 2>nul`.strip
|
587
|
-
if !task_output.empty?
|
588
|
-
process_name = task_output.split(',').first.gsub('"', '')
|
589
|
-
return {
|
590
|
-
pid: pid,
|
591
|
-
name: process_name,
|
592
|
-
command: process_name
|
593
|
-
}
|
594
|
-
end
|
595
|
-
end
|
596
|
-
|
597
|
-
return nil
|
598
|
-
rescue => e
|
599
|
-
puts "获取进程信息时出错: #{e.message}"
|
600
|
-
return nil
|
601
|
-
end
|
602
|
-
end
|
603
|
-
|
604
|
-
# 询问用户是否终止占用端口的进程
|
605
|
-
def ask_user_kill_process?
|
606
|
-
puts "\n是否终止占用端口的进程?"
|
607
|
-
puts " y/yes - 终止进程并继续 (可能影响其他应用)"
|
608
|
-
puts " n/no - 取消登录 (默认)"
|
609
|
-
print "> "
|
610
|
-
|
611
|
-
begin
|
612
|
-
response = STDIN.gets&.chomp&.downcase
|
613
|
-
return ['y', 'yes'].include?(response)
|
614
|
-
rescue Interrupt
|
615
|
-
puts "\n\n用户中断操作"
|
616
|
-
return false
|
617
|
-
rescue => e
|
618
|
-
puts "无法读取用户输入: #{e.message}"
|
619
|
-
return false
|
620
|
-
end
|
621
|
-
end
|
622
|
-
|
623
|
-
# 根据 PID 终止进程
|
624
|
-
def kill_process_by_pid(pid)
|
625
|
-
return false if pid.nil? || pid.empty?
|
626
|
-
|
627
|
-
begin
|
628
|
-
puts "正在终止进程 #{pid}..."
|
629
|
-
|
630
|
-
# 跨平台的进程终止命令
|
631
|
-
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
|
632
|
-
# Windows
|
633
|
-
success = system("taskkill /PID #{pid} /F >nul 2>&1")
|
634
|
-
else
|
635
|
-
# macOS/Linux - 先尝试优雅终止,再强制终止
|
636
|
-
success = system("kill -TERM #{pid} 2>/dev/null")
|
637
|
-
sleep(2)
|
638
|
-
# 检查进程是否还存在
|
639
|
-
if system("kill -0 #{pid} 2>/dev/null")
|
640
|
-
puts "优雅终止失败,使用强制终止..."
|
641
|
-
success = system("kill -KILL #{pid} 2>/dev/null")
|
642
|
-
end
|
643
|
-
end
|
644
|
-
|
645
|
-
if success
|
646
|
-
puts "✓ 进程 #{pid} 已终止"
|
647
|
-
sleep(1) # 等待端口释放
|
648
|
-
|
649
|
-
# 再次检查端口是否可用
|
650
|
-
unless port_occupied?(@server_port)
|
651
|
-
puts "✓ 端口 #{@server_port} 现在可用"
|
652
|
-
return true
|
653
|
-
else
|
654
|
-
puts "⚠️ 端口仍被占用,可能需要等待更长时间"
|
655
|
-
return false
|
656
|
-
end
|
657
|
-
else
|
658
|
-
puts "✗ 终止进程失败,可能需要管理员权限"
|
659
|
-
return false
|
660
|
-
end
|
661
|
-
rescue => e
|
662
|
-
puts "终止进程时出错: #{e.message}"
|
663
|
-
return false
|
664
|
-
end
|
665
|
-
end
|
666
|
-
|
667
|
-
end
|
668
|
-
|
669
|
-
end
|