feat: embed a Flutter G4F client with Cronet, cookie jar, and origin WebView
Port the Oracle off the Python g4f container onto on-device providers: Chromium HTTP via Cronet, a shared cookie session, IterList retry, hosted OpenAI-compat aggregators, and same-origin WebView fallbacks instead of CORS fetch.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import 'dart:convert';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../http_client.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../http_util.dart';
|
||||
import '../models.dart';
|
||||
import '../sse.dart';
|
||||
import 'base_provider.dart';
|
||||
|
||||
/// z.ai GLM web chat with a guest JWT. Captcha-protected replies fail fast.
|
||||
class GlmZai extends BaseProvider {
|
||||
@override
|
||||
String get name => 'GLM';
|
||||
|
||||
@override
|
||||
bool get working => true;
|
||||
|
||||
@override
|
||||
List<String> get models => ['x-preview-l', 'GLM-5-Turbo', 'glm-4.7'];
|
||||
|
||||
@override
|
||||
String get defaultModel => 'x-preview-l';
|
||||
|
||||
static const _base = 'https://chat.z.ai';
|
||||
static const _uuid = Uuid();
|
||||
|
||||
@override
|
||||
Stream<String> createAsyncGenerator(
|
||||
String model,
|
||||
List<Message> messages, {
|
||||
bool stream = false,
|
||||
}) async* {
|
||||
final authRes = await g4fHttp.get(
|
||||
Uri.parse('$_base/api/v1/auths/'),
|
||||
headers: browserHeaders(origin: _base, referer: '$_base/', desktop: true),
|
||||
).timeout(const Duration(seconds: 12));
|
||||
if (authRes.statusCode != 200) {
|
||||
throw Exception('GLM auth ${authRes.statusCode}');
|
||||
}
|
||||
final auth = jsonDecode(authRes.body) as Map<String, dynamic>;
|
||||
final token = auth['token']?.toString();
|
||||
final userId = auth['id']?.toString() ?? '';
|
||||
if (token == null || token.isEmpty) {
|
||||
throw Exception('GLM: no guest token');
|
||||
}
|
||||
|
||||
final headers = browserHeaders(
|
||||
origin: _base,
|
||||
referer: '$_base/',
|
||||
contentType: 'application/json',
|
||||
desktop: true,
|
||||
extra: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'x-fe-version': 'prod-fe-1.1.69',
|
||||
'x-region': 'overseas',
|
||||
},
|
||||
);
|
||||
|
||||
String useModel = defaultModel;
|
||||
try {
|
||||
final modelsRes = await g4fHttp.get(
|
||||
Uri.parse('$_base/api/models'),
|
||||
headers: headers,
|
||||
).timeout(const Duration(seconds: 10));
|
||||
if (modelsRes.statusCode == 200) {
|
||||
final data = jsonDecode(modelsRes.body);
|
||||
final list = (data is Map ? data['data'] : data) as List?;
|
||||
if (list != null && list.isNotEmpty) {
|
||||
final ids = [
|
||||
for (final m in list)
|
||||
if (m is Map && m['id'] != null) m['id'].toString()
|
||||
];
|
||||
if (ids.contains(model)) {
|
||||
useModel = model;
|
||||
} else if (ids.isNotEmpty) {
|
||||
useModel = ids.first;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
final chatRes = await g4fHttp.post(
|
||||
Uri.parse('$_base/api/v1/chats/new'),
|
||||
headers: headers,
|
||||
body: jsonEncode({
|
||||
'chat': {
|
||||
'id': _uuid.v4(),
|
||||
'title': 'New Chat',
|
||||
'models': [useModel],
|
||||
'params': {},
|
||||
'history': {'messages': {}, 'currentId': null},
|
||||
'tags': [],
|
||||
'flags': [],
|
||||
'features': [],
|
||||
}
|
||||
}),
|
||||
).timeout(const Duration(seconds: 12));
|
||||
if (chatRes.statusCode != 200) {
|
||||
throw Exception('GLM new chat ${chatRes.statusCode}');
|
||||
}
|
||||
final chatJson = jsonDecode(chatRes.body) as Map<String, dynamic>;
|
||||
final chatId = (chatJson['id'] ?? (chatJson['chat'] as Map?)?['id'])?.toString();
|
||||
if (chatId == null) throw Exception('GLM: no chat id');
|
||||
|
||||
final ts = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final requestId = _uuid.v4();
|
||||
final sigSrc = 'requestId=$requestId,timestamp=$ts,user_id=$userId';
|
||||
final signature = Hmac(sha256, utf8.encode('')).convert(utf8.encode(sigSrc)).toString();
|
||||
headers['x-signature'] = signature;
|
||||
|
||||
final lastUser = getLastUserMessage(messages);
|
||||
final body = jsonEncode({
|
||||
'stream': true,
|
||||
'model': useModel,
|
||||
'messages': [
|
||||
{'role': 'user', 'content': lastUser}
|
||||
],
|
||||
'signature_prompt': lastUser.substring(0, lastUser.length.clamp(0, 500)),
|
||||
'params': {},
|
||||
'extra': {},
|
||||
'features': {
|
||||
'image_generation': false,
|
||||
'web_search': false,
|
||||
'auto_web_search': false,
|
||||
'preview_mode': true,
|
||||
'enable_thinking': false,
|
||||
},
|
||||
'chat_id': chatId,
|
||||
'id': _uuid.v4(),
|
||||
});
|
||||
|
||||
final url = Uri.parse(
|
||||
'$_base/api/v2/chat/completions?timestamp=$ts&requestId=$requestId&user_id=$userId&version=0.0.1&platform=web&token=$token',
|
||||
);
|
||||
final req = http.Request('POST', url);
|
||||
req.headers.addAll(headers);
|
||||
req.body = body;
|
||||
final res = await g4fHttp.send(req).timeout(const Duration(seconds: 30));
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception('GLM completions ${res.statusCode}');
|
||||
}
|
||||
|
||||
final collected = StringBuffer();
|
||||
await for (final line in splitSseLines(res.stream.transform(utf8.decoder))) {
|
||||
final trimmed = line.trim();
|
||||
if (trimmed.isEmpty || !trimmed.startsWith('data:')) continue;
|
||||
final data = trimmed.substring(5).trim();
|
||||
if (data.contains('CAPTCHA') || data.contains('captcha')) {
|
||||
throw Exception('GLM captcha required');
|
||||
}
|
||||
final content = extractSseContent(data);
|
||||
if (content != null && content.isNotEmpty) {
|
||||
collected.write(content);
|
||||
if (stream) yield content;
|
||||
}
|
||||
}
|
||||
final text = collected.toString().trim();
|
||||
if (!looksLikeModelOutput(text)) {
|
||||
throw Exception('GLM empty response');
|
||||
}
|
||||
if (!stream) yield text;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user