fbf04d90db
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.
110 lines
3.6 KiB
Dart
110 lines
3.6 KiB
Dart
import 'dart:convert';
|
|
import '../http_util.dart';
|
|
import '../models.dart';
|
|
import '../sse.dart';
|
|
import 'webview_base_provider.dart';
|
|
|
|
/// Qwen chat (chat.qwen.ai). WAF is solved inside the hidden WebView.
|
|
class QwenChat extends WebViewBaseProvider {
|
|
@override
|
|
String get name => 'Qwen';
|
|
|
|
@override
|
|
bool get working => true;
|
|
|
|
@override
|
|
List<String> get models => ['qwen3.5-flash', 'qwen3.7-plus', 'qwen3-max-2026-01-23'];
|
|
|
|
@override
|
|
String get defaultModel => 'qwen3.5-flash';
|
|
|
|
@override
|
|
String get baseUrl => 'https://chat.qwen.ai/';
|
|
|
|
@override
|
|
String get apiEndpoint => 'https://chat.qwen.ai/api/v2/chat/completions';
|
|
|
|
@override
|
|
String buildJsFetchCode(String model, List<Message> messages, bool stream) {
|
|
final modelJson = jsonEncode(models.contains(model) ? model : defaultModel);
|
|
final promptJson = jsonEncode(getLastUserMessage(messages));
|
|
return '''
|
|
(async function() {
|
|
for (let i = 0; i < 16; i++) {
|
|
if (!document.documentElement.innerHTML.includes('aliyun_waf_aa')) break;
|
|
await new Promise(r => setTimeout(r, 400));
|
|
}
|
|
const headers = {'Content-Type': 'application/json', 'source': 'web'};
|
|
const created = await fetch('/api/v2/chats/new', {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({
|
|
title: 'New Chat',
|
|
models: [$modelJson],
|
|
chat_mode: 'normal',
|
|
chat_type: 't2t',
|
|
timestamp: Date.now()
|
|
})
|
|
});
|
|
const createdJson = await created.json();
|
|
const chatId = createdJson.data && createdJson.data.id;
|
|
if (!chatId) return JSON.stringify({error: createdJson});
|
|
const resp = await fetch('/api/v2/chat/completions?chat_id=' + chatId, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({
|
|
stream: false,
|
|
version: '2.1',
|
|
incremental_output: false,
|
|
chat_id: chatId,
|
|
chat_mode: 'normal',
|
|
model: $modelJson,
|
|
messages: [{
|
|
fid: crypto.randomUUID(),
|
|
role: 'user',
|
|
content: $promptJson,
|
|
user_action: 'chat',
|
|
files: [],
|
|
timestamp: Date.now(),
|
|
models: [$modelJson],
|
|
chat_type: 't2t',
|
|
extra: {meta: {subChatType: 't2t'}},
|
|
sub_chat_type: 't2t'
|
|
}],
|
|
timestamp: Date.now()
|
|
})
|
|
});
|
|
return await resp.text();
|
|
})();
|
|
''';
|
|
}
|
|
|
|
@override
|
|
String parseResponse(String rawResponse) {
|
|
final sse = extractAllSseContent(rawResponse).join();
|
|
if (looksLikeModelOutput(sse)) return sse;
|
|
try {
|
|
final json = jsonDecode(rawResponse);
|
|
if (json is Map) {
|
|
if (json['error'] != null) throw Exception(json['error']);
|
|
final choices = json['choices'];
|
|
if (choices is List && choices.isNotEmpty) {
|
|
final c = choices.first;
|
|
if (c is Map) {
|
|
final msg = c['message'];
|
|
if (msg is Map && msg['content'] is String) return msg['content'];
|
|
final delta = c['delta'];
|
|
if (delta is Map && delta['content'] is String) return delta['content'];
|
|
}
|
|
}
|
|
final data = json['data'];
|
|
if (data is Map && data['content'] is String) return data['content'];
|
|
}
|
|
} catch (e) {
|
|
if (e.toString().startsWith('Exception:')) rethrow;
|
|
}
|
|
if (looksLikeModelOutput(rawResponse)) return rawResponse.trim();
|
|
throw Exception('Failed to parse Qwen: ${rawResponse.substring(0, rawResponse.length.clamp(0, 180))}');
|
|
}
|
|
}
|