Files
Alexyz 7f91366201 feat: Random.org dice and daily word matrix, interpreter voices, curated lexicon
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.
2026-09-08 13:46:11 +05:00

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))),
);
}
}