jpsclient 1.4.0 → 1.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5dc83223b65fce83cf83423489a63175292008cadba1fa25d20a428d5401cef0
4
- data.tar.gz: 78c8a24b9d535fcd0bc64ab6add14c466c99725212ae0f5267a67248060d1ecb
3
+ metadata.gz: fc2afd23d09740af2160dbbfc2c6c1f0cffb937b5da3ab905c29582d0738eeb1
4
+ data.tar.gz: 6dacc82e06e921343226aca9a2277ea7a5a6f2c43f949fc46f4cbdc5136ba1ad
5
5
  SHA512:
6
- metadata.gz: a85d81582f25660667707be4c4370f0db5555ffd8ef805da89e23c54c4be94c0926b1e4f38b61bb98cb76807356e6f2b01898f855734e338e868a3a3c2717a1b
7
- data.tar.gz: 25b41812658ae7a22d49359a849509129b233e951d4aacb0bc3c73a78cd3ba086fe3bd494c9c72a85f972a3109f8084414138ce16ac5963d64912069b73cabbf
6
+ metadata.gz: b9b0002b6139c3b5d83dc4afcb9d4c185e4009205845db11fecbc06429cf4b588d7557bf71f1a648465f20532e2915f3d3a3939b4fec6209b6b57f862f4715b6
7
+ data.tar.gz: 0ab8ae8afd9e9db0a5a508a8ce2c4a6f2ba5552906506f05168ac6d053140ffda61f62c15d7de8599438d9b62c03d63018a8aa98940ede6e7921201ffc0aadb0
@@ -4,31 +4,67 @@ module JPSClient
4
4
  # 处理 /api/commit_log/* 路径的所有接口
5
5
  module CommitLog
6
6
 
7
+ # 更新提交记录
8
+ #
9
+ # @param id [Integer] 提交记录ID(必需)
10
+ # @param params [Hash] 更新参数
11
+ # @option params [String] :description 描述
12
+ # @option params [Array<String>] :fileUrls 文件URL数组
13
+ # @return [Hash] API响应
14
+ def update_commit_log(id:, params: {})
15
+ config = @request_config && @request_config["commit_log_update"]
16
+ raise JPSClient::ExceptionError, "Missing config for commit_log_update" unless config && config["url"]
17
+ path = config["url"]
18
+
19
+ body_params = {
20
+ id: id
21
+ }
22
+
23
+ # 添加可选参数
24
+ body_params[:description] = params[:description] if params.key?(:description)
25
+ body_params[:fileUrls] = params[:fileUrls] if params[:fileUrls]
26
+
27
+ response = @http_client.post(path, body: body_params)
28
+ result = JPSClient::Response.new(response)
29
+
30
+ if result.need_login?
31
+ do_login(force_login: true)
32
+ response = @http_client.post(path, body: body_params)
33
+ result = JPSClient::Response.new(response)
34
+ end
35
+
36
+ return result.to_h
37
+ end
38
+
7
39
  # 发送提交记录消息通知
8
40
  #
9
41
  # @param projectId [String] 项目ID(必需)
10
- # @param commitIds [Array<String>] 提交ID数组(必需)
42
+ # @param workflowId [Integer] 工作流ID(必需)
11
43
  # @param params [Hash] 其他参数
12
44
  # @option params [Boolean] :single 是否单个提交(默认 true)
13
- # @option params [String] :branch 分支名称,多个分支用逗号分隔
45
+ # @option params [Array<String>] :branches 分支名称数组
46
+ # @option params [Array<String>] :commitIds 提交ID数组
47
+ # @option params [Integer] :startTimestamp 开始时间戳
48
+ # @option params [Integer] :endTimestamp 结束时间戳
14
49
  # @option params [Integer] :indexNo 索引号
15
- # @option params [Integer] :workflowId 工作流ID
16
50
  # @return [Hash] API响应
17
- def send_commit_log_message(projectId:, commitIds:, params: {})
51
+ def send_commit_log_message(projectId:, workflowId:, params: {})
18
52
  config = @request_config && @request_config["commit_log_send_message"]
19
53
  raise JPSClient::ExceptionError, "Missing config for commit_log_send_message" unless config && config["url"]
20
54
  path = config["url"]
21
55
 
22
56
  body_params = {
23
- projectId: projectId,
24
- commitIds: commitIds,
25
- single: params[:single] || true
57
+ projectId: projectId,
58
+ workflowId: workflowId,
59
+ single: params.fetch(:single, true)
26
60
  }
27
61
 
28
62
  # 添加可选参数
29
- body_params[:branch] = params[:branch] if params[:branch]
63
+ body_params[:branches] = params[:branches] if params[:branches]
64
+ body_params[:commitIds] = params[:commitIds] if params[:commitIds]
65
+ body_params[:startTimestamp] = params[:startTimestamp] if params[:startTimestamp]
66
+ body_params[:endTimestamp] = params[:endTimestamp] if params[:endTimestamp]
30
67
  body_params[:indexNo] = params[:indexNo] if params[:indexNo]
31
- body_params[:workflowId] = params[:workflowId] if params[:workflowId]
32
68
 
33
69
  response = @http_client.post(path, body: body_params)
34
70
  result = JPSClient::Response.new(response)
@@ -36,7 +72,6 @@ module JPSClient
36
72
  # 处理 401 错误,自动重新登录
37
73
  if result.need_login?
38
74
  do_login(force_login: true)
39
- # 重试请求
40
75
  response = @http_client.post(path, body: body_params)
41
76
  result = JPSClient::Response.new(response)
42
77
  end
