7f91366201
Wire cleromancy rolls and the once-per-UTC-day 000-999 word shuffle to RANDOM.ORG JSON-RPC, with a local CSPRNG fallback. Add settings for interpreter personas and a cleaner default oracle prompt. Replace proper names and bureaucratic filler in the 1000-word lexicon with sacred symbols.
268 lines
8.5 KiB
Dart
268 lines
8.5 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
import '../http_client.dart';
|
|
import '../http_util.dart';
|
|
import '../models.dart';
|
|
import 'base_provider.dart';
|
|
import 'webview_base_provider.dart';
|
|
|
|
/// Microsoft Copilot: Cronet session + WebSocket, then origin WebView (nodriver analog).
|
|
class CopilotApp extends BaseProvider {
|
|
@override
|
|
String get name => 'CopilotApp';
|
|
|
|
@override
|
|
String get url => 'https://copilot.microsoft.com';
|
|
|
|
@override
|
|
bool get working => !kIsWeb;
|
|
|
|
@override
|
|
Duration get requestTimeout => const Duration(seconds: 90);
|
|
|
|
@override
|
|
List<String> get models => ['smart', 'chat', 'reasoning'];
|
|
|
|
@override
|
|
String get defaultModel => 'smart';
|
|
|
|
static const _ua =
|
|
'CopilotNative/30.0.440505001-prod (Android 14; Google; Pixel 8 Pro)';
|
|
static const _startUrl = 'https://copilot.microsoft.com/c/api/start';
|
|
|
|
@override
|
|
Stream<String> createAsyncGenerator(
|
|
String model,
|
|
List<Message> messages, {
|
|
bool stream = false,
|
|
}) async* {
|
|
if (kIsWeb) {
|
|
throw UnsupportedError('CopilotApp requires dart:io (Android/iOS/desktop)');
|
|
}
|
|
|
|
final seeds = _fakeCookieSeeds();
|
|
_seedFakeCookies(seeds);
|
|
final prompt = formatPrompt(messages);
|
|
try {
|
|
yield await _viaRestAndWs(model, prompt);
|
|
return;
|
|
} catch (e) {
|
|
debugPrint('Copilot REST/WS failed, trying origin WebView: $e');
|
|
}
|
|
yield await _viaWebView(model, prompt, seeds);
|
|
}
|
|
|
|
Future<String> _viaRestAndWs(String model, String prompt) async {
|
|
final startReq = http.Request('POST', Uri.parse(_startUrl));
|
|
startReq.headers.addAll({
|
|
'content-type': 'application/json',
|
|
'user-agent': _ua,
|
|
'X-Search-UILang': 'en-US', 'Host': 'copilot.microsoft.com',
|
|
});
|
|
startReq.body = jsonEncode({
|
|
'timeZone': 'Europe/Moscow',
|
|
'startNewConversation': true,
|
|
'teenSupportEnabled': true,
|
|
'correctPersonalizationSetting': true,
|
|
'deferredDataUseCapable': true,
|
|
});
|
|
|
|
final startRes = await g4fHttp.send(startReq).timeout(const Duration(seconds: 15));
|
|
if (startRes.statusCode != 200) {
|
|
final err = await startRes.stream.bytesToString();
|
|
throw Exception('Copilot start ${startRes.statusCode}: $err');
|
|
}
|
|
final startBody = jsonDecode(utf8.decode(await startRes.stream.toBytes()));
|
|
final conversationId = (startBody['currentConversationId'] ?? '').toString();
|
|
if (conversationId.isEmpty) {
|
|
throw Exception('Copilot: no conversation id');
|
|
}
|
|
|
|
final cookieHeader = g4fCookies.headerFor(Uri.parse(url));
|
|
final sessionId = const Uuid().v4();
|
|
final wsUrl =
|
|
'wss://copilot.microsoft.com/c/api/chat?api-version=2&clientSessionId=$sessionId';
|
|
final ws = await WebSocket.connect(
|
|
wsUrl,
|
|
headers: {
|
|
'User-Agent': _ua,
|
|
'Pragma': 'no-cache',
|
|
'Cache-Control': 'no-cache',
|
|
if (cookieHeader.isNotEmpty) 'Cookie': cookieHeader,
|
|
},
|
|
).timeout(const Duration(seconds: 12));
|
|
|
|
try {
|
|
final mode = _modeFor(model);
|
|
var connected = false;
|
|
final collected = StringBuffer();
|
|
|
|
await for (final raw in ws.timeout(const Duration(seconds: 35))) {
|
|
if (raw is! String) continue;
|
|
Map<String, dynamic> data;
|
|
try {
|
|
data = jsonDecode(raw) as Map<String, dynamic>;
|
|
} catch (_) {
|
|
continue;
|
|
}
|
|
final event = data['event']?.toString();
|
|
if (event == 'connected' || event == 'received') {
|
|
if (!connected) {
|
|
connected = true;
|
|
ws.add(jsonEncode({
|
|
'event': 'send',
|
|
'conversationId': conversationId,
|
|
'content': [
|
|
{'type': 'text', 'text': prompt},
|
|
],
|
|
'mode': mode,
|
|
}));
|
|
}
|
|
continue;
|
|
}
|
|
if (event == 'appendText' || event == 'replaceText') {
|
|
final text = data['text']?.toString() ?? '';
|
|
if (text.isNotEmpty) collected.write(text);
|
|
} else if (event == 'done') {
|
|
break;
|
|
} else if (event == 'error') {
|
|
throw Exception('Copilot error: $data');
|
|
}
|
|
}
|
|
|
|
final text = collected.toString().trim();
|
|
if (!looksLikeModelOutput(text)) {
|
|
throw Exception('Copilot returned empty response');
|
|
}
|
|
return text;
|
|
} finally {
|
|
await ws.close();
|
|
}
|
|
}
|
|
|
|
Future<String> _viaWebView(
|
|
String model,
|
|
String prompt,
|
|
List<CookieSeed> seeds,
|
|
) async {
|
|
final promptJson = jsonEncode(prompt);
|
|
final modeJson = jsonEncode(_modeFor(model));
|
|
final raw = await runOriginWebView(
|
|
originUrl: url,
|
|
timeout: requestTimeout,
|
|
seeds: seeds,
|
|
functionBody: '''
|
|
const prompt = $promptJson;
|
|
const mode = $modeJson;
|
|
const start = await fetch('/c/api/start', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
|
|
startNewConversation: true,
|
|
teenSupportEnabled: true,
|
|
correctPersonalizationSetting: true,
|
|
deferredDataUseCapable: true
|
|
})
|
|
});
|
|
if (!start.ok) {
|
|
throw new Error('start ' + start.status + ' ' + (await start.text()).slice(0, 160));
|
|
}
|
|
const started = await start.json();
|
|
const conversationId = started.currentConversationId;
|
|
if (!conversationId) throw new Error('no conversation id');
|
|
const sessionId = (crypto.randomUUID && crypto.randomUUID()) || String(Math.random()).slice(2);
|
|
return await new Promise((resolve, reject) => {
|
|
const ws = new WebSocket('wss://copilot.microsoft.com/c/api/chat?api-version=2&clientSessionId=' + sessionId);
|
|
let text = '';
|
|
let sent = false;
|
|
const timer = setTimeout(() => { try { ws.close(); } catch (e) {} reject(new Error('ws timeout')); }, 45000);
|
|
ws.onmessage = (e) => {
|
|
let data;
|
|
try { data = JSON.parse(e.data); } catch (err) { return; }
|
|
const event = data.event;
|
|
if ((event === 'connected' || event === 'received') && !sent) {
|
|
sent = true;
|
|
ws.send(JSON.stringify({
|
|
event: 'send',
|
|
conversationId,
|
|
content: [{ type: 'text', text: prompt }],
|
|
mode
|
|
}));
|
|
}
|
|
if (event === 'appendText' || event === 'replaceText') text += data.text || '';
|
|
if (event === 'done') { clearTimeout(timer); ws.close(); resolve(text); }
|
|
if (event === 'error') { clearTimeout(timer); reject(new Error(JSON.stringify(data))); }
|
|
};
|
|
ws.onerror = () => { clearTimeout(timer); reject(new Error('ws error')); };
|
|
});
|
|
''',
|
|
);
|
|
final text = raw.trim();
|
|
if (!looksLikeModelOutput(text)) {
|
|
throw Exception('Copilot WebView unusable: ${text.substring(0, text.length.clamp(0, 180))}');
|
|
}
|
|
return text;
|
|
}
|
|
|
|
void _seedFakeCookies(List<CookieSeed> seeds) {
|
|
final uri = Uri.parse(url);
|
|
for (final seed in seeds) {
|
|
g4fCookies.set(
|
|
name: seed.name,
|
|
value: seed.value,
|
|
domain: seed.domain ?? 'microsoft.com',
|
|
path: seed.path,
|
|
secure: seed.isSecure,
|
|
hostOnly: false,
|
|
);
|
|
}
|
|
// Touch the jar host as well as the parent domain.
|
|
g4fCookies.set(
|
|
name: '_C_ETH',
|
|
value: '1',
|
|
domain: uri.host,
|
|
secure: true,
|
|
);
|
|
}
|
|
|
|
List<CookieSeed> _fakeCookieSeeds() {
|
|
return [
|
|
CookieSeed(name: '_C_ETH', value: '1', domain: '.microsoft.com'),
|
|
|
|
CookieSeed(name: 'MUID', value: _hex(32), domain: '.microsoft.com'),
|
|
CookieSeed(name: 'MUIDB', value: _hex(32), domain: '.microsoft.com'),
|
|
CookieSeed(
|
|
name: '_EDGE_S',
|
|
value: 'F=1&SID=${_hex(32)}',
|
|
domain: '.microsoft.com',
|
|
),
|
|
CookieSeed(name: '_EDGE_V', value: '1', domain: '.microsoft.com'),
|
|
];
|
|
}
|
|
|
|
String _modeFor(String model) {
|
|
final m = model.toLowerCase();
|
|
if (m.contains('reason') || m.contains('think') || m.contains('o1')) {
|
|
return 'reasoning';
|
|
}
|
|
if (m.contains('chat') || m.contains('gpt-4')) return 'chat';
|
|
return 'smart';
|
|
}
|
|
|
|
String _hex(int len) {
|
|
const hex = '0123456789ABCDEF';
|
|
final r = Random();
|
|
return String.fromCharCodes(
|
|
Iterable.generate(len, (_) => hex.codeUnitAt(r.nextInt(16))),
|
|
);
|
|
}
|
|
}
|