前言
时间一转眼我的上一篇博文更新已经接近3个月有余。我这段时间都在干嘛?除了4月因为裁员的事情心有余悸,这两三个月我都在捣鼓Mac的GPT客户端。转眼我的Mac客户端Ai Chat - 你问我答也上线了2个多月了。这几天也在准备把之前一次性返回接口,改回流式返回。其实1.0上线的时候我也就遇到了这个问题,当我的Flutter Mac桌面包Archive后,使用官方HttpClient的接口直接请求OpenAi的接口,因为我们有墙,我必须开启VPN才能请求,但是就是每次Archive后我都无法再请求成功。当时为了快速上线,我选择了直接用原生Api也就是NSURlSession的Api然后通过PlatforChannel返回。但是最近想升级优化,不再通过此方式,所以也就捣鼓了下为什么HttpClient Archive后无法请求代理接口,所以就有了以下的研究。
研究过程
- 查阅HttpClient源码
我发现HttpClient有一个特别的方法引起了我的关注
/// Sets the function used to resolve the proxy server to be used for
/// opening a HTTP connection to the specified [url]. If this
/// function is not set, direct connections will always be used.
///
/// The string returned by [f] must be in the format used by browser
/// PAC (proxy auto-config) scripts. That is either
///
/// "DIRECT"
///
/// for using a direct connection or
///
/// "PROXY host:port"
///
/// for using the proxy server `host` on port `port`.
///
/// A configuration can contain several configuration elements
/// separated by semicolons, e.g.
///
/// "PROXY host:port; PROXY host2:port2; DIRECT"
///
/// The static function [findProxyFromEnvironment] on this class can
/// be used to implement proxy server resolving based on environment
/// variables.
void set findProxy(String Function(Uri url)? f);
这个方法的注释写得很明白了,只要实现这个方法,返回特别的代理,它就能强制某个Uri用代理接口,当时我一下就又了80%的信心能解决次问题了。
- 尝试使用默认代理配置
紧接着我又发现了有一个默认的配置
HttpClient client = HttpClient();
/// client.findProxy = HttpClient.findProxyFromEnvironment;
后面我尝试在debug以及Archive下分别打印了HttpClient.findProxyFromEnvironment。这个闭包函数。惊奇的发现,debug下,这个函数是能反正我当前的代理信息的(也就是全局翻墙配置)类似127.0.0.1:8888 这样的信息。但是Archive后的包是无法获取到的。
- 结论
这里就破案了,当Flutter Mac的包Archive后(而且开启了Sandbox,只有开启了App才能上架到AppStore),应用会有个安全措施,阻止你获取系统环境变量等敏感信息,导致自然也无法获取到代理信息,所以如果你的接口需要代理(也就是俗称的翻墙或提子)才能请求(如openai的接口),那么就会请求失败。
修改方案
既然由于无法获取到代理信息呆滞请求失败,那么我们只需要请求接口前把代理信息给获取到那么理论上就能成功请求了。那么我们如何能获取到代理信息呢?答案就是通过原生获取,由于原生苹果官方Api的强大,我们苹果官方原生是有接口能获取到代理信息的,这也间接解释了,为什么我1.0版本通过NSURlSession苹果官方的Http接口就没有这个问题。然后我们通过插件的方式,返回到Flutter端即可。
原生Swift接口如下
func getProxySetting() -> NSDictionary? {
guard let proxySettings = CFNetworkCopySystemProxySettings()?.takeUnretainedValue(),
let url = URL(string: "https://api.openai.com/") else {
return nil
}
let proxies = CFNetworkCopyProxiesForURL((url as CFURL), proxySettings).takeUnretainedValue() as NSArray
guard let settings = proxies.firstObject as? NSDictionary,
let _ = settings.object(forKey: (kCFProxyTypeKey as String)) as? String else {
return nil
}
if let hostName = settings.object(forKey: (kCFProxyHostNameKey as String)), let port = settings.object(forKey: (kCFProxyPortNumberKey as String)) {
return ["host":hostName, "port":port]
}
return nil;
}
由于时间的关系,我并没有直接自己编写插件而是自己使用了native_flutter_proxy这个插件。但是这个插件是不支持macos的,但是问题不大我们只需要把源码下载下来,新增一个macos下的插件,他们的原生代码和iOS是一样的。
Flutter端代码如下,我们只需要在请求之前,获取到代理信息,返回到findProxy方法即可。
bool enabled = false;
String? host;
int? port;
try {
ProxySetting settings = await NativeProxyReader.proxySetting;
enabled = settings.enabled;
host = settings.host;
port = settings.port;
} catch (e) {
print(e);
}
String steamData = "";
HttpClient httpClient = HttpClient();
if (enabled && host != null) {
httpClient.findProxy = (uri) {
return "PROXY $host:$port";
};
print("proxy enabled $host:$port");
} else {
print("not have proxy");
}
AppStore体验地址
附上目前效果,由于我的apiKey是支持GPT-4的,所以也可选择GPT-4返回
iOS版或m1系列mac 可以兼容安装。iOS版是直接使用私有云的,不订阅的话每天只能聊500token以下,望理解。另流式返回的还在审核中,iOS目前还是一次性返回。
Mac版
目前我的Mac应用只要使用自己的apiKey是能直接使用的(前提就是要你自己能翻墙以及有api key),或者也能订阅我的云服务器(那就不需要翻墙),但是要一定费用,不需要就不订阅就好了哈,[Sick]勿喷,毕竟辛苦肝了几个月了。