@@ -44,28 +79,53 @@ module JPSClient
44
79
  return result.to_h
45
80
  end
46
81
 
47
- # 获取提交记录列表(如果有这个接口)
48
- def get_commit_log_list(projectId:, params: {})
49
- config = @request_config && @request_config["commit_log_list"]
50
- raise JPSClient::ExceptionError, "Missing config for commit_log_list" unless config && config["url"]
82
+ # 获取提交记录简要信息
83
+ #
84
+ # @param commitId [String] 提交ID(必需)
85
+ # @return [Hash] API响应
86
+ def get_commit_log_simple(commitId:)
87
+ config = @request_config && @request_config["commit_log_simple"]
88
+ raise JPSClient::ExceptionError, "Missing config for commit_log_simple" unless config && config["url"]
51
89
  path = config["url"]
52
90
 
53
91
  get_params = {
54
- projectId: projectId,
55
- pageNo: params[:pageNo] || 1,
56
- pageSize: params[:pageSize] || 20
92
+ commitId: commitId
57
93
  }
58
94
 
59
- # 添加可选的筛选参数
60
- get_params[:branch] = params[:branch] if params[:branch]
61
- get_params[:author] = params[:author] if params[:author]
62
- get_params[:startTime] = params[:startTime] if params[:startTime]
63
- get_params[:endTime] = params[:endTime] if params[:endTime]
95
+ response = @http_client.get(path, params: get_params)
96
+ result = JPSClient::Response.new(response)
97
+
98
+ if result.need_login?
99
+ do_login(force_login: true)
100
+ response = @http_client.get(path, params: get_params)
101
+ result = JPSClient::Response.new(response)
102
+ end
103
+
104
+ return result.to_h
105
+ end
106
+
107
+ # 预览提交记录
108
+ #
109
+ # @param workflowId [Integer] 工作流ID(必需)
110
+ # @param commitIds [Array<String>] 提交ID数组(必需)
111
+ # @param params [Hash] 其他参数
112
+ # @option params [Boolean] :onlyCliff 是否只显示cliff格式
113
+ # @return [Hash] API响应
114
+ def get_commit_log_preview(workflowId:, commitIds:, params: {})
115
+ config = @request_config && @request_config["commit_log_preview"]
116
+ raise JPSClient::ExceptionError, "Missing config for commit_log_preview" unless config && config["url"]
117
+ path = config["url"]
118
+
119
+ get_params = {
120
+ workflowId: workflowId,
121
+ commitIds: commitIds.is_a?(Array) ? commitIds.join(',') : commitIds
122
+ }
123
+
124
+ get_params[:onlyCliff] = params[:onlyCliff] if params.key?(:onlyCliff)
64
125
 
65
126
  response = @http_client.get(path, params: get_params)
66
127
  result = JPSClient::Response.new(response)
67
128
 
68
- # 处理 401 错误
69
129
  if result.need_login?
70
130
  do_login(force_login: true)
71
131
  response = @http_client.get(path, params: get_params)
@@ -75,17 +135,44 @@ module JPSClient
75
135
  return result.to_h
76
136
  end
77
137
 
78
- # 获取单个提交记录详情
79
- def get_commit_log_detail(projectId:, commitId:)
80
- config = @request_config && @request_config["commit_log_detail"]
81
- raise JPSClient::ExceptionError, "Missing config for commit_log_detail" unless config && config["url"]
138
+ # 获取提交记录列表
139
+ #
140
+ # @param params [Hash] 查询参数
141
+ # @option params [Array<Integer>] :ids 提交记录ID数组
142
+ # @option params [Array<Integer>] :workflowIds 工作流ID数组
143
+ # @option params [Array<String>] :committers 提交者数组
144
+ # @option params [String] :repoPath 仓库路径
145
+ # @option params [String] :keyword 关键字
146
+ # @option params [String] :remark 备注
147
+ # @option params [Boolean] :onlyCliff 是否只显示cliff格式
148
+ # @option params [Boolean] :personalCenter 是否个人中心
149
+ # @option params [Integer] :pageNo 页码,默认1
150
+ # @option params [Integer] :pageSize 每页数量,默认20
151
+ # @option params [Integer] :startTimestamp 开始时间戳
152
+ # @option params [Integer] :endTimestamp 结束时间戳
153
+ # @return [Hash] API响应
154
+ def get_commit_log_list(params: {})
155
+ config = @request_config && @request_config["commit_log_list"]
156
+ raise JPSClient::ExceptionError, "Missing config for commit_log_list" unless config && config["url"]
82
157
  path = config["url"]
83
158
 
84
159
  get_params = {
85
- projectId: projectId,
86
- commitId: commitId
160
+ pageNo: params[:pageNo] || 1,
161
+ pageSize: params[:pageSize] || 20
87
162
  }
88
163
 
164
+ # 添加可选的筛选参数
165
+ get_params[:ids] = params[:ids].join(',') if params[:ids]
166
+ get_params[:workflowIds] = params[:workflowIds].join(',') if params[:workflowIds]
167
+ get_params[:committers] = params[:committers].join(',') if params[:committers]
168
+ get_params[:repoPath] = params[:repoPath] if params[:repoPath]
169
+ get_params[:keyword] = params[:keyword] if params[:keyword]
170
+ get_params[:remark] = params[:remark] if params[:remark]
171
+ get_params[:onlyCliff] = params[:onlyCliff] if params.key?(:onlyCliff)
172
+ get_params[:personalCenter] = params[:personalCenter] if params.key?(:personalCenter)
173
+ get_params[:startTimestamp] = params[:startTimestamp] if params[:startTimestamp]
174
+ get_params[:endTimestamp] = params[:endTimestamp] if params[:endTimestamp]
175
+
89
176
  response = @http_client.get(path, params: get_params)
