fix(room): never expose AI model UID to frontend

Backend:
- room_ai_list: batch-fetch models, skip entries where model_name
  cannot be resolved (instead of falling back to "AI {uid}")
- room_ai_upsert: return None for model_name when lookup fails
  (instead of "AI {uid}")

Frontend:
- room-context: discard configs with missing modelName after retries
- DiscordMemberList: filter out configs without modelName
- MessageInput: filter out configs without modelName
- RoomSettingsPanel: prefer model_name from API, fallback to
  availableModels lookup, never render raw UID
- RoomAiTasksPanel: fix broken id/name mapping (was cfg.id/cfg.name
  which don't exist), filter out configs without model_name
This commit is contained in:
ZhenYi 2026-04-28 23:21:45 +08:00
parent 5351df773b
commit bc1bdd8491
6 changed files with 46 additions and 32 deletions

View File

@ -16,21 +16,38 @@ impl RoomService {
let user_id = ctx.user_id;
self.require_room_member(room_id, user_id).await?;
let models = room_ai::Entity::find()
let configs = room_ai::Entity::find()
.filter(room_ai::Column::Room.eq(room_id))
.all(&self.db)
.await?;
let mut responses = Vec::with_capacity(models.len());
for model in models {
let model_name = ai_model::Entity::find_by_id(model.model)
.one(&self.db)
.await
.ok()
.flatten()
.map(|m| m.name)
.unwrap_or_else(|| format!("AI {}", model.model));
let mut resp = super::RoomAiResponse::from(model);
if configs.is_empty() {
return Ok(Vec::new());
}
// Batch-fetch all referenced models to avoid N+1 queries
let model_ids: Vec<Uuid> = configs.iter().map(|c| c.model).collect();
let models = ai_model::Entity::find()
.filter(ai_model::Column::Id.is_in(model_ids))
.all(&self.db)
.await?;
let model_names: std::collections::HashMap<Uuid, String> = models
.into_iter()
.map(|m| (m.id, m.name))
.collect();
let mut responses = Vec::with_capacity(configs.len());
for config in configs {
// Skip entries where model_name cannot be resolved — never expose UID
let Some(model_name) = model_names.get(&config.model).cloned() else {
tracing::warn!(
"room_ai_list: skipping config with unknown model_id={}",
config.model
);
continue;
};
let mut resp = super::RoomAiResponse::from(config);
resp.model_name = Some(model_name);
responses.push(resp);
}
@ -114,10 +131,9 @@ impl RoomService {
.await
.ok()
.flatten()
.map(|m| m.name)
.unwrap_or_else(|| format!("AI {}", saved.model));
.map(|m| m.name);
let mut resp = super::RoomAiResponse::from(saved);
resp.model_name = Some(model_name);
resp.model_name = model_name;
if let Ok(room) = self.find_room_or_404(room_id).await {
self.publish_room_event(

View File

@ -170,11 +170,8 @@ export const DiscordMemberList = memo(function DiscordMemberList({
title={`AI — ${aiConfigs.length}`}
icon={<Bot className="h-3 w-3" />}
>
{aiConfigs.map((ai) => {
// Fallback: try modelName, then short model ID (no provider prefix), then 'AI'
const label = ai.modelName
|| ai.model?.split('/').pop()
|| 'AI';
{aiConfigs.filter((ai) => !!ai.modelName).map((ai) => {
const label = ai.modelName!;
return (
<button
key={ai.model}

View File

@ -32,12 +32,15 @@ export const RoomAiTasksPanel = memo(function RoomAiTasksPanel({ roomId, onClose
});
const data = (resp.data as any)?.data as any[] | undefined;
if (data) {
setAiConfigs(data.map((cfg: any) => ({
id: cfg.id,
name: cfg.name || cfg.model_name || 'Unknown',
enabled: cfg.enabled !== false,
system_prompt: cfg.system_prompt,
})));
// Only include configs that have a valid model_name — never expose UID
setAiConfigs(data
.filter((cfg: any) => !!cfg.model_name)
.map((cfg: any) => ({
id: cfg.model,
name: cfg.model_name,
enabled: cfg.enabled !== false,
system_prompt: cfg.system_prompt,
})));
}
} catch (err) {
console.error('Failed to load AI configs:', err);

View File

@ -260,7 +260,7 @@ export const RoomSettingsPanel = memo(function RoomSettingsPanel({
<div className="flex items-center gap-2 min-w-0">
<Bot className="h-4 w-4 shrink-0" style={{ color: 'var(--room-text-muted)' }} />
<span className="text-sm truncate" style={{ color: 'var(--room-text)' }}>
{availableModels.find((m) => m.id === config.model)?.name ?? config.model}
{config.model_name ?? availableModels.find((m) => m.id === config.model)?.name ?? 'Unknown'}
</span>
{config.stream && (
<span

View File

@ -145,12 +145,9 @@ export const MessageInput = forwardRef<MessageInputHandle, MessageInputProps>(fu
avatar: m.user_info?.avatar_url ?? undefined,
})),
channels: [] as { id: string; label: string; type: 'channel'; avatar?: string }[],
ai: roomAiConfigs.map((cfg) => ({
ai: roomAiConfigs.filter((cfg) => !!cfg.modelName).map((cfg) => ({
id: cfg.model,
// Fallback: try modelName, then short model ID (no provider prefix), then 'AI'
label: cfg.modelName
|| cfg.model?.split('/').pop()
|| 'AI',
label: cfg.modelName!,
type: 'ai' as const,
})),
repos: projectRepos.map((r) => ({

View File

@ -1361,7 +1361,8 @@ export function RoomProvider({
configs = await fetchOnce();
}
setRoomAiConfigs(configs);
// Discard configs that still have no modelName — never expose UID
setRoomAiConfigs(configs.filter((c) => !!c.modelName));
} catch {
setRoomAiConfigs([]);
} finally {