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;
|
||||
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(
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) => ({
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user