90
177
  result = JPSClient::Response.new(response)
91
178
 
@@ -16,13 +16,39 @@ module JPSClient
16
16
  region: upload_config.region
17
17
  }
18
18
 
19
- response = @http_client.post(path, body: body_params, timeout: 60)
19
+ response = @http_client.post(path, body: body_params)
20
20
  result = JPSClient::Response.new(response)
21
21
 
22
22
  # 处理 401 错误,自动重新登录
23
23
  if result.need_login?
24
24
  do_login(force_login: true)
25
- response = @http_client.post(path, body: body_params, timeout: 60)
25
+ response = @http_client.post(path, body: body_params)
26
+ result = JPSClient::Response.new(response)
27
+ end
28
+
29
+ return result.to_h
30
+ end
31
+
32
+ # 获取简单上传预签名URL(用于小文件直接上传,使用 media 配置)
33
+ # @param s3_key [String] S3 文件路径
34
+ # @param upload_config [UploadConfig] 上传配置
35
+ # @return [Hash] 包含预签名URL的响应
36
+ def get_simple_sign_url(s3_key:, upload_config:)
37
+ path = @request_config["file_sign_url"]["url"]
38
+
39
+ body_params = {
40
+ s3Key: s3_key,
41
+ uploadType: upload_config.upload_type,
42
+ bucketName: upload_config.media_bucket_name,
43
+ region: upload_config.media_region
44
+ }
45
+
46
+ response = @http_client.post(path, body: body_params)
47
+ result = JPSClient::Response.new(response)
48
+
49
+ if result.need_login?
50
+ do_login(force_login: true)
51
+ response = @http_client.post(path, body: body_params)
26
52
  result = JPSClient::Response.new(response)
27
53
  end
28
54
 
@@ -42,12 +68,12 @@ module JPSClient
42
68
  region: upload_config.region
43
69
  }
44
70
 
45
- response = @http_client.post(path, body: body_params, timeout: 60)
71
+ response = @http_client.post(path, body: body_params)
46
72
  result = JPSClient::Response.new(response)
47
73
 
48
74
  if result.need_login?
49
75
  do_login(force_login: true)
50
- response = @http_client.post(path, body: body_params, timeout: 60)
76
+ response = @http_client.post(path, body: body_params)
51
77
  result = JPSClient::Response.new(response)
52
78
  end
53
79
 
@@ -66,13 +92,12 @@ module JPSClient
66
92
  region: upload_config.region
67
93
  }
68
94
 
69
- # 完成上传可能需要更长时间
70
- response = @http_client.post(path, body: body_params, timeout: 120)
95
+ response = @http_client.post(path, body: body_params)
71
96
  result = JPSClient::Response.new(response)
72
97
 
73
98
  if result.need_login?
74
99
  do_login(force_login: true)
75
- response = @http_client.post(path, body: body_params, timeout: 120)
100
+ response = @http_client.post(path, body: body_params)
76
101
  result = JPSClient::Response.new(response)
77
102
  end
78
103
 
@@ -5,6 +5,7 @@ module JPSClient
5
5
  #
6
6
  # 支持的功能:
7
7
  # - 上传项目包
8
+ # - 上传 NuGet 包
8
9
  # - 获取项目包列表
9
10
  # - 更新项目包信息
10
11
  # - 签名项目包
@@ -15,7 +16,10 @@ module JPSClient
15
16
  module ProjectPackage
16
17
 
17
18
  # 上传项目包
18
- def upload_project_package(projectId:nil, params:nil)
19
+ # @param projectId [String] 项目ID(必需)
20
+ # @param params [Hash] 上传参数
21
+ # @param timeout [Integer, nil] 请求超时时间(秒),nil 时使用 common_http_config.timeout_seconds
22
+ def upload_project_package(projectId:nil, params:nil, timeout:nil)
19
23
  config = @request_config && @request_config["project_package_upload"]
20
24
  raise JPSClient::ExceptionError, "Missing config for project_package_upload" unless config && config["url"]
21
25
  path = config["url"]
@@ -36,6 +40,58 @@ module JPSClient
36
40
  body_params[:chatEnv] ||= params&.dig(:chatEnv)
37
41
  body_params[:commitId] ||= params&.dig(:commitId)
38
42
  body_params[:projectPackageId] ||= params&.dig(:projectPackageId)
