一洼绿地

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