Flutter stream
·2 min read
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:onote/main.dart';
import 'package:onote/model/prompt_message.dart';
import 'package:onote/service/auth/auth_service.dart';
import 'package:onote/service/logger_factory.dart';
import 'package:onote/util/http_util.dart';
class AiCompletionsService {
static final Logger logger = LoggerFactory.instance;
static MainController get mainController => Get.find<MainController>();
static Stream<String> completions({required List<PromptMessage> messages, CancelToken? cancelToken}) async* {
if (messages.isEmpty || AuthService.currentSession==null) {
yield "[DONE]";
return;
}
// request
final stream = HttpUtil.streamStringPost(
mainController.mcpConfig.aiApi,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${AuthService.currentSession?.accessToken}'
},
data: {
'model': mainController.mcpConfig.aiModel,
'stream': true,
'messages': PromptMessage.toMapList(messages)
},
cancelToken: cancelToken
);
try {
await for (final response in stream) {
List<String> dataline = response.toString().split("\n");
for (String line in dataline) {
if (line.trim().startsWith("data:")) {
String lineJson = line.trim().substring(5).trim();
if (lineJson=="[DONE]") {
yield "[DONE]";
}
else if (lineJson.contains("assistant")) {
dynamic dyn = JsonDecoder().convert(lineJson);
if (dyn!=null && dyn["choices"]!=null && dyn["choices"].length>0 && dyn["choices"][0]["delta"]!=null && dyn["choices"][0]["delta"]["content"]!=null) {
yield dyn["choices"][0]["delta"]["content"];
}
}
}
}
}
}
catch(e) {
logger.i(e);
yield "[ERROR]";
}
}
static Stream<String> streamStringPost(String url, {CancelToken? cancelToken, Map<String, String>? headers, dynamic data}) async* {
try {
final response = await getDioInstance().post(url, data: data,
options: Options(
headers: buildHeaders(headers),
responseType: ResponseType.stream
),
cancelToken: cancelToken
);
if (response.data!=null) {
await for (final line in response.data.stream) {
yield utf8.decode(line);
}
}
}
catch(e) {
logger.i(e);
}
}
}
supabase Edge function proxy
import { serve } from "https://deno.land/std@0.177.0/http/server.ts"
serve(async (req) => {
const apiKey = Deno.env.get("OPENROUTER_API_KEY")
if (!apiKey) {
return new Response("Missing Openrouter API key", { status: 500 })
}
const targetURL = "https://openrouter.ai/api/v1/chat/completions"
const res = await fetch(targetURL, {
method: req.method,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body: req.body,
})
return new Response(res.body, {
status: res.status,
headers: {
// 重要:保留内容类型为 `text/event-stream`,否则前端无法处理
"Content-Type": res.headers.get("Content-Type") ?? "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
})
})