43
+ body_params[:workflowId] ||= params&.dig(:workflowId)
44
+
45
+ # 移除值为nil的键
46
+ body_params.compact!
47
+
48
+ response = @http_client.post(path, body: body_params, timeout: timeout)
49
+ result = JPSClient::Response.new(response)
50
+
51
+ # 处理 401 错误,自动重新登录
52
+ if result.need_login?
53
+ do_login(force_login: true)
54
+ # 重试请求
55
+ response = @http_client.post(path, body: body_params, timeout: timeout)
56
+ result = JPSClient::Response.new(response)
57
+ end
58
+
59
+ return result.to_h
60
+ end
61
+
62
+ # 上传 NuGet 包
63
+ # 用于上传 NuGet 包到项目,不需要 workflowId 参数
64
+ #
65
+ # @param projectId [String] 项目ID(必需)
66
+ # @param params [Hash] 可选参数
67
+ # @option params [String] :packageUrl 包文件URL(必需)
68
+ # @option params [String] :description 包描述
69
+ # @option params [Array<String>] :attachFileUrls 附件文件URL列表
70
+ # @option params [String] :chatEnv 聊天环境
71
+ # @option params [String] :commitId 提交ID
72
+ # @option params [String] :projectPackageId 项目包ID
73
+ # @return [Hash] API响应
74
+ def upload_nuget_package(projectId:nil, params:nil)
75
+ config = @request_config && @request_config["project_package_upload_nuget"]
76
+ raise JPSClient::ExceptionError, "Missing config for project_package_upload_nuget" unless config && config["url"]
77
+ path = config["url"]
78
+
79
+ body_params = {
80
+ projectId: projectId
81
+ }
82
+
83
+ # 合并传入的参数
84
+ if params
85
+ params.each { |key,value| body_params[key] = value }
86
+ end
87
+
88
+ # 确保必要的字段存在(不包括 workflowId)
89
+ body_params[:packageUrl] ||= params&.dig(:packageUrl)
90
+ body_params[:description] ||= params&.dig(:description)
91
+ body_params[:attachFileUrls] ||= params&.dig(:attachFileUrls) || []
92
+ body_params[:chatEnv] ||= params&.dig(:chatEnv)
93
+ body_params[:commitId] ||= params&.dig(:commitId)
94
+ body_params[:projectPackageId] ||= params&.dig(:projectPackageId)
39
95
 
40
96
  # 移除值为nil的键
41
97
  body_params.compact!
@@ -215,7 +271,7 @@ module JPSClient
215
271
  packId: params[:packId],
216
272
  packageType: params[:packageType],
217
273
  packageVersion: params[:packageVersion],
218
- projectName: params[:projectName]
274
+ projectId: params[:projectId]
219
275
  }.compact # 移除nil值
220
276
 
221
277
  response = @http_client.get(path, params: get_params)
@@ -3,6 +3,7 @@
3
3
  require 'webrick'
4
4
  require 'json'
5
5
  require 'net/http'
6
+ require 'net/https'
6
7
  require 'uri'
7
8
  require 'securerandom'
8
9
  require 'digest'
@@ -79,10 +80,25 @@ module JPSClient
79
80
  @server_port = @config.server_port
80
81
 
81
82
  @scope_list = [
83
+ 'offline_access',
82
84
  'task:task:write',
83
85
  'task:section:write',
84
86
  'task:custom_field:write',
85
- 'task:tasklist:write'
87
+ 'task:tasklist:write',
88
+ 'drive:drive',
89
+ 'wiki:wiki',
90
+ 'docx:document',
91
+ 'bitable:app',
92
+ 'contact:user.employee_id:readonly',
93
+ 'docs:document.content:read',
94
+ 'im:chat',
95
+ 'base:app:copy',
96
+ 'base:record:update',
97
+ 'task:comment:write',
98
+ 'task:comment',
99
+ 'task:attachment:write',
100
+ 'vc:room:readonly',
101
+ 'vc:meeting:readonly'
86
102
  ]
87
103
 
88
104
  @access_token = nil
@@ -258,6 +274,9 @@ module JPSClient
258
274
  # 使用授权码换取 token
259
275
  def exchange_code_for_token(code)
260
276
  begin
