feat(hub-client): GetInstalledModuleDocs + per-file MIME

- New getInstalledModuleDocs(name, locale) wraps the new gRPC
  RPC; callers get text + source path + a not-installed flag so
  Studio can decide whether to show a Documentation section at
  all.
- runSavedFlow gains an optional fileMimeTypes map keyed
  parallel to fileInputs. Hard-coded application/octet-stream
  remains as the fallback, but text.extract and other
  MIME-gating modules now get an accurate type when the caller
  knows one.
- fetchModuleDocs is marked @Deprecated; it still works for
  older consumers but Studio no longer calls it.
- Proto bindings regenerated against fai/platform proto.

Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-25 11:17:31 +02:00
parent b8bcae4b14
commit 0879a95aba
4 changed files with 240 additions and 9 deletions

View file

@ -59,6 +59,7 @@ typedef ClearEventLogResponse = pb.ClearEventLogResponse;
typedef DaemonPathsResponse = pb.DaemonPathsResponse;
typedef UninstallModuleResponse = pb.UninstallModuleResponse;
typedef FetchModuleDocsResponse = pb.FetchModuleDocsResponse;
typedef GetInstalledModuleDocsResponse = pb.GetInstalledModuleDocsResponse;
typedef ListMcpClientsResponse = pb.ListMcpClientsResponse;
typedef McpClientStatus = pb.McpClientStatus;
typedef McpClientConfigEntry = pb.McpClientConfigEntry;
@ -240,6 +241,10 @@ class HubClient {
/// when set, so Studio doesn't have to handle credentials.
/// Errors come back inside [FetchModuleDocsResponse.errorKind]
/// (not as exceptions) so the UI can render fallbacks.
///
/// Deprecated: prefer [getInstalledModuleDocs], which reads
/// `MODULE.md`/`MODULE.<locale>.md` shipped inside the bundle.
@Deprecated('Use getInstalledModuleDocs (reads from bundle, no network)')
Future<FetchModuleDocsResponse> fetchModuleDocs(
String name, {
String locale = '',
@ -249,6 +254,25 @@ class HubClient {
);
}
/// Read the installed module's `MODULE.md` (or `MODULE.<locale>.md`)
/// from disk under `~/.fai/modules/<module>/`. No network. Works in
/// air-gapped installs. The response distinguishes three states via
/// [GetInstalledModuleDocsResponse.notInstalled] and an empty
/// [GetInstalledModuleDocsResponse.text]:
///
/// - text non-empty docs found, render them
/// - text empty + notInstalled=false installed but bundle had
/// no inline docs
/// - text empty + notInstalled=true module is not installed
Future<GetInstalledModuleDocsResponse> getInstalledModuleDocs(
String name, {
String locale = '',
}) {
return _admin.getInstalledModuleDocs(
pb.GetInstalledModuleDocsRequest(name: name, locale: locale),
);
}
/// List every configured MCP server with the last-discovery
/// snapshot. Drives Studio's MCP-clients editor.
Future<ListMcpClientsResponse> listMcpClients() {
@ -489,27 +513,33 @@ class HubClient {
/// flow that mixes text and binary inputs (e.g. extract
/// taking a `document: bytes`) flows through one RPC:
/// * [textInputs] string values wrapped as text Payloads
/// * [fileInputs] raw bytes wrapped as bytes Payloads,
/// with `application/octet-stream` MIME
/// type. Caller can override the MIME by
/// passing a [bytePayloads] entry instead.
/// Both maps default to empty so existing text-only callers
/// * [fileInputs] raw bytes wrapped as bytes Payloads.
/// MIME type defaults to
/// `application/octet-stream`; clients that
/// know the real type pass it via
/// [fileMimeTypes] (same key). Modules
/// typically gate on MIME e.g.
/// `text.extract` rejects octet-stream so
/// accurate types matter.
/// All maps default to empty so existing text-only callers
/// are backward-compatible. Keys must not collide between
/// the maps; on collision the bytes-shaped entry wins (the
/// less-common case is more likely to be the operator's
/// intent).
/// [textInputs] and [fileInputs]; on collision the bytes-shaped
/// entry wins (the less-common case is more likely to be the
/// operator's intent).
Future<SubmitResponse> runSavedFlow({
required String name,
Map<String, String> textInputs = const {},
Map<String, Uint8List> fileInputs = const {},
Map<String, String> fileMimeTypes = const {},
}) async {
final req = pb.RunSavedFlowRequest()..name = name;
for (final entry in textInputs.entries) {
req.inputs[entry.key] = pb_common.Payload()..text = entry.value;
}
for (final entry in fileInputs.entries) {
final mime = fileMimeTypes[entry.key] ?? 'application/octet-stream';
final bytes = pb_common.Bytes()
..mimeType = 'application/octet-stream'
..mimeType = mime
..data = entry.value;
req.inputs[entry.key] = pb_common.Payload()..bytes = bytes;
}