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:
parent
5351df773b
commit
bc1bdd8491
@ -16,21 +16,38 @@ impl RoomService {
|
|||||||
let user_id = ctx.user_id;
|
let user_id = ctx.user_id;
|
||||||
self.require_room_member(room_id, user_id).await?;
|
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))
|
.filter(room_ai::Column::Room.eq(room_id))
|
||||||
.all(&self.db)
|
.all(&self.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut responses = Vec::with_capacity(models.len());
|
if configs.is_empty() {
|
||||||
for model in models {
|
return Ok(Vec::new());
|
||||||
let model_name = ai_model::Entity::find_by_id(model.model)
|
}
|
||||||
.one(&self.db)
|
|
||||||
.await
|
// Batch-fetch all referenced models to avoid N+1 queries
|
||||||
.ok()
|
let model_ids: Vec<Uuid> = configs.iter().map(|c| c.model).collect();
|
||||||
.flatten()
|
let models = ai_model::Entity::find()
|
||||||
.map(|m| m.name)
|
.filter(ai_model::Column::Id.is_in(model_ids))
|
||||||
.unwrap_or_else(|| format!("AI {}", model.model));
|
.all(&self.db)
|
||||||
let mut resp = super::RoomAiResponse::from(model);
|
.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);
|
resp.model_name = Some(model_name);
|
||||||
responses.push(resp);
|
responses.push(resp);
|
||||||
}
|
}
|
||||||
@ -114,10 +131,9 @@ impl RoomService {
|
|||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|m| m.name)
|
.map(|m| m.name);
|
||||||
.unwrap_or_else(|| format!("AI {}", saved.model));
|
|
||||||
let mut resp = super::RoomAiResponse::from(saved);
|
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 {
|
if let Ok(room) = self.find_room_or_404(room_id).await {
|
||||||
self.publish_room_event(
|
self.publish_room_event(
|
||||||
|
|||||||
@ -170,11 +170,8 @@ export const DiscordMemberList = memo(function DiscordMemberList({
|
|||||||
title={`AI — ${aiConfigs.length}`}
|
title={`AI — ${aiConfigs.length}`}
|
||||||
icon={<Bot className="h-3 w-3" />}
|
icon={<Bot className="h-3 w-3" />}
|
||||||
>
|
>
|
||||||
{aiConfigs.map((ai) => {
|
{aiConfigs.filter((ai) => !!ai.modelName).map((ai) => {
|
||||||
// Fallback: try modelName, then short model ID (no provider prefix), then 'AI'
|
const label = ai.modelName!;
|
||||||
const label = ai.modelName
|
|
||||||
|| ai.model?.split('/').pop()
|
|
||||||
|| 'AI';
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={ai.model}
|
key={ai.model}
|
||||||
|
|||||||
@ -32,12 +32,15 @@ export const RoomAiTasksPanel = memo(function RoomAiTasksPanel({ roomId, onClose
|
|||||||
});
|
});
|
||||||
const data = (resp.data as any)?.data as any[] | undefined;
|
const data = (resp.data as any)?.data as any[] | undefined;
|
||||||
if (data) {
|
if (data) {
|
||||||
setAiConfigs(data.map((cfg: any) => ({
|
// Only include configs that have a valid model_name — never expose UID
|
||||||
id: cfg.id,
|
setAiConfigs(data
|
||||||
name: cfg.name || cfg.model_name || 'Unknown',
|
.filter((cfg: any) => !!cfg.model_name)
|
||||||
enabled: cfg.enabled !== false,
|
.map((cfg: any) => ({
|
||||||
system_prompt: cfg.system_prompt,
|
id: cfg.model,
|
||||||
})));
|
name: cfg.model_name,
|
||||||
|
enabled: cfg.enabled !== false,
|
||||||
|
system_prompt: cfg.system_prompt,
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load AI configs:', err);
|
console.error('Failed to load AI configs:', err);
|
||||||
|
|||||||
@ -260,7 +260,7 @@ export const RoomSettingsPanel = memo(function RoomSettingsPanel({
|
|||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<Bot className="h-4 w-4 shrink-0" style={{ color: 'var(--room-text-muted)' }} />
|
<Bot className="h-4 w-4 shrink-0" style={{ color: 'var(--room-text-muted)' }} />
|
||||||
<span className="text-sm truncate" style={{ color: 'var(--room-text)' }}>
|
<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>
|
</span>
|
||||||
{config.stream && (
|
{config.stream && (
|
||||||
<span
|
<span
|
||||||
|
|||||||
@ -145,12 +145,9 @@ export const MessageInput = forwardRef<MessageInputHandle, MessageInputProps>(fu
|
|||||||
avatar: m.user_info?.avatar_url ?? undefined,
|
avatar: m.user_info?.avatar_url ?? undefined,
|
||||||
})),
|
})),
|
||||||
channels: [] as { id: string; label: string; type: 'channel'; avatar?: string }[],
|
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,
|
id: cfg.model,
|
||||||
// Fallback: try modelName, then short model ID (no provider prefix), then 'AI'
|
label: cfg.modelName!,
|
||||||
label: cfg.modelName
|
|
||||||
|| cfg.model?.split('/').pop()
|
|
||||||
|| 'AI',
|
|
||||||
type: 'ai' as const,
|
type: 'ai' as const,
|
||||||
})),
|
})),
|
||||||
repos: projectRepos.map((r) => ({
|
repos: projectRepos.map((r) => ({
|
||||||
|
|||||||
@ -1361,7 +1361,8 @@ export function RoomProvider({
|
|||||||
configs = await fetchOnce();
|
configs = await fetchOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
setRoomAiConfigs(configs);
|
// Discard configs that still have no modelName — never expose UID
|
||||||
|
setRoomAiConfigs(configs.filter((c) => !!c.modelName));
|
||||||
} catch {
|
} catch {
|
||||||
setRoomAiConfigs([]);
|
setRoomAiConfigs([]);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user