277
+ puts "🔄 获取授权码成功: #{code[0..10]}..."
278
+ puts "🔄 正在换取访问令牌..."
279
+
261
280
  request_data = {
262
281
  'code' => code,
263
282
  'redirectUri' => @redirect_uri,
@@ -268,6 +287,16 @@ module JPSClient
268
287
  http = Net::HTTP.new(uri.host, uri.port)
269
288
  http.use_ssl = (uri.scheme == 'https')
270
289
 
290
+ # 配置 SSL 以兼容 OpenSSL 3.x(避免 CRL 检查失败)
291
+ if http.use_ssl?
292
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
293
+ # 创建 SSL 上下文并禁用 CRL 检查
294
+ http.cert_store = OpenSSL::X509::Store.new
295
+ http.cert_store.set_default_paths
296
+ # verify_flags = 0 表示不检查 CRL
297
+ http.cert_store.flags = 0
298
+ end
299
+
271
300
  request = Net::HTTP::Post.new(uri)
272
301
  request['Content-Type'] = 'application/json'
273
302
  request.body = request_data.to_json
@@ -222,8 +222,17 @@ module JPSClient
222
222
  @token = @token_manager.to_h
223
223
  end
224
224
 
225
+ # 从 common_http_config 获取 HTTP 配置
226
+ common_http_config = @config_json["common_http_config"] || {}
227
+ default_timeout = common_http_config["timeout_seconds"]
228
+ max_retry_times = common_http_config["max_retry_times"] || 3
229
+
225
230
  # 初始化共享的 HTTP 客户端
226
- @http_client = HttpClient.new(base_url: @baseurl)
231
+ @http_client = HttpClient.new(
232
+ base_url: @baseurl,
233
+ default_timeout: default_timeout,
234
+ max_retry_times: max_retry_times
235
+ )
227
236
  @http_client.update_token(@token["token"]) if @token && @token["token"]
228
237
 
229
238
  rescue JSON::ParserError => error
@@ -7,19 +7,29 @@ module JPSClient
7
7
  class HttpClient
8
8
  attr_accessor :base_url
9
9
  attr_accessor :token
10
-
11
- def initialize(base_url: nil, token: nil)
10
+ attr_accessor :default_timeout
11
+ attr_accessor :max_retry_times
12
+
13
+ # @param base_url [String] API 基础 URL
14
+ # @param token [String] 认证 token
15
+ # @param default_timeout [Integer] 默认超时时间(秒),默认 60 秒
16
+ # @param max_retry_times [Integer] 最大重试次数,默认 3
17
+ def initialize(base_url: nil, token: nil, default_timeout: nil, max_retry_times: 3)
12
18
  @base_url = base_url
13
19
  @token = token
20
+ @default_timeout = default_timeout || 60
21
+ @max_retry_times = max_retry_times || 3
14
22
  @connection = nil
15
23
  end
16
24
 
17
25
  # GET 请求
18
- def get(path, params: nil)
19
- request(:get, path, params: params)
26
+ # @param timeout [Integer, nil] 超时时间,nil 时使用 default_timeout
27
+ def get(path, params: nil, timeout: nil)
28
+ request(:get, path, params: params, timeout: timeout)
20
29
  end
21
30
 
22
31
  # POST 请求
32
+ # @param timeout [Integer, nil] 超时时间,nil 时使用 default_timeout
23
33
  def post(path, body: nil, timeout: nil)
24
34
  request(:post, path, body: body, timeout: timeout)
25
35
  end
@@ -35,6 +45,16 @@ module JPSClient
35
45
  def request(method, path, params: nil, body: nil, timeout: nil)
36
46
  url = @base_url + path
37
47
 
48
+ # 如果没有指定 timeout,使用默认超时
49
+ actual_timeout = timeout || @default_timeout
50
+
51
+ # 调试输出:请求信息
52
+ if ENV['JPS_CLIENT_DEBUG']
53
+ puts "[JPS_CLIENT_DEBUG] HTTP #{method.upcase} #{url}"
54
+ puts "[JPS_CLIENT_DEBUG] Params: #{params.inspect}" if params
55
+ puts "[JPS_CLIENT_DEBUG] Timeout: #{actual_timeout}s" if actual_timeout
56
+ end
57
+
38
58
  begin
39
59
  response = connection.send(method) do |req|
40
60
  req.url url
@@ -42,11 +62,24 @@ module JPSClient
42
62
  req.headers['token'] = @token if @token
43
63
  req.params = params if params
44
64
  req.body = body.to_json if body && method == :post
45
- req.options.timeout = timeout if timeout
65
+ req.options.timeout = actual_timeout if actual_timeout
46
66
  end
47
67
 
48
- parse_response(response)
68
+ result = parse_response(response)
69
+
70
+ # 调试输出:响应信息
71
+ if ENV['JPS_CLIENT_DEBUG']
72
+ puts "[JPS_CLIENT_DEBUG] HTTP Response: code=#{result[:code]}, success=#{result[:success]}"
73
+ puts "[JPS_CLIENT_DEBUG] Response body: #{result[:body].inspect}" if result[:body]
74
+ end
75
+
76
+ result
49
77
  rescue Faraday::Error => e
78
+ # 调试输出:错误信息
79
+ if ENV['JPS_CLIENT_DEBUG']
80
+ puts "[JPS_CLIENT_DEBUG] HTTP Error: #{e.class} - #{e.message}"
81
+ end
82
+
50
83
  # 处理网络错误
51
84
  {
52
85
  code: 0,
@@ -62,7 +95,7 @@ module JPSClient
62
95
  @connection ||= Faraday.new do |config|
63
96
  # 重试配置
64
97
  config.request :retry, {
65
- max: 3,
98
+ max: @max_retry_times,
66
99
  interval: 0.5,
67
100
  backoff_factor: 2,
68
101
  interval_randomness: 0.5,
@@ -9,11 +9,14 @@ module JPSClient
9
9
  attr_accessor :access_key_secret
10
10
  attr_accessor :default_url
11
11
  attr_accessor :attach_url
12
- attr_accessor :upload_type # 新增:上传类型
13
- attr_accessor :concurrent_workers # 新增:并发上传工作线程数
14
- attr_accessor :chunk_size_mb # 新增:分片大小(MB)
15
- attr_accessor :max_retry_times # 新增:每个分片的最大重试次数
16
- attr_accessor :timeout_seconds # 新增:单个分片的超时时间(秒)
12
+ attr_accessor :upload_type # 上传类型
13
+ attr_accessor :concurrent_workers # 并发上传工作线程数
14
+ attr_accessor :chunk_size_mb # 分片大小(MB)
15
+ attr_accessor :max_retry_times # 每个分片的最大重试次数
16
+ attr_accessor :timeout_seconds # 单个分片的超时时间(秒)
17
+ attr_accessor :media_region # 媒体文件上传区域
18
+ attr_accessor :media_bucket_name # 媒体文件存储桶
19
+ attr_accessor :media_default_url # 媒体文件默认路径
17
20
 
18
21
  def initialize(
19
22
  region: nil,
@@ -26,7 +29,10 @@ module JPSClient
26
29
  concurrent_workers: nil,
27
30
  chunk_size_mb: nil,
28
31
  max_retry_times: nil,
29
- timeout_seconds: nil
32
+ timeout_seconds: nil,
33
+ media_region: nil,
34
+ media_bucket_name: nil,
35
+ media_default_url: nil
30
36
  )
31
37
  @region = region
32
38
  @bucket_name = bucket_name
@@ -39,6 +45,9 @@ module JPSClient
39
45
  @chunk_size_mb = chunk_size_mb
40
46
  @max_retry_times = max_retry_times
41
47
  @timeout_seconds = timeout_seconds
48
+ @media_region = media_region
49
+ @media_bucket_name = media_bucket_name
50
+ @media_default_url = media_default_url
42
51
  end
43
52
 
44
53
  # 从 JSON 配置创建实例
@@ -59,7 +68,10 @@ module JPSClient
59
68
  concurrent_workers: concurrent_workers,
60
69
  chunk_size_mb: json_config['chunk_size_mb'] || 5,
61
70
  max_retry_times: json_config['max_retry_times'] || 6, # 每个分片重试6次
62
- timeout_seconds: json_config['timeout_seconds'] || 600 # 单个分片超时10分钟
71
+ timeout_seconds: json_config['timeout_seconds'] || 600, # 单个分片超时10分钟
72
+ media_region: json_config['media_region'] || "ap-east-1",
73
+ media_bucket_name: json_config['media_bucket_name'] || "jps-resource",
74
+ media_default_url: json_config['media_default_url'] || "resource/"
63
75
  )
64
76
  end
65
77
 
@@ -126,7 +138,10 @@ module JPSClient
126
138
  'concurrent_workers' => @concurrent_workers,
127
139
  'chunk_size_mb' => @chunk_size_mb,
128
140
  'max_retry_times' => @max_retry_times,
129
- 'timeout_seconds' => @timeout_seconds
141
+ 'timeout_seconds' => @timeout_seconds,
142
+ 'media_region' => @media_region,
143
+ 'media_bucket_name' => @media_bucket_name,
144
+ 'media_default_url' => @media_default_url
130
145
  }
131
146
  end
132
147
  end
@@ -0,0 +1,374 @@
1
+ require 'securerandom'
2
+ require 'typhoeus'
3
+ require 'thread'
4
+ require 'jpsclient/base/exception'
5
+ require 'jpsclient/upload/upload_config'
6
+ require 'jpsclient/utils/logger'
7
+
8
+ module JPSClient
9
+ # Media 文件上传客户端
10
+ # 用于并发上传多个小文件(图片、视频等)到 S3
11
+ # 与 UploadClient 的区别:
12
+ # - UploadClient: 单个大文件 → 分片 → 并发上传分片
13
+ # - UploadMediaClient: 多个小文件 → 并发上传文件
14
+ class UploadMediaClient
15
+ attr_reader :jps_client
16
+
17
+ def initialize(jps_client)
18
+ raise ExceptionError, "必须提供 Client 实例" unless jps_client
19
+
20
+ @jps_client = jps_client
21
+
22
+ # 从 Client 获取配置
23
+ config_json = @jps_client.config_json
24
+
25
+ # 加载上传配置
26
+ @upload_config = UploadConfig.from_json(config_json["upload_config"])
27
+ unless @upload_config
28
+ raise ExceptionError, "上传配置无效或不完整"
29
+ end
30
+
31
+ # 线程安全的互斥锁
32
+ @results_mutex = Mutex.new
33
+ @tasks_queue_mutex = Mutex.new
34
+ @active_tasks_mutex = Mutex.new
35
+ @upload_failed_mutex = Mutex.new
36
+ @upload_failed = false
37
+ end
38
+
39
+ # 并发上传多个 media 文件
40
+ #
41
+ # @param file_paths [Array<String>] 要上传的文件路径列表
42
+ # @return [Hash] 上传结果
43
+ # - results: 每个文件的上传结果数组
44
+ # - success_urls: 成功上传的 URL 列表
45
+ # - failed_files: 上传失败的文件路径列表
46
+ # - total: 总文件数
47
+ # - success_count: 成功数
48
+ # - failed_count: 失败数
49
+ def upload_files(file_paths:)
50
+ result = {
51
+ "results" => [],
52
+ "success_urls" => [],
53
+ "failed_files" => [],
54
+ "total" => 0,
55
+ "success_count" => 0,
56
+ "failed_count" => 0
57
+ }
58
+
59
+ # 验证参数
60
+ if file_paths.nil? || file_paths.empty?
61
+ Logger.instance.fancyinfo_error("未提供要上传的文件")
62
+ return result
63
+ end
64
+
65
+ # 过滤有效文件
66
+ valid_files = file_paths.select { |f| File.exist?(f) }
67
+ invalid_files = file_paths - valid_files
68
+
69
+ # 记录无效文件
70
+ invalid_files.each do |f|
71
+ result["results"] << {
72
+ "file_path" => f,
73
+ "url" => nil,
74
+ "success" => false,
75
+ "error" => "文件不存在"
76
+ }
77
+ result["failed_files"] << f
78
+ end
79
+
80
+ if valid_files.empty?
81
+ result["total"] = file_paths.size
82
+ result["failed_count"] = invalid_files.size
83
+ Logger.instance.fancyinfo_error("没有有效的文件可上传")
84
+ return result
85
+ end
86
+
87
+ result["total"] = file_paths.size
88
+
89
+ # 重置状态
90
+ @upload_failed = false
91
+ @upload_results = []
92
+
93
+ # 准备任务队列
94
+ @tasks_queue = Queue.new
95
+ @worker_threads = []
96
+ @expected_files = valid_files.size
97
+ @active_tasks = 0
98
+
99
+ # 获取并发和重试配置
100
+ concurrent_workers = [@upload_config.concurrent_workers, valid_files.size].min
101
+ retry_count = @upload_config.max_retry_times
102
+
103
+ puts "准备上传 #{valid_files.size} 个文件"
104
+ puts "并发上传线程数: #{concurrent_workers}"
105
+ puts "失败重试次数: #{retry_count}"
106
+ puts
107
+
108
+ # 创建文件上传任务
109
+ valid_files.each do |file_path|
110
+ task_item = {
111
+ "file_path" => file_path,
112
+ "retry_count" => retry_count
113
+ }
114
+ @tasks_queue.push(task_item)
115
+ end
116
+
117
+ # 开始并发上传
118
+ Logger.instance.fancyinfo_start("开始上传...")
119
+
120
+ begin
121
+ concurrent_upload(concurrency: concurrent_workers)
122
+ ensure
123
+ cleanup_worker_threads
124
+ end
125
+
126
+ # 收集结果
127
+ @upload_results.each do |item|
128
+ result["results"] << item
129
+ if item["success"]
130
+ result["success_urls"] << item["url"]
131
+ result["success_count"] += 1
132
+ else
133
+ result["failed_files"] << item["file_path"]
134
+ result["failed_count"] += 1
135
+ end
136
+ end
137
+
138
+ # 加上之前的无效文件
139
+ result["failed_count"] += invalid_files.size
140
+
141
+ # 输出统计
142
+ if result["success_count"] > 0
143
+ Logger.instance.fancyinfo_success("上传完成! 成功: #{result["success_count"]}, 失败: #{result["failed_count"]}")
144
+ else
145
+ Logger.instance.fancyinfo_error("上传失败! 成功: #{result["success_count"]}, 失败: #{result["failed_count"]}")
146
+ end
147
+
148
+ return result
149
+ end
150
+
151
+ # 上传单个文件(便捷方法)
152
+ #
153
+ # @param file_path [String] 文件路径
154
+ # @return [String, nil] 成功返回 URL,失败返回 nil
155
+ def upload_file(file_path:)
156
+ result = upload_files(file_paths: [file_path])
157
+ result["success_urls"].first
158
+ end
159
+
160
+ private
161
+
162
+ # 并发上传控制
163
+ def concurrent_upload(concurrency: 1)
164
+ @worker_threads = []
165
+ @active_tasks = 0
166
+ @stop_workers = false
167
+ @task_complete_cv = ConditionVariable.new
168
+
169
+ # 创建工作线程
170
+ concurrency.times do
171
+ @worker_threads << Thread.new { worker_loop }
172
+ end
173
+
174
+ # 等待所有任务完成
175
+ @tasks_queue_mutex.synchronize do
176
+ while (@active_tasks > 0 || !@tasks_queue.empty?) && !upload_failed?
177
+ @task_complete_cv.wait(@tasks_queue_mutex, 30)
178
+ end
179
+ end
180
+
181
+ # 停止工作线程
182
+ @stop_workers = true
183
+ @tasks_queue_mutex.synchronize { @task_complete_cv.broadcast }
184
+
185
+ # 等待线程结束
186
+ @worker_threads.each do |t|
187
+ t.join(5)
188
+ t.kill if t.alive?
189
+ end
190
+ end
191
+
192
+ # 工作线程循环
193
+ def worker_loop
194
+ loop do
195
+ task_item = nil
196
+
197
+ # 从队列获取任务
198
+ @tasks_queue_mutex.synchronize do
199
+ return if @stop_workers || (upload_failed? && @tasks_queue.empty?)
200
+
201
+ if @tasks_queue.empty?
202
+ @task_complete_cv.wait(@tasks_queue_mutex, 1)
203
+ next
204
+ end
205
+
206
+ task_item = @tasks_queue.pop
207
+ end
208
+
209
+ # 增加活跃任务计数
210
+ @active_tasks_mutex.synchronize { @active_tasks += 1 } if task_item
211
+
212
+ # 处理任务
213
+ if task_item
214
+ begin
215
+ process_upload_task(task_item)
216
+ rescue => e
217
+ handle_task_error(task_item, e.message)
218
+ ensure
219
+ @active_tasks_mutex.synchronize { @active_tasks -= 1 }
220
+ @tasks_queue_mutex.synchronize { @task_complete_cv.broadcast }
221
+ end
222
+ end
223
+
224
+ break if upload_failed?
225
+ end
226
+ end
227
+
228
+ # 处理单个文件上传任务
229
+ def process_upload_task(task_item)
230
+ file_path = task_item["file_path"]
231
+ file_name = File.basename(file_path)
232
+
233
+ # 生成 S3 Key
234
+ file_uuid = SecureRandom.uuid
235
+ extension = File.extname(file_path)
236
+ s3_key = "#{@upload_config.media_default_url}#{file_uuid}#{extension}"
237
+
238
+ # 获取预签名 URL
239
+ sign_result = @jps_client.get_simple_sign_url(
240
+ s3_key: s3_key,
241
+ upload_config: @upload_config
242
+ )
243
+
244
+ unless sign_result && sign_result.dig("data", "url")
245
+ handle_retry(task_item, "获取预签名 URL 失败")
246
+ return
247
+ end
248
+
249
+ upload_url = sign_result["data"]["url"]
250
+
251
+ # 读取文件
252
+ begin
253
+ file_content = File.binread(file_path)
254
+ file_size = file_content.bytesize
255
+ rescue => e
256
+ handle_retry(task_item, "读取文件失败: #{e.message}")
257
+ return
258
+ end
259
+
260
+ # 计算超时时间(基于文件大小,最小 30 秒,最大 300 秒)
261
+ timeout = calculate_timeout(file_size)
262
+
263
+ # 上传文件
264
+ request = Typhoeus::Request.new(
265
+ upload_url,
266
+ method: :put,
267
+ body: file_content,
268
+ headers: {
269
+ 'Content-Type' => 'application/octet-stream',
270
+ 'Content-Length' => file_size.to_s
271
+ },
272
+ timeout: timeout,
273
+ connecttimeout: 30
274
+ )
275
+
276
+ response = request.run
277
+
278
+ if response.success?
279
+ # 构建文件访问 URL
280
+ file_url = "https://#{@upload_config.media_bucket_name}.s3.#{@upload_config.media_region}.amazonaws.com/#{s3_key}"
281
+
282
+ # 记录成功结果
283
+ @results_mutex.synchronize do
284
+ @upload_results << {
285
+ "file_path" => file_path,
286
+ "url" => file_url,
287
+ "success" => true,
288
+ "error" => nil
289
+ }
290
+ end
291
+
292
+ puts " ✅ #{file_name}"
293
+ elsif response.timed_out?
294
+ handle_retry(task_item, "上传超时")
295
+ elsif response.code == 0
296
+ error_msg = response.return_message || "网络错误"
297
+ handle_retry(task_item, error_msg)
298
+ else
299
+ handle_retry(task_item, "HTTP #{response.code}")
300
+ end
301
+ end
302
+
303
+ # 处理重试
304
+ def handle_retry(task_item, error_reason)
305
+ file_path = task_item["file_path"]
306
+ file_name = File.basename(file_path)
307
+
308
+ task_item["retry_count"] -= 1
309
+
310
+ if task_item["retry_count"] > 0
311
+ # 放回队列重试
312
+ puts " ⚠️ #{file_name} 失败: #{error_reason},准备重试..."
313
+ @tasks_queue_mutex.synchronize { @tasks_queue.push(task_item) }
314
+ else
315
+ # 达到最大重试次数,记录失败
316
+ puts " ❌ #{file_name} 失败: #{error_reason}"
317
+ @results_mutex.synchronize do
318
+ @upload_results << {
319
+ "file_path" => file_path,
320
+ "url" => nil,
321
+ "success" => false,
322
+ "error" => error_reason
323
+ }
324
+ end
325
+ end
326
+ end
327
+
328
+ # 处理任务异常
329
+ def handle_task_error(task_item, error_message)
330
+ file_path = task_item["file_path"]
331
+ @results_mutex.synchronize do
332
+ @upload_results << {
333
+ "file_path" => file_path,
334
+ "url" => nil,
335
+ "success" => false,
336
+ "error" => error_message
337
+ }
338
+ end
339
+ end
340
+
341
+ # 计算上传超时时间
342
+ def calculate_timeout(file_size)
343
+ # 假设最低速度 100KB/s
344
+ min_speed = 100 * 1024
345
+ calculated = (file_size.to_f / min_speed).ceil + 10
346
+
347
+ # 限制在 30-300 秒之间
348
+ [[calculated, 30].max, 300].min
349
+ end
350
+
351
+ # 检查是否上传失败
352
+ def upload_failed?
353
+ @upload_failed_mutex.synchronize { @upload_failed }
354
+ end
355
+
356
+ # 设置上传失败状态
357
+ def set_upload_failed(error_msg = nil)
358
+ @upload_failed_mutex.synchronize do
359
+ @upload_failed = true
360
+ Logger.instance.fancyinfo_error("上传失败: #{error_msg}") if error_msg
361
+ end
362
+ end
363
+
364
+ # 清理工作线程
365
+ def cleanup_worker_threads
366
+ return unless @worker_threads
367
+
368
+ @worker_threads.each do |thread|
369
+ thread.exit if thread.alive?
370
+ end
371
+ @worker_threads.clear
372
+ end
373
+ end
374
+ end
@@ -1,3 +1,3 @@
1
1
  module JPSClient
2
- VERSION = "1.4.0"
2
+ VERSION = "1.7.0"
3
3
  end
data/lib/jpsclient.rb CHANGED
@@ -4,9 +4,7 @@ require 'jpsclient/version'
4
4
  # 基础模块
5
5
  require 'jpsclient/base/exception'
6
6
  require 'jpsclient/base/api_config'
7
-
8
- # API 客户端
9
- require 'jpsclient/api/client'
7
+ require 'jpsclient/base/client'
10
8
 
11
9
  # 工具模块
12
10
  require 'jpsclient/utils/logger'
@@ -23,6 +21,7 @@ require 'jpsclient/auth/auth'
23
21
  require 'jpsclient/upload/upload_config'
24
22
  require 'jpsclient/upload/upload_progress'
25
23
  require 'jpsclient/upload/upload_client'
24
+ require 'jpsclient/upload/upload_media_client'
26
25
 
27
26
  module JPSClient
28
27
  class << self
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jpsclient
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Your Name
@@ -211,7 +211,6 @@ files:
211
211
  - lib/jpsclient/api/bug.rb
212
212
  - lib/jpsclient/api/category.rb
213
213
  - lib/jpsclient/api/cert.rb
214
- - lib/jpsclient/api/client.rb
215
214
  - lib/jpsclient/api/collect.rb
216
215
  - lib/jpsclient/api/collection.rb
217
216
  - lib/jpsclient/api/commit_log.rb
@@ -273,10 +272,12 @@ files:
273
272
  - lib/jpsclient/auth/auth.rb
274
273
  - lib/jpsclient/auth/token.rb
275
274
  - lib/jpsclient/base/api_config.rb
275
+ - lib/jpsclient/base/client.rb
276
276
  - lib/jpsclient/base/exception.rb
277
277
  - lib/jpsclient/http/http_client.rb
278
278
  - lib/jpsclient/upload/upload_client.rb
279
279
  - lib/jpsclient/upload/upload_config.rb
280
+ - lib/jpsclient/upload/upload_media_client.rb
280
281
  - lib/jpsclient/upload/upload_progress.rb
281
282
  - lib/jpsclient/utils/aes.rb
282
283
  - lib/jpsclient/utils/logger.rb