feat(ios): add Fastlane xuqm_release lane, expand IM SDK

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-04-29 00:37:02 +08:00
父节点 437e4b042a
当前提交 b413573178
共有 3 个文件被更改,包括 215 次插入2 次删除

查看文件

@ -174,6 +174,7 @@ public struct ImMessage: Decodable {
public let extra: String? public let extra: String?
public let revoked: Bool public let revoked: Bool
public let createdAt: String public let createdAt: String
public let editedAt: Int64?
} }
``` ```

查看文件

@ -244,8 +244,13 @@ public final class ImSDK {
) )
} }
public func revokeMessage(messageId: String) { public func revokeMessage(messageId: String) async throws -> ImMessage {
client?.revoke(messageId: messageId) let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/messages/\(messageId)/revoke",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
} }
public func editMessage(messageId: String, content: String) async throws -> ImMessage { public func editMessage(messageId: String, content: String) async throws -> ImMessage {
@ -465,6 +470,26 @@ public final class ImSDK {
try await ApiClient.shared.request(path: "/api/im/groups/\(groupId)") try await ApiClient.shared.request(path: "/api/im/groups/\(groupId)")
} }
public func listGroupMembers(groupId: String) async throws -> [UserProfile] {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/members",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func searchGroupMembers(groupId: String, keyword: String, size: Int = 20) async throws -> [UserProfile] {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/members/search",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "keyword", value: keyword),
URLQueryItem(name: "size", value: String(size))
]
)
}
public func updateGroupInfo(groupId: String, name: String? = nil, announcement: String? = nil) async throws -> ImGroup { public func updateGroupInfo(groupId: String, name: String? = nil, announcement: String? = nil) async throws -> ImGroup {
try await ApiClient.shared.request( try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)", path: "/api/im/groups/\(groupId)",

187
scripts/Fastfile 普通文件
查看文件

@ -0,0 +1,187 @@
# XuqmGroup Update Service — iOS Fastlane Release Lane
#
# Requires: fastlane (gem install fastlane) — standard iOS platform toolchain
# Usage (from your iOS project root):
# fastlane xuqm_release
#
# Config: add a .env.xuqm file to your project root with:
# XUQM_SERVER_URL=https://update.dev.xuqinmin.com
# XUQM_APP_ID=your-app-id
# XUQM_API_TOKEN=your-api-token
# XUQM_SCHEME=MyApp
# XUQM_WORKSPACE=MyApp.xcworkspace
# XUQM_BUNDLE_ID=com.example.myapp
# XUQM_STORE_TARGETS=APP_STORE # comma-separated, e.g. "APP_STORE"
# XUQM_AUTO_PUBLISH_AFTER_REVIEW=false
# XUQM_WEBHOOK_URL= # optional
# XUQM_SCHEDULED_PUBLISH_AT= # optional, ISO datetime
# # For App Store Connect API (used by upload_to_app_store):
# APP_STORE_CONNECT_API_KEY_ID=XXXXXXXXXX
# APP_STORE_CONNECT_API_ISSUER_ID=xxxx-xxxx-xxxx-xxxx
# APP_STORE_CONNECT_API_KEY_FILEPATH=~/.appstoreconnect/AuthKey_XXXXXXXXXX.p8
require 'net/http'
require 'json'
require 'uri'
default_platform(:ios)
platform :ios do
desc "Build, upload to XuqmGroup update service, and submit to App Store"
lane :xuqm_release do
server_url = ENV.fetch("XUQM_SERVER_URL")
app_id = ENV.fetch("XUQM_APP_ID")
api_token = ENV.fetch("XUQM_API_TOKEN")
scheme = ENV.fetch("XUQM_SCHEME")
workspace = ENV.fetch("XUQM_WORKSPACE")
bundle_id = ENV.fetch("XUQM_BUNDLE_ID", "")
store_targets = ENV.fetch("XUQM_STORE_TARGETS", "")
auto_publish = ENV.fetch("XUQM_AUTO_PUBLISH_AFTER_REVIEW", "false") == "true"
webhook_url = ENV.fetch("XUQM_WEBHOOK_URL", "")
scheduled_at = ENV.fetch("XUQM_SCHEDULED_PUBLISH_AT", "")
# ── 1. Read local version ────────────────────────────────────────────
version_name = get_version_number(target: scheme)
version_code = get_build_number.to_i
UI.message("Local version: #{version_name} (#{version_code})")
# ── 2. Check server latest ───────────────────────────────────────────
server_version_code = xuqm_get_latest_version_code(server_url, app_id, api_token, "IOS")
UI.message("Server latest versionCode: #{server_version_code}")
if version_code <= server_version_code
UI.user_error!(
"Local versionCode (#{version_code}) must be greater than server (#{server_version_code}). " \
"Please bump CFBundleVersion before releasing."
)
end
# ── 3. Archive & export IPA ──────────────────────────────────────────
archive_path = File.join(Dir.tmpdir, "xuqm_#{scheme}.xcarchive")
ipa_output = File.join(Dir.tmpdir, "xuqm_#{scheme}_ipa")
build_app(
scheme: scheme,
workspace: workspace,
archive_path: archive_path,
output_directory: ipa_output,
export_method: "app-store",
silent: false,
)
ipa_path = Dir.glob("#{ipa_output}/*.ipa").first
UI.user_error!("IPA not found in #{ipa_output}") unless ipa_path
UI.success("IPA: #{ipa_path}")
# ── 4. Upload to XuqmGroup update service ────────────────────────────
change_log = prompt(text: "Release notes: ", ci_input: "")
version_id = xuqm_upload_ipa(
server_url: server_url,
app_id: app_id,
api_token: api_token,
ipa_path: ipa_path,
version_name: version_name,
version_code: version_code,
change_log: change_log,
package_name: bundle_id,
store_targets: store_targets.empty? ? [] : store_targets.split(",").map(&:strip),
auto_publish_after_review: auto_publish,
scheduled_publish_at: scheduled_at,
webhook_url: webhook_url,
)
UI.success("Uploaded, version ID: #{version_id}")
# ── 5. Submit to App Store via App Store Connect API ─────────────────
if store_targets.include?("APP_STORE")
UI.message("Submitting to App Store Connect...")
# Use Fastlane's built-in action — handles JWT auth, upload, and submission
# Requires APP_STORE_CONNECT_API_KEY_* env vars
api_key = app_store_connect_api_key(
key_id: ENV.fetch("APP_STORE_CONNECT_API_KEY_ID"),
issuer_id: ENV.fetch("APP_STORE_CONNECT_API_ISSUER_ID"),
key_filepath: ENV.fetch("APP_STORE_CONNECT_API_KEY_FILEPATH"),
)
upload_to_app_store(
api_key: api_key,
ipa: ipa_path,
submit_for_review: true,
force: true,
automatic_release: auto_publish,
skip_metadata: true,
skip_screenshots: true,
run_precheck_before_submit: false,
)
# Notify update service that we've submitted
xuqm_post(
url: "#{server_url}/api/v1/updates/store/app/#{version_id}/submit",
token: api_token,
body: { storeTypes: ["APP_STORE"] }.to_json,
)
UI.success("App Store submission done. Review status will update via webhook or manually.")
end
# ── 6. Trigger server-side store submission for other stores ──────────
other_stores = store_targets.split(",").map(&:strip).reject { |s| s == "APP_STORE" }
unless other_stores.empty?
xuqm_post(
url: "#{server_url}/api/v1/updates/store/app/#{version_id}/execute-submit",
token: api_token,
body: { storeTypes: other_stores }.to_json,
)
end
UI.success("Release complete. Version ID: #{version_id}")
UI.message(scheduled_at.empty? ? "Publish manually or wait for auto-publish." : "Will publish at #{scheduled_at}")
end
# ── Private helpers ──────────────────────────────────────────────────────
private_lane :xuqm_get_latest_version_code do |options|
uri = URI("#{options[:server_url]}/api/v1/updates/app/list?appId=#{options[:app_id]}&platform=#{options[:platform]}")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{options[:api_token]}"
resp = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |h| h.request(req) }
data = JSON.parse(resp.body)
codes = (data.dig("data") || []).filter_map { |v| v["versionCode"] }
codes.max || 0
end
private_lane :xuqm_upload_ipa do |options|
require 'net/http/post/multipart'
uri = URI("#{options[:server_url]}/api/v1/updates/app/upload")
request = Net::HTTP::Post::Multipart.new(uri.path,
"appId" => options[:app_id],
"platform" => "IOS",
"versionName" => options[:version_name],
"versionCode" => options[:version_code].to_s,
"changeLog" => options[:change_log].to_s,
"forceUpdate" => "false",
"packageName" => options[:package_name].to_s,
"autoPublishAfterReview" => options[:auto_publish_after_review].to_s,
"storeSubmitTargets" => options[:store_targets].empty? ? "" : options[:store_targets].to_json,
"scheduledPublishAt" => options[:scheduled_publish_at].to_s,
"webhookUrl" => options[:webhook_url].to_s,
"apkFile" => UploadIO.new(File.open(options[:ipa_path]), "application/octet-stream", File.basename(options[:ipa_path])),
)
request["Authorization"] = "Bearer #{options[:api_token]}"
resp = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |h| h.request(request) }
data = JSON.parse(resp.body)
data.dig("data", "id") || UI.user_error!("Upload failed: #{resp.body}")
end
private_lane :xuqm_post do |options|
uri = URI(options[:url])
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{options[:token]}"
req["Content-Type"] = "application/json"
req.body = options[:body]
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |h| h.request(req) }
end
end