mirror of
https://github.com/foss42/apidash.git
synced 2025-12-02 10:49:49 +08:00
feat: add support to fetch openapi spec from url
This commit is contained in:
@@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:apidash/providers/providers.dart';
|
import 'package:apidash/providers/providers.dart';
|
||||||
import 'package:apidash/models/models.dart';
|
import 'package:apidash/models/models.dart';
|
||||||
import 'package:apidash/utils/file_utils.dart';
|
|
||||||
import 'package:apidash/utils/utils.dart';
|
import 'package:apidash/utils/utils.dart';
|
||||||
import '../../../core/model/chat_attachment.dart';
|
import '../../../core/model/chat_attachment.dart';
|
||||||
import '../../../core/services/curl_import_service.dart';
|
import '../../../core/services/curl_import_service.dart';
|
||||||
@@ -96,6 +95,30 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect OpenAPI import flow: if the last system message was an OpenAPI import prompt,
|
||||||
|
// then treat pasted URL or raw spec as part of the import flow.
|
||||||
|
final lastSystemOpenApi = existingMessages.lastWhere(
|
||||||
|
(m) =>
|
||||||
|
m.role == MessageRole.system &&
|
||||||
|
m.messageType == ChatMessageType.importOpenApi,
|
||||||
|
orElse: () => ChatMessage(
|
||||||
|
id: '',
|
||||||
|
content: '',
|
||||||
|
role: MessageRole.system,
|
||||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final openApiFlowActive = lastSystemOpenApi.id.isNotEmpty;
|
||||||
|
if ((_looksLikeOpenApi(text) || _looksLikeUrl(text)) &&
|
||||||
|
(type == ChatMessageType.importOpenApi || openApiFlowActive)) {
|
||||||
|
if (_looksLikeOpenApi(text)) {
|
||||||
|
await handlePotentialOpenApiPaste(text);
|
||||||
|
} else {
|
||||||
|
await handlePotentialOpenApiUrl(text);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final promptBuilder = _ref.read(promptBuilderProvider);
|
final promptBuilder = _ref.read(promptBuilderProvider);
|
||||||
// Prepare a substituted copy of current request for prompt context
|
// Prepare a substituted copy of current request for prompt context
|
||||||
final currentReq = _currentRequest;
|
final currentReq = _currentRequest;
|
||||||
@@ -155,7 +178,7 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
ChatMessage(
|
ChatMessage(
|
||||||
id: getNewUuid(),
|
id: getNewUuid(),
|
||||||
content:
|
content:
|
||||||
'{"explnation":"Upload your OpenAPI (JSON or YAML) specification or paste it here.","actions":[${jsonEncode(uploadAction.toJson())}]}',
|
'{"explnation":"Upload your OpenAPI (JSON or YAML) specification, paste the full spec text, or paste a URL to a spec (e.g., https://api.example.com/openapi.json).","actions":[${jsonEncode(uploadAction.toJson())}]}',
|
||||||
role: MessageRole.system,
|
role: MessageRole.system,
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
messageType: ChatMessageType.importOpenApi,
|
messageType: ChatMessageType.importOpenApi,
|
||||||
@@ -164,6 +187,8 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
);
|
);
|
||||||
if (_looksLikeOpenApi(text)) {
|
if (_looksLikeOpenApi(text)) {
|
||||||
await handlePotentialOpenApiPaste(text);
|
await handlePotentialOpenApiPaste(text);
|
||||||
|
} else if (_looksLikeUrl(text)) {
|
||||||
|
await handlePotentialOpenApiUrl(text);
|
||||||
}
|
}
|
||||||
state = state.copyWith(isGenerating: false, currentStreamingResponse: '');
|
state = state.copyWith(isGenerating: false, currentStreamingResponse: '');
|
||||||
return;
|
return;
|
||||||
@@ -600,6 +625,131 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _looksLikeUrl(String input) {
|
||||||
|
final t = input.trim();
|
||||||
|
if (t.isEmpty) return false;
|
||||||
|
return t.startsWith('http://') || t.startsWith('https://');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handlePotentialOpenApiUrl(String text) async {
|
||||||
|
final trimmed = text.trim();
|
||||||
|
if (!_looksLikeUrl(trimmed)) return;
|
||||||
|
state = state.copyWith(isGenerating: true, currentStreamingResponse: '');
|
||||||
|
try {
|
||||||
|
// Build a simple GET using existing networking stack
|
||||||
|
final httpModel = HttpRequestModel(
|
||||||
|
method: HTTPVerb.get,
|
||||||
|
url: trimmed,
|
||||||
|
headers: const [
|
||||||
|
// Hint servers that we can accept JSON or YAML
|
||||||
|
NameValueModel(
|
||||||
|
name: 'Accept',
|
||||||
|
value: 'application/json, application/yaml, text/yaml, */*'),
|
||||||
|
],
|
||||||
|
isHeaderEnabledList: const [true],
|
||||||
|
);
|
||||||
|
|
||||||
|
final (resp, _, err) = await sendHttpRequest(
|
||||||
|
getNewUuid(),
|
||||||
|
APIType.rest,
|
||||||
|
httpModel,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (err != null) {
|
||||||
|
final safe = err.replaceAll('"', "'");
|
||||||
|
_appendSystem(
|
||||||
|
'{"explnation":"Failed to fetch URL: $safe","actions":[]}',
|
||||||
|
ChatMessageType.importOpenApi,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (resp == null) {
|
||||||
|
_appendSystem(
|
||||||
|
'{"explnation":"No response received when fetching the URL.","actions":[]}',
|
||||||
|
ChatMessageType.importOpenApi,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final body = resp.body;
|
||||||
|
if (body.trim().isEmpty) {
|
||||||
|
_appendSystem(
|
||||||
|
'{"explnation":"The fetched URL returned an empty body.","actions":[]}',
|
||||||
|
ChatMessageType.importOpenApi,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse fetched content as OpenAPI
|
||||||
|
final spec = OpenApiImportService.tryParseSpec(body);
|
||||||
|
if (spec == null) {
|
||||||
|
_appendSystem(
|
||||||
|
'{"explnation":"The fetched content does not look like a valid OpenAPI spec (JSON or YAML).","actions":[]}',
|
||||||
|
ChatMessageType.importOpenApi,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build insights and show picker (reuse local method)
|
||||||
|
String? insights;
|
||||||
|
try {
|
||||||
|
final ai = _selectedAIModel;
|
||||||
|
if (ai != null) {
|
||||||
|
final summary = OpenApiImportService.summaryForSpec(spec);
|
||||||
|
final meta = OpenApiImportService.extractSpecMeta(spec);
|
||||||
|
final sys = dash.DashbotPrompts()
|
||||||
|
.openApiInsightsPrompt(specSummary: summary, specMeta: meta);
|
||||||
|
final res = await _repo.sendChat(
|
||||||
|
request: ai.copyWith(
|
||||||
|
systemPrompt: sys,
|
||||||
|
userPrompt:
|
||||||
|
'Provide concise, actionable insights about these endpoints.',
|
||||||
|
stream: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (res != null && res.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final map = MessageJson.safeParse(res);
|
||||||
|
if (map['explnation'] is String) insights = map['explnation'];
|
||||||
|
} catch (_) {
|
||||||
|
insights = res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[OpenAPI URL] insights error: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
final picker = OpenApiImportService.buildOperationPicker(
|
||||||
|
spec,
|
||||||
|
insights: insights,
|
||||||
|
);
|
||||||
|
final rqId = _currentRequest?.id ?? 'global';
|
||||||
|
_addMessage(
|
||||||
|
rqId,
|
||||||
|
ChatMessage(
|
||||||
|
id: getNewUuid(),
|
||||||
|
content: jsonEncode(picker),
|
||||||
|
role: MessageRole.system,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
messageType: ChatMessageType.importOpenApi,
|
||||||
|
actions: (picker['actions'] as List)
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.map(ChatAction.fromJson)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
final safe = e.toString().replaceAll('"', "'");
|
||||||
|
_appendSystem(
|
||||||
|
'{"explnation":"Failed to fetch or parse OpenAPI from URL: $safe","actions":[]}',
|
||||||
|
ChatMessageType.importOpenApi,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
state = state.copyWith(isGenerating: false, currentStreamingResponse: '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> handlePotentialOpenApiPaste(String text) async {
|
Future<void> handlePotentialOpenApiPaste(String text) async {
|
||||||
final trimmed = text.trim();
|
final trimmed = text.trim();
|
||||||
if (!_looksLikeOpenApi(trimmed)) return;
|
if (!_looksLikeOpenApi(trimmed)) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user