mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-09-21 13:52:09 +08:00
fix: harden shared agent source permissions
This commit is contained in:
@@ -36,7 +36,7 @@ export function useStream() {
|
||||
let renderTimer: number | null = null
|
||||
|
||||
// 启动流式请求
|
||||
const startStream = async (params: { session_id: any; query: any; knowledge_base_ids?: string[]; knowledge_ids?: string[]; tag_ids?: string[]; agent_enabled?: boolean; agent_id?: string; web_search_enabled?: boolean; summary_model_id?: string; mcp_service_ids?: string[]; skill_names?: string[]; mentioned_items?: Array<{id: string; name: string; type: string; kb_type?: string; kb_id?: string; kb_name?: string; service_id?: string; skill_name?: string}>; images?: Array<{data: string}>; attachment_uploads?: Array<{data: string; file_name: string; file_size: number}>; attachment_ids?: string[]; suggestion_attribution?: { suggestion_set_id: string; question_id: string }; method: string; url: string; embed_token?: string; embed_session_sig?: string; embed_visitor_id?: string }) => {
|
||||
const startStream = async (params: { session_id: any; query: any; knowledge_base_ids?: string[]; knowledge_ids?: string[]; tag_ids?: string[]; agent_enabled?: boolean; agent_id?: string; agent_source_tenant_id?: string | number; web_search_enabled?: boolean; summary_model_id?: string; mcp_service_ids?: string[]; skill_names?: string[]; mentioned_items?: Array<{id: string; name: string; type: string; kb_type?: string; kb_id?: string; kb_name?: string; service_id?: string; skill_name?: string}>; images?: Array<{data: string}>; attachment_uploads?: Array<{data: string; file_name: string; file_size: number}>; attachment_ids?: string[]; suggestion_attribution?: { suggestion_set_id: string; question_id: string }; method: string; url: string; embed_token?: string; embed_session_sig?: string; embed_visitor_id?: string }) => {
|
||||
const myGeneration = ++streamGeneration
|
||||
// 重置状态
|
||||
output.value = '';
|
||||
@@ -99,6 +99,9 @@ export function useStream() {
|
||||
if (params.agent_id) {
|
||||
postBody.agent_id = params.agent_id;
|
||||
}
|
||||
if (params.agent_source_tenant_id) {
|
||||
postBody.agent_source_tenant_id = Number(params.agent_source_tenant_id);
|
||||
}
|
||||
// Include web_search_enabled if provided
|
||||
if (params.web_search_enabled !== undefined) {
|
||||
postBody.web_search_enabled = params.web_search_enabled;
|
||||
|
||||
@@ -26,12 +26,14 @@ export function uploadTemporaryAttachment(
|
||||
sessionId: string,
|
||||
file: File,
|
||||
agentId?: string,
|
||||
agentSourceTenantId?: string,
|
||||
parserEngine?: string,
|
||||
onProgress?: (percent: number) => void,
|
||||
): Promise<AttachmentResponse> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
if (agentId) form.append('agent_id', agentId);
|
||||
if (agentSourceTenantId) form.append('agent_source_tenant_id', agentSourceTenantId);
|
||||
if (parserEngine) form.append('parser_engine', parserEngine);
|
||||
return postUpload(
|
||||
`/api/v1/sessions/${sessionId}/attachments`,
|
||||
|
||||
@@ -29,6 +29,7 @@ export async function listKnowledgeBaseActivity(
|
||||
// 知识库管理 API(列表、创建、获取、更新、删除、复制)
|
||||
export function listKnowledgeBases(params?: {
|
||||
agent_id?: string;
|
||||
agent_source_tenant_id?: string;
|
||||
/**
|
||||
* Optional creator filter. Server-side semantics:
|
||||
* - "mine" → only KBs whose creator_id matches the caller
|
||||
@@ -41,6 +42,7 @@ export function listKnowledgeBases(params?: {
|
||||
}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params?.agent_id) query.set('agent_id', params.agent_id);
|
||||
if (params?.agent_source_tenant_id) query.set('agent_source_tenant_id', params.agent_source_tenant_id);
|
||||
if (params?.creator && params.creator !== 'all') query.set('creator', params.creator);
|
||||
const qs = query.toString();
|
||||
return get(qs ? `/api/v1/knowledge-bases?${qs}` : '/api/v1/knowledge-bases');
|
||||
@@ -125,9 +127,10 @@ export function createKnowledgeBase(data: {
|
||||
return post(`/api/v1/knowledge-bases`, data);
|
||||
}
|
||||
|
||||
export function getKnowledgeBaseById(id: string, options?: { agent_id?: string }) {
|
||||
export function getKnowledgeBaseById(id: string, options?: { agent_id?: string; agent_source_tenant_id?: string }) {
|
||||
const query = new URLSearchParams();
|
||||
if (options?.agent_id) query.set('agent_id', options.agent_id);
|
||||
if (options?.agent_source_tenant_id) query.set('agent_source_tenant_id', options.agent_source_tenant_id);
|
||||
const qs = query.toString();
|
||||
return get(qs ? `/api/v1/knowledge-bases/${id}?${qs}` : `/api/v1/knowledge-bases/${id}`);
|
||||
}
|
||||
@@ -277,9 +280,10 @@ export function listKnowledgeFiles(
|
||||
return get(`/api/v1/knowledge-bases/${kbId}/knowledge?${qs}`);
|
||||
}
|
||||
|
||||
export function getKnowledgeDetails(id: string, options?: { agent_id?: string }) {
|
||||
export function getKnowledgeDetails(id: string, options?: { agent_id?: string; agent_source_tenant_id?: string }) {
|
||||
const query = new URLSearchParams();
|
||||
if (options?.agent_id) query.set('agent_id', options.agent_id);
|
||||
if (options?.agent_source_tenant_id) query.set('agent_source_tenant_id', options.agent_source_tenant_id);
|
||||
const qs = query.toString();
|
||||
return get(qs ? `/api/v1/knowledge/${id}?${qs}` : `/api/v1/knowledge/${id}`);
|
||||
}
|
||||
@@ -322,10 +326,11 @@ export function previewKnowledgeFile(id: string) {
|
||||
}
|
||||
|
||||
/** @param idsQueryString - query string with ids (e.g. ids=xxx&ids=yyy) */
|
||||
export function batchQueryKnowledge(idsQueryString: string, kbId?: string, agentId?: string) {
|
||||
export function batchQueryKnowledge(idsQueryString: string, kbId?: string, agentId?: string, agentSourceTenantId?: string) {
|
||||
let qs = idsQueryString;
|
||||
if (kbId) qs += `&kb_id=${encodeURIComponent(kbId)}`;
|
||||
if (agentId) qs += `&agent_id=${encodeURIComponent(agentId)}`;
|
||||
if (agentSourceTenantId) qs += `&agent_source_tenant_id=${encodeURIComponent(agentSourceTenantId)}`;
|
||||
return get(`/api/v1/knowledge/batch?${qs}`);
|
||||
}
|
||||
|
||||
@@ -539,7 +544,7 @@ export function searchKnowledge(
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
fileTypes?: string[],
|
||||
options?: { agent_id?: string; recent?: boolean }
|
||||
options?: { agent_id?: string; agent_source_tenant_id?: string; recent?: boolean }
|
||||
) {
|
||||
const query = new URLSearchParams();
|
||||
if (keyword) {
|
||||
@@ -551,6 +556,7 @@ export function searchKnowledge(
|
||||
query.set('file_types', fileTypes.join(','));
|
||||
}
|
||||
if (options?.agent_id) query.set('agent_id', options.agent_id);
|
||||
if (options?.agent_source_tenant_id) query.set('agent_source_tenant_id', options.agent_source_tenant_id);
|
||||
if (options?.recent) query.set('recent', 'true');
|
||||
return get(`/api/v1/knowledge/search?${query.toString()}`);
|
||||
}
|
||||
|
||||
@@ -311,6 +311,8 @@ export interface SharedAgentInfo {
|
||||
shared_at: string
|
||||
shared_by_user_id?: string
|
||||
shared_by_username?: string
|
||||
/** 由后端在源空间解析,不与当前空间的搜索引擎列表比较 */
|
||||
web_search_ready: boolean
|
||||
/** 当前用户是否已停用该共享智能体(仅影响本人对话下拉显示) */
|
||||
disabled_by_me?: boolean
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ const emit = defineEmits<{
|
||||
type AgentDetailTarget = {
|
||||
agent: CustomAgent;
|
||||
sourceTenantId?: string;
|
||||
sharedMeta?: { org_name?: string; shared_by_username?: string };
|
||||
sharedMeta?: { org_name?: string; shared_by_username?: string; web_search_ready?: boolean };
|
||||
};
|
||||
|
||||
type SharedAgentSelection = Omit<SharedAgentInfo, 'agent'> & {
|
||||
@@ -322,7 +322,11 @@ const isWebSearchEnabledForAgent = (agent: CustomAgent): boolean => {
|
||||
};
|
||||
|
||||
const isWebSearchReadyForAgent = (agent: CustomAgent): boolean => {
|
||||
return isAgentWebSearchReady(agent.config, webSearchProviders.value);
|
||||
return isAgentWebSearchReady(
|
||||
agent.config,
|
||||
webSearchProviders.value,
|
||||
activeDetail.value?.sourceTenantId ? activeDetail.value.sharedMeta?.web_search_ready : undefined,
|
||||
);
|
||||
};
|
||||
|
||||
const isImageUploadEnabledForAgent = (agent: CustomAgent): boolean => {
|
||||
@@ -475,6 +479,7 @@ const onSharedOptionEnter = (shared: SharedAgentSelection, event: MouseEvent) =>
|
||||
onOptionEnter(shared.agent, event, String(shared.source_tenant_id), {
|
||||
org_name: shared.org_name,
|
||||
shared_by_username: shared.shared_by_username,
|
||||
web_search_ready: shared.web_search_ready,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ const props = defineProps<{
|
||||
disabled?: boolean;
|
||||
sessionId?: string;
|
||||
agentId?: string;
|
||||
agentSourceTenantId?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -139,6 +140,7 @@ const uploadAttachment = async (attachment: AttachmentFile) => {
|
||||
props.sessionId,
|
||||
attachment.file,
|
||||
props.agentId,
|
||||
props.agentSourceTenantId,
|
||||
'auto',
|
||||
(progress) => {
|
||||
attachment.progress = progress;
|
||||
|
||||
@@ -173,6 +173,13 @@ const selectedAgent = computed(() => {
|
||||
config: { agent_mode: 'quick-answer' as const }
|
||||
} as CustomAgent;
|
||||
});
|
||||
const selectedSharedAgent = computed(() => {
|
||||
const sourceTenantId = settingsStore.selectedAgentSourceTenantId;
|
||||
if (!sourceTenantId) return undefined;
|
||||
return orgStore.sharedAgents?.find(
|
||||
s => s.agent.id === selectedAgentId.value && String(s.source_tenant_id) === sourceTenantId
|
||||
);
|
||||
});
|
||||
|
||||
// 判断是否为自定义智能体(非内置)
|
||||
const isCustomAgent = computed(() => {
|
||||
@@ -247,7 +254,7 @@ watch([selectedAgentId, agentKnowledgeBases, agentKBSelectionMode], ([newAgentId
|
||||
watch([selectedAgentId, () => settingsStore.selectedAgentSourceTenantId], async ([agentId, sourceTenantId]) => {
|
||||
if (sourceTenantId && agentId) {
|
||||
try {
|
||||
const list = await chatResources.ensureAgentKnowledgeBases(agentId);
|
||||
const list = await chatResources.ensureAgentKnowledgeBases(agentId, sourceTenantId);
|
||||
sharedAgentKbList.value = list.map((kb: any) => ({
|
||||
id: kb.id,
|
||||
name: kb.name,
|
||||
@@ -438,7 +445,11 @@ const showWebSearchButton = computed(() => {
|
||||
if (!hasAgentConfig.value) {
|
||||
return isTenantWebSearchReady(webSearchProviders.value);
|
||||
}
|
||||
return isAgentWebSearchReady(currentAgentConfig.value, webSearchProviders.value);
|
||||
return isAgentWebSearchReady(
|
||||
currentAgentConfig.value,
|
||||
webSearchProviders.value,
|
||||
selectedSharedAgent.value?.web_search_ready,
|
||||
);
|
||||
});
|
||||
const showImageUploadButton = computed(() => isImageUploadEnabledByAgent.value);
|
||||
|
||||
@@ -721,7 +732,7 @@ const loadKnowledgeBases = async (force = false) => {
|
||||
const agentId = settingsStore.selectedAgentId;
|
||||
if (sourceTenantId && agentId) {
|
||||
try {
|
||||
const list = await chatResources.ensureAgentKnowledgeBases(agentId, force);
|
||||
const list = await chatResources.ensureAgentKnowledgeBases(agentId, sourceTenantId, force);
|
||||
list.forEach((kb: any) => kb?.id && sharedAgentKbIdSet.add(kb.id));
|
||||
} catch {
|
||||
sharedAgentKbIdSet = new Set();
|
||||
@@ -766,7 +777,8 @@ const loadFiles = async () => {
|
||||
const runBatch = async (batchIds: string[], kbId?: string, agentId?: string) => {
|
||||
const query = new URLSearchParams();
|
||||
batchIds.forEach((id: string) => query.append('ids', id));
|
||||
const res: any = await batchQueryKnowledge(query.toString(), kbId, agentId);
|
||||
const sourceTenantId = agentId ? settingsStore.selectedAgentSourceTenantId ?? undefined : undefined;
|
||||
const res: any = await batchQueryKnowledge(query.toString(), kbId, agentId, sourceTenantId);
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
res.data.forEach((f: any) => allNewFiles.push({ id: f.id, name: f.title || f.file_name }));
|
||||
}
|
||||
@@ -801,22 +813,29 @@ watch(selectedFileIds, () => {
|
||||
|
||||
const isWebSearchConfigured = computed(() => {
|
||||
if (hasAgentConfig.value) {
|
||||
return isAgentWebSearchReady(currentAgentConfig.value, webSearchProviders.value);
|
||||
return isAgentWebSearchReady(
|
||||
currentAgentConfig.value,
|
||||
webSearchProviders.value,
|
||||
selectedSharedAgent.value?.web_search_ready,
|
||||
);
|
||||
}
|
||||
return isTenantWebSearchReady(webSearchProviders.value);
|
||||
});
|
||||
const isWebSearchReadinessKnown = computed(
|
||||
() => !settingsStore.selectedAgentSourceTenantId || selectedSharedAgent.value !== undefined
|
||||
);
|
||||
|
||||
const loadWebSearchConfig = async (force = false) => {
|
||||
try {
|
||||
await chatResources.ensureWebSearchProviders(force);
|
||||
|
||||
if (!isWebSearchConfigured.value && settingsStore.isWebSearchEnabled) {
|
||||
if (isWebSearchReadinessKnown.value && !isWebSearchConfigured.value && settingsStore.isWebSearchEnabled) {
|
||||
settingsStore.toggleWebSearch(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load web search config:', error);
|
||||
chatResources.invalidate('webSearchProviders');
|
||||
if (settingsStore.isWebSearchEnabled) {
|
||||
if (!settingsStore.selectedAgentSourceTenantId && settingsStore.isWebSearchEnabled) {
|
||||
settingsStore.toggleWebSearch(false);
|
||||
}
|
||||
}
|
||||
@@ -1166,7 +1185,7 @@ const loadMentionItems = async (q: string, resetIndex = true, append = false) =>
|
||||
if (sourceTenantId && agentId) {
|
||||
// 共享智能体:按 agent_id 拉取该智能体配置的知识库范围(后端从共享关系解析空间)
|
||||
try {
|
||||
const list = await chatResources.ensureAgentKnowledgeBases(agentId);
|
||||
const list = await chatResources.ensureAgentKnowledgeBases(agentId, sourceTenantId);
|
||||
const orgLabel = sharedAgentOrgName.value || '';
|
||||
// 保留 capabilities / indexing_strategy,后面过滤时要用
|
||||
availableKbs = list.map((kb: any) => ({
|
||||
@@ -1345,7 +1364,7 @@ const loadMentionItems = async (q: string, resetIndex = true, append = false) =>
|
||||
const sourceTenantId = settingsStore.selectedAgentSourceTenantId;
|
||||
const agentId = selectedAgentId.value;
|
||||
const searchOptions = {
|
||||
...(sourceTenantId && agentId ? { agent_id: agentId } : {}),
|
||||
...(sourceTenantId && agentId ? { agent_id: agentId, agent_source_tenant_id: sourceTenantId } : {}),
|
||||
recent: !fileSearchKeyword,
|
||||
};
|
||||
const res: any = await searchKnowledge(
|
||||
@@ -2480,6 +2499,7 @@ defineExpose({
|
||||
<!-- 附件列表区域 (由 AttachmentUpload 组件渲染) -->
|
||||
<AttachmentUpload ref="attachmentUploadRef" :max-files="5"
|
||||
:session-id="sessionId" :agent-id="selectedAgentId"
|
||||
:agent-source-tenant-id="settingsStore.selectedAgentSourceTenantId ?? undefined"
|
||||
@update:files="uploadedAttachments = $event" />
|
||||
|
||||
<!-- 选中的知识库和文件标签(显示在输入框内顶部) -->
|
||||
|
||||
@@ -308,6 +308,7 @@ const agentIdForDetail = computed(() => {
|
||||
const agentId = settingsStore.selectedAgentId;
|
||||
return sourceTenantId && agentId ? agentId : undefined;
|
||||
});
|
||||
const agentSourceTenantIdForDetail = computed(() => settingsStore.selectedAgentSourceTenantId ?? undefined);
|
||||
|
||||
const kbItems = computed(() => props.items.filter(item => item.type === 'kb'));
|
||||
const fileItems = computed(() => props.items.filter(item => item.type === 'file'));
|
||||
@@ -445,7 +446,10 @@ async function fetchKbDetail(item: { id: string }) {
|
||||
if (detailCache.value[item.id]?.data || detailCache.value[item.id]?.loading) return;
|
||||
detailCache.value = { ...detailCache.value, [item.id]: { loading: true } };
|
||||
try {
|
||||
const opts = agentIdForDetail.value ? { agent_id: agentIdForDetail.value } : undefined;
|
||||
const opts = agentIdForDetail.value ? {
|
||||
agent_id: agentIdForDetail.value,
|
||||
agent_source_tenant_id: agentSourceTenantIdForDetail.value,
|
||||
} : undefined;
|
||||
const res: any = await getKnowledgeBaseById(item.id, opts);
|
||||
detailCache.value = { ...detailCache.value, [item.id]: { loading: false, data: res?.data ?? res } };
|
||||
} catch (e: any) {
|
||||
@@ -457,7 +461,10 @@ async function fetchFileDetail(item: { id: string }) {
|
||||
if (detailCache.value[item.id]?.data || detailCache.value[item.id]?.loading) return;
|
||||
detailCache.value = { ...detailCache.value, [item.id]: { loading: true } };
|
||||
try {
|
||||
const opts = agentIdForDetail.value ? { agent_id: agentIdForDetail.value } : undefined;
|
||||
const opts = agentIdForDetail.value ? {
|
||||
agent_id: agentIdForDetail.value,
|
||||
agent_source_tenant_id: agentSourceTenantIdForDetail.value,
|
||||
} : undefined;
|
||||
const res: any = await getKnowledgeDetails(item.id, opts);
|
||||
detailCache.value = { ...detailCache.value, [item.id]: { loading: false, data: res?.data ?? res } };
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -186,25 +186,29 @@ export const useChatResourcesStore = defineStore('chatResources', () => {
|
||||
])
|
||||
}
|
||||
|
||||
async function ensureAgentKnowledgeBases(agentId: string, force = false): Promise<any[]> {
|
||||
const cached = agentKbCache.get(agentId)
|
||||
async function ensureAgentKnowledgeBases(agentId: string, sourceTenantId?: string, force = false): Promise<any[]> {
|
||||
const cacheKey = `${agentId}:${sourceTenantId || 'current'}`
|
||||
const cached = agentKbCache.get(cacheKey)
|
||||
if (!force && cached && Date.now() - cached.at < CACHE_TTL_MS) {
|
||||
return cached.data
|
||||
}
|
||||
const existing = agentKbInflight.get(agentId)
|
||||
const existing = agentKbInflight.get(cacheKey)
|
||||
if (existing) return existing
|
||||
|
||||
const p = (async () => {
|
||||
try {
|
||||
const res: any = await listKnowledgeBases({ agent_id: agentId })
|
||||
const res: any = await listKnowledgeBases({
|
||||
agent_id: agentId,
|
||||
agent_source_tenant_id: sourceTenantId,
|
||||
})
|
||||
const list = res?.data && Array.isArray(res.data) ? res.data : []
|
||||
agentKbCache.set(agentId, { at: Date.now(), data: list })
|
||||
agentKbCache.set(cacheKey, { at: Date.now(), data: list })
|
||||
return list
|
||||
} finally {
|
||||
agentKbInflight.delete(agentId)
|
||||
agentKbInflight.delete(cacheKey)
|
||||
}
|
||||
})()
|
||||
agentKbInflight.set(agentId, p)
|
||||
agentKbInflight.set(cacheKey, p)
|
||||
return p
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,21 @@ test('isAgentWebSearchReady requires enabled flag and resolvable provider', () =
|
||||
);
|
||||
});
|
||||
|
||||
test('isAgentWebSearchReady trusts source workspace readiness for a shared agent', () => {
|
||||
assert.equal(
|
||||
isAgentWebSearchReady(
|
||||
{ web_search_enabled: true, web_search_provider_id: 'source-provider' },
|
||||
[],
|
||||
true,
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isAgentWebSearchReady({ web_search_enabled: true }, providers, false),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('isTenantWebSearchReady checks default provider only', () => {
|
||||
assert.equal(isTenantWebSearchReady(providers), true);
|
||||
assert.equal(
|
||||
|
||||
@@ -26,8 +26,10 @@ export function isAgentWebSearchEnabled(config: AgentWebSearchConfig | undefined
|
||||
export function isAgentWebSearchReady(
|
||||
config: AgentWebSearchConfig | undefined,
|
||||
providers: WebSearchProviderEntity[],
|
||||
sourceWorkspaceReady?: boolean,
|
||||
): boolean {
|
||||
if (!isAgentWebSearchEnabled(config)) return false;
|
||||
if (sourceWorkspaceReady !== undefined) return sourceWorkspaceReady;
|
||||
return resolveAgentWebSearchProviderId(config, providers) !== null;
|
||||
}
|
||||
|
||||
|
||||
@@ -664,6 +664,9 @@ const sendMsg = async (value, modelId = '', mentionedItems = [], imageFiles = []
|
||||
isReplying.value = true;
|
||||
loading.value = true;
|
||||
const selectedAgentId = props.embeddedMode ? props.agentId : (useSettingsStoreInstance.selectedAgentId || '');
|
||||
const selectedAgentSourceTenantId = props.embeddedMode
|
||||
? undefined
|
||||
: (useSettingsStoreInstance.selectedAgentSourceTenantId || undefined);
|
||||
|
||||
// Images are unified with the attachment pipeline: on the authenticated web
|
||||
// client they upload as temporary documents (understood in the background by
|
||||
@@ -690,7 +693,9 @@ const sendMsg = async (value, modelId = '', mentionedItems = [], imageFiles = []
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const upload = await uploadTemporaryAttachment(session_id.value, file, selectedAgentId, 'auto');
|
||||
const upload = await uploadTemporaryAttachment(
|
||||
session_id.value, file, selectedAgentId, selectedAgentSourceTenantId, 'auto'
|
||||
);
|
||||
imageAttachmentIds.push(upload.data.id);
|
||||
} catch (e) {
|
||||
console.error('[Image] Temporary image upload failed, falling back to inline:', e);
|
||||
@@ -710,7 +715,7 @@ const sendMsg = async (value, modelId = '', mentionedItems = [], imageFiles = []
|
||||
await Promise.all(localAttachments.map(async (attachment) => {
|
||||
attachment.status = 'uploading';
|
||||
const upload = await uploadTemporaryAttachment(
|
||||
session_id.value, attachment.file, selectedAgentId, 'auto'
|
||||
session_id.value, attachment.file, selectedAgentId, selectedAgentSourceTenantId, 'auto'
|
||||
);
|
||||
attachment.documentId = upload.data.id;
|
||||
attachment.status = upload.data.status;
|
||||
@@ -818,6 +823,7 @@ const sendMsg = async (value, modelId = '', mentionedItems = [], imageFiles = []
|
||||
knowledge_ids: knowledgeIds,
|
||||
agent_enabled: agentEnabled,
|
||||
agent_id: selectedAgentId,
|
||||
agent_source_tenant_id: selectedAgentSourceTenantId,
|
||||
web_search_enabled: webSearchEnabled,
|
||||
summary_model_id: modelId,
|
||||
mcp_service_ids: requestMcpServiceIds,
|
||||
|
||||
@@ -209,3 +209,33 @@ func (r *agentShareRepository) GetShareByAgentIDForTenant(ctx context.Context, t
|
||||
}
|
||||
return &share, nil
|
||||
}
|
||||
|
||||
// GetShareByAgentIDAndSourceForTenant validates an exact source selector
|
||||
// against organization membership. This avoids loading every shared agent and
|
||||
// makes same-ID builtins from multiple workspaces deterministic.
|
||||
func (r *agentShareRepository) GetShareByAgentIDAndSourceForTenant(
|
||||
ctx context.Context,
|
||||
tenantID uint64,
|
||||
agentID string,
|
||||
sourceTenantID uint64,
|
||||
) (*types.AgentShare, error) {
|
||||
var share types.AgentShare
|
||||
tx := r.db.WithContext(ctx).
|
||||
Joins("JOIN organization_tenant_members otm ON otm.organization_id = agent_shares.organization_id").
|
||||
Joins("JOIN organizations ON organizations.id = agent_shares.organization_id AND organizations.deleted_at IS NULL").
|
||||
Joins("JOIN custom_agents ON custom_agents.id = agent_shares.agent_id AND custom_agents.tenant_id = agent_shares.source_tenant_id AND custom_agents.deleted_at IS NULL").
|
||||
Where("agent_shares.agent_id = ?", agentID).
|
||||
Where("agent_shares.source_tenant_id = ?", sourceTenantID).
|
||||
Where("otm.tenant_id = ?", tenantID).
|
||||
Where("agent_shares.deleted_at IS NULL").
|
||||
Order("agent_shares.id").
|
||||
Limit(1).
|
||||
Find(&share)
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
if tx.RowsAffected == 0 {
|
||||
return nil, ErrAgentShareNotFound
|
||||
}
|
||||
return &share, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestGetShareByAgentIDAndSourceForTenantDisambiguatesSource(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open("file:agent-share-source?mode=memory&cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
statements := []string{
|
||||
`CREATE TABLE agent_shares (
|
||||
id TEXT PRIMARY KEY, agent_id TEXT, organization_id TEXT,
|
||||
shared_by_user_id TEXT, source_tenant_id INTEGER, permission TEXT,
|
||||
created_at DATETIME, updated_at DATETIME, deleted_at DATETIME
|
||||
)`,
|
||||
`CREATE TABLE organization_tenant_members (organization_id TEXT, tenant_id INTEGER)`,
|
||||
`CREATE TABLE organizations (id TEXT PRIMARY KEY, deleted_at DATETIME)`,
|
||||
`CREATE TABLE custom_agents (id TEXT, tenant_id INTEGER, deleted_at DATETIME)`,
|
||||
`INSERT INTO organizations(id) VALUES ('org')`,
|
||||
`INSERT INTO organization_tenant_members(organization_id, tenant_id) VALUES ('org', 7)`,
|
||||
`INSERT INTO custom_agents(id, tenant_id) VALUES ('builtin-smart-reasoning', 42), ('builtin-smart-reasoning', 84)`,
|
||||
`INSERT INTO agent_shares(id, agent_id, organization_id, source_tenant_id) VALUES
|
||||
('share-42', 'builtin-smart-reasoning', 'org', 42),
|
||||
('share-84', 'builtin-smart-reasoning', 'org', 84)`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
require.NoError(t, db.Exec(statement).Error)
|
||||
}
|
||||
|
||||
repo := &agentShareRepository{db: db}
|
||||
share, err := repo.GetShareByAgentIDAndSourceForTenant(
|
||||
context.Background(), 7, "builtin-smart-reasoning", 84,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "share-84", share.ID)
|
||||
require.Equal(t, uint64(84), share.SourceTenantID)
|
||||
|
||||
_, err = repo.GetShareByAgentIDAndSourceForTenant(
|
||||
context.Background(), 8, "builtin-smart-reasoning", 84,
|
||||
)
|
||||
require.ErrorIs(t, err, ErrAgentShareNotFound)
|
||||
}
|
||||
@@ -438,6 +438,9 @@ func (s *agentService) registerTools(
|
||||
allowedTools = tools.DefaultAllowedTools()
|
||||
logger.Infof(ctx, "Using default allowed tools: %v", allowedTools)
|
||||
}
|
||||
if config.SharedAgentReadOnly {
|
||||
allowedTools = filterSharedAgentWriteTools(allowedTools)
|
||||
}
|
||||
|
||||
// ---- Capability detection from SearchTargets ----
|
||||
var hasVectorKB bool
|
||||
@@ -679,6 +682,27 @@ func (s *agentService) registerTools(
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterSharedAgentWriteTools enforces the read-only contract of AgentShare.
|
||||
// These tools write source-workspace Wiki state and otherwise bypass the HTTP
|
||||
// KB permission middleware because they execute inside the agent engine.
|
||||
func filterSharedAgentWriteTools(allowed []string) []string {
|
||||
sourceWorkspaceWrites := map[string]bool{
|
||||
tools.ToolWikiFlagIssue: true,
|
||||
tools.ToolWikiUpdateIssue: true,
|
||||
tools.ToolWikiWritePage: true,
|
||||
tools.ToolWikiReplaceText: true,
|
||||
tools.ToolWikiRenamePage: true,
|
||||
tools.ToolWikiDeletePage: true,
|
||||
}
|
||||
filtered := make([]string, 0, len(allowed))
|
||||
for _, name := range allowed {
|
||||
if !sourceWorkspaceWrites[name] {
|
||||
filtered = append(filtered, name)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// ValidateConfig validates the agent configuration
|
||||
func (s *agentService) ValidateConfig(config *types.AgentConfig) error {
|
||||
if config == nil {
|
||||
|
||||
@@ -62,11 +62,12 @@ func agentRequiresRerankModel(agent *types.CustomAgent) bool {
|
||||
// tenant. callerTenantRole flows through every read path so the 3-D
|
||||
// cap (tenant Viewer → at most OrgRoleViewer) lands consistently.
|
||||
type agentShareService struct {
|
||||
shareRepo interfaces.AgentShareRepository
|
||||
disabledRepo interfaces.TenantDisabledSharedAgentRepository
|
||||
orgRepo interfaces.OrganizationRepository
|
||||
agentRepo interfaces.CustomAgentRepository
|
||||
userRepo interfaces.UserRepository
|
||||
shareRepo interfaces.AgentShareRepository
|
||||
disabledRepo interfaces.TenantDisabledSharedAgentRepository
|
||||
orgRepo interfaces.OrganizationRepository
|
||||
agentRepo interfaces.CustomAgentRepository
|
||||
userRepo interfaces.UserRepository
|
||||
webSearchProviderRepo interfaces.WebSearchProviderRepository
|
||||
}
|
||||
|
||||
// NewAgentShareService creates a new agent share service
|
||||
@@ -76,16 +77,72 @@ func NewAgentShareService(
|
||||
orgRepo interfaces.OrganizationRepository,
|
||||
agentRepo interfaces.CustomAgentRepository,
|
||||
userRepo interfaces.UserRepository,
|
||||
webSearchProviderRepo interfaces.WebSearchProviderRepository,
|
||||
) interfaces.AgentShareService {
|
||||
return &agentShareService{
|
||||
shareRepo: shareRepo,
|
||||
disabledRepo: disabledRepo,
|
||||
orgRepo: orgRepo,
|
||||
agentRepo: agentRepo,
|
||||
userRepo: userRepo,
|
||||
shareRepo: shareRepo,
|
||||
disabledRepo: disabledRepo,
|
||||
orgRepo: orgRepo,
|
||||
agentRepo: agentRepo,
|
||||
userRepo: userRepo,
|
||||
webSearchProviderRepo: webSearchProviderRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *agentShareService) sharedAgentInfo(
|
||||
ctx context.Context,
|
||||
share *types.AgentShare,
|
||||
effective types.OrgMemberRole,
|
||||
webSearchReadyCache map[string]bool,
|
||||
) *types.SharedAgentInfo {
|
||||
info := &types.SharedAgentInfo{
|
||||
Agent: share.Agent,
|
||||
ShareID: share.ID,
|
||||
OrganizationID: share.OrganizationID,
|
||||
Permission: effective,
|
||||
SourceTenantID: share.SourceTenantID,
|
||||
SharedAt: share.CreatedAt,
|
||||
SharedByUserID: share.SharedByUserID,
|
||||
}
|
||||
if share.Organization != nil {
|
||||
info.OrgName = share.Organization.Name
|
||||
}
|
||||
if share.SharedByUserID != "" && s.userRepo != nil {
|
||||
if u, err := s.userRepo.GetUserByID(ctx, share.SharedByUserID); err == nil && u != nil {
|
||||
info.SharedByUsername = u.Username
|
||||
}
|
||||
}
|
||||
cacheKey := fmt.Sprintf("%d:%t:%s", share.SourceTenantID, share.Agent.Config.WebSearchEnabled, share.Agent.Config.WebSearchProviderID)
|
||||
if ready, ok := webSearchReadyCache[cacheKey]; ok {
|
||||
info.WebSearchReady = ready
|
||||
} else {
|
||||
info.WebSearchReady = s.isAgentWebSearchReady(ctx, share.Agent, share.SourceTenantID)
|
||||
webSearchReadyCache[cacheKey] = info.WebSearchReady
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
// isAgentWebSearchReady resolves only an availability bit in the source
|
||||
// workspace. Returning source provider rows would leak configuration, while
|
||||
// comparing provider IDs with the receiver workspace produces false negatives.
|
||||
func (s *agentShareService) isAgentWebSearchReady(
|
||||
ctx context.Context,
|
||||
agent *types.CustomAgent,
|
||||
sourceTenantID uint64,
|
||||
) bool {
|
||||
if agent == nil || !agent.Config.WebSearchEnabled || s.webSearchProviderRepo == nil {
|
||||
return false
|
||||
}
|
||||
var provider *types.WebSearchProviderEntity
|
||||
var err error
|
||||
if agent.Config.WebSearchProviderID != "" {
|
||||
provider, err = s.webSearchProviderRepo.GetByID(ctx, sourceTenantID, agent.Config.WebSearchProviderID)
|
||||
} else {
|
||||
provider, err = s.webSearchProviderRepo.GetDefault(ctx, sourceTenantID)
|
||||
}
|
||||
return err == nil && provider != nil
|
||||
}
|
||||
|
||||
// ShareAgent shares an agent to an organization. Permission is forced to
|
||||
// OrgRoleViewer (cross-tenant agent edit is not part of v1).
|
||||
func (s *agentShareService) ShareAgent(ctx context.Context, agentID string, orgID string, userID string, tenantID uint64, permission types.OrgMemberRole) (*types.AgentShare, error) {
|
||||
@@ -221,6 +278,7 @@ func (s *agentShareService) ListSharedAgents(ctx context.Context, tenantID uint6
|
||||
}
|
||||
|
||||
agentInfoMap := make(map[string]*types.SharedAgentInfo)
|
||||
webSearchReadyCache := make(map[string]bool)
|
||||
for _, share := range shares {
|
||||
if share.SourceTenantID == tenantID {
|
||||
continue
|
||||
@@ -234,24 +292,7 @@ func (s *agentShareService) ListSharedAgents(ctx context.Context, tenantID uint6
|
||||
}
|
||||
effective := types.MinOrgRole(share.Permission, tm.Role)
|
||||
effective = applyTenantRoleCap(effective, callerTenantRole)
|
||||
info := &types.SharedAgentInfo{
|
||||
Agent: share.Agent,
|
||||
ShareID: share.ID,
|
||||
OrganizationID: share.OrganizationID,
|
||||
OrgName: "",
|
||||
Permission: effective,
|
||||
SourceTenantID: share.SourceTenantID,
|
||||
SharedAt: share.CreatedAt,
|
||||
SharedByUserID: share.SharedByUserID,
|
||||
}
|
||||
if share.Organization != nil {
|
||||
info.OrgName = share.Organization.Name
|
||||
}
|
||||
if share.SharedByUserID != "" {
|
||||
if u, err := s.userRepo.GetUserByID(ctx, share.SharedByUserID); err == nil && u != nil {
|
||||
info.SharedByUsername = u.Username
|
||||
}
|
||||
}
|
||||
info := s.sharedAgentInfo(ctx, share, effective, webSearchReadyCache)
|
||||
key := fmt.Sprintf("%s_%d", share.AgentID, share.SourceTenantID)
|
||||
existing, exists := agentInfoMap[key]
|
||||
if !exists {
|
||||
@@ -302,6 +343,7 @@ func (s *agentShareService) ListSharedAgentsInOrganization(ctx context.Context,
|
||||
}
|
||||
|
||||
result := make([]*types.OrganizationSharedAgentItem, 0, len(shares))
|
||||
webSearchReadyCache := make(map[string]bool)
|
||||
for _, share := range shares {
|
||||
if share.Agent == nil {
|
||||
continue
|
||||
@@ -310,25 +352,7 @@ func (s *agentShareService) ListSharedAgentsInOrganization(ctx context.Context,
|
||||
effective := types.MinOrgRole(share.Permission, tm.Role)
|
||||
effective = applyTenantRoleCap(effective, callerTenantRole)
|
||||
|
||||
orgName := ""
|
||||
if share.Organization != nil {
|
||||
orgName = share.Organization.Name
|
||||
}
|
||||
info := &types.SharedAgentInfo{
|
||||
Agent: share.Agent,
|
||||
ShareID: share.ID,
|
||||
OrganizationID: share.OrganizationID,
|
||||
OrgName: orgName,
|
||||
Permission: effective,
|
||||
SourceTenantID: share.SourceTenantID,
|
||||
SharedAt: share.CreatedAt,
|
||||
SharedByUserID: share.SharedByUserID,
|
||||
}
|
||||
if share.SharedByUserID != "" {
|
||||
if u, err := s.userRepo.GetUserByID(ctx, share.SharedByUserID); err == nil && u != nil {
|
||||
info.SharedByUsername = u.Username
|
||||
}
|
||||
}
|
||||
info := s.sharedAgentInfo(ctx, share, effective, webSearchReadyCache)
|
||||
|
||||
item := &types.OrganizationSharedAgentItem{
|
||||
SharedAgentInfo: *info,
|
||||
@@ -380,6 +404,7 @@ func (s *agentShareService) ListSharedAgentsInOrganizations(ctx context.Context,
|
||||
disabledSet[fmt.Sprintf("%s_%d", d.AgentID, d.SourceTenantID)] = true
|
||||
}
|
||||
}
|
||||
webSearchReadyCache := make(map[string]bool)
|
||||
for orgID, list := range byOrg {
|
||||
tm := members[orgID]
|
||||
result := make([]*types.OrganizationSharedAgentItem, 0, len(list))
|
||||
@@ -389,25 +414,7 @@ func (s *agentShareService) ListSharedAgentsInOrganizations(ctx context.Context,
|
||||
}
|
||||
effective := types.MinOrgRole(share.Permission, tm.Role)
|
||||
effective = applyTenantRoleCap(effective, callerTenantRole)
|
||||
orgName := ""
|
||||
if share.Organization != nil {
|
||||
orgName = share.Organization.Name
|
||||
}
|
||||
info := &types.SharedAgentInfo{
|
||||
Agent: share.Agent,
|
||||
ShareID: share.ID,
|
||||
OrganizationID: share.OrganizationID,
|
||||
OrgName: orgName,
|
||||
Permission: effective,
|
||||
SourceTenantID: share.SourceTenantID,
|
||||
SharedAt: share.CreatedAt,
|
||||
SharedByUserID: share.SharedByUserID,
|
||||
}
|
||||
if share.SharedByUserID != "" {
|
||||
if u, err := s.userRepo.GetUserByID(ctx, share.SharedByUserID); err == nil && u != nil {
|
||||
info.SharedByUsername = u.Username
|
||||
}
|
||||
}
|
||||
info := s.sharedAgentInfo(ctx, share, effective, webSearchReadyCache)
|
||||
item := &types.OrganizationSharedAgentItem{
|
||||
SharedAgentInfo: *info,
|
||||
IsMine: share.SourceTenantID == tenantID,
|
||||
@@ -442,10 +449,34 @@ func (s *agentShareService) SetSharedAgentDisabledByMe(ctx context.Context, tena
|
||||
// callerTenantRole is currently only used for symmetry / future caps on
|
||||
// agent execution (e.g. tenant Viewers might be banned from
|
||||
// state-changing tool calls in a follow-up).
|
||||
func (s *agentShareService) GetSharedAgentForTenant(ctx context.Context, tenantID uint64, callerTenantRole types.TenantRole, agentID string) (*types.CustomAgent, error) {
|
||||
func (s *agentShareService) GetSharedAgentForTenant(
|
||||
ctx context.Context,
|
||||
tenantID uint64,
|
||||
callerTenantRole types.TenantRole,
|
||||
agentID string,
|
||||
sourceTenantID ...uint64,
|
||||
) (*types.CustomAgent, error) {
|
||||
if agentID == "" {
|
||||
return nil, ErrAgentShareNotFound
|
||||
}
|
||||
if len(sourceTenantID) > 0 && sourceTenantID[0] != 0 {
|
||||
if sourceTenantID[0] == tenantID {
|
||||
return nil, ErrAgentShareNotFound
|
||||
}
|
||||
share, err := s.shareRepo.GetShareByAgentIDAndSourceForTenant(ctx, tenantID, agentID, sourceTenantID[0])
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrAgentShareNotFound) {
|
||||
return nil, ErrAgentSharePermission
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
agent, err := s.agentRepo.GetAgentByID(ctx, agentID, share.SourceTenantID)
|
||||
if err != nil || agent == nil {
|
||||
return nil, ErrAgentNotFoundForShare
|
||||
}
|
||||
_ = callerTenantRole
|
||||
return agent, nil
|
||||
}
|
||||
share, err := s.shareRepo.GetShareByAgentIDForTenant(ctx, tenantID, agentID, tenantID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrAgentShareNotFound) {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/agent/tools"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type sharedAgentWebSearchRepo struct {
|
||||
byIDTenant uint64
|
||||
byID string
|
||||
defaultTenant uint64
|
||||
explicit *types.WebSearchProviderEntity
|
||||
defaultProvider *types.WebSearchProviderEntity
|
||||
}
|
||||
|
||||
func (r *sharedAgentWebSearchRepo) Create(context.Context, *types.WebSearchProviderEntity) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *sharedAgentWebSearchRepo) GetByID(_ context.Context, tenantID uint64, id string) (*types.WebSearchProviderEntity, error) {
|
||||
r.byIDTenant = tenantID
|
||||
r.byID = id
|
||||
return r.explicit, nil
|
||||
}
|
||||
|
||||
func (r *sharedAgentWebSearchRepo) GetDefault(_ context.Context, tenantID uint64) (*types.WebSearchProviderEntity, error) {
|
||||
r.defaultTenant = tenantID
|
||||
return r.defaultProvider, nil
|
||||
}
|
||||
|
||||
func (r *sharedAgentWebSearchRepo) List(context.Context, uint64) ([]*types.WebSearchProviderEntity, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *sharedAgentWebSearchRepo) Update(context.Context, *types.WebSearchProviderEntity) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *sharedAgentWebSearchRepo) Delete(context.Context, uint64, string) error { return nil }
|
||||
|
||||
func (r *sharedAgentWebSearchRepo) ClearDefault(context.Context, uint64, string) error { return nil }
|
||||
|
||||
func TestSharedAgentWebSearchReadyUsesSourceWorkspace(t *testing.T) {
|
||||
repo := &sharedAgentWebSearchRepo{
|
||||
explicit: &types.WebSearchProviderEntity{ID: "source-provider", TenantID: 42},
|
||||
}
|
||||
svc := &agentShareService{webSearchProviderRepo: repo}
|
||||
agent := &types.CustomAgent{Config: types.CustomAgentConfig{
|
||||
WebSearchEnabled: true,
|
||||
WebSearchProviderID: "source-provider",
|
||||
}}
|
||||
|
||||
require.True(t, svc.isAgentWebSearchReady(context.Background(), agent, 42))
|
||||
require.Equal(t, uint64(42), repo.byIDTenant)
|
||||
require.Equal(t, "source-provider", repo.byID)
|
||||
}
|
||||
|
||||
func TestSharedAgentWebSearchReadyUsesSourceDefault(t *testing.T) {
|
||||
repo := &sharedAgentWebSearchRepo{
|
||||
defaultProvider: &types.WebSearchProviderEntity{ID: "source-default", TenantID: 42, IsDefault: true},
|
||||
}
|
||||
svc := &agentShareService{webSearchProviderRepo: repo}
|
||||
agent := &types.CustomAgent{Config: types.CustomAgentConfig{WebSearchEnabled: true}}
|
||||
|
||||
require.True(t, svc.isAgentWebSearchReady(context.Background(), agent, 42))
|
||||
require.Equal(t, uint64(42), repo.defaultTenant)
|
||||
}
|
||||
|
||||
func TestFilterSharedAgentWriteTools(t *testing.T) {
|
||||
got := filterSharedAgentWriteTools([]string{
|
||||
tools.ToolWikiReadPage,
|
||||
tools.ToolWikiFlagIssue,
|
||||
tools.ToolWikiWritePage,
|
||||
tools.ToolWikiReplaceText,
|
||||
tools.ToolWikiRenamePage,
|
||||
tools.ToolWikiDeletePage,
|
||||
tools.ToolWikiReadIssue,
|
||||
tools.ToolWikiUpdateIssue,
|
||||
tools.ToolWebSearch,
|
||||
})
|
||||
|
||||
require.Equal(t, []string{
|
||||
tools.ToolWikiReadPage,
|
||||
tools.ToolWikiReadIssue,
|
||||
tools.ToolWebSearch,
|
||||
}, got)
|
||||
}
|
||||
@@ -233,6 +233,7 @@ func (s *sessionService) buildAgentConfig(
|
||||
RetrieveKBOnlyWhenMentioned: customAgent.Config.RetrieveKBOnlyWhenMentioned,
|
||||
LLMCallTimeout: customAgent.Config.LLMCallTimeout,
|
||||
RetainRetrievalHistory: customAgent.Config.RetainRetrievalHistory,
|
||||
SharedAgentReadOnly: req.SharedAgentReadOnly,
|
||||
}
|
||||
|
||||
// Falls back to global configuration if no specific timeout is set for the agent.
|
||||
@@ -260,7 +261,7 @@ func (s *sessionService) buildAgentConfig(
|
||||
// Apply per-turn @Skill / @MCP scope. Each helper narrows the agent's
|
||||
// whitelist to the mentioned items and records the pinned set used for the
|
||||
// <must_use> hint, keeping all scope logic in one place per resource type.
|
||||
isSharedAgent := req.Session != nil && req.Session.TenantID != customAgent.TenantID
|
||||
isSharedAgent := req.SharedAgentReadOnly
|
||||
applyPerRequestSkillScope(ctx, agentConfig, customAgent.Config.SkillsSelectionMode, req.SkillNames)
|
||||
applyPerRequestMCPScope(ctx, agentConfig, customAgent.Config.MCPServices, isSharedAgent, req.MCPServiceIDs)
|
||||
|
||||
|
||||
@@ -150,7 +150,11 @@ func (s *temporaryDocumentService) Create(
|
||||
return nil, fmt.Errorf("unsafe file name: %w", err)
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(baseName))
|
||||
if !s.supportsExtension(ctx, tenantID, ext) {
|
||||
resourceTenantID := options.ResourceTenantID
|
||||
if resourceTenantID == 0 {
|
||||
resourceTenantID = tenantID
|
||||
}
|
||||
if !s.supportsExtension(ctx, resourceTenantID, ext) {
|
||||
return nil, fmt.Errorf("unsupported file type: %s", ext)
|
||||
}
|
||||
maxSize := secutils.GetMaxFileSizeMB() * 1024 * 1024
|
||||
@@ -288,9 +292,15 @@ func (s *temporaryDocumentService) Process(ctx context.Context, task *asynq.Task
|
||||
if err := s.repo.MarkProcessing(ctx, payload.TenantID, payload.DocumentID, startedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
// Ensure model resolution (VLM/ASR) has a tenant ID in context.
|
||||
ctx = context.WithValue(ctx, types.TenantIDContextKey, payload.TenantID)
|
||||
if tenant, tenantErr := s.tenantService.GetTenantByID(ctx, payload.TenantID); tenantErr == nil && tenant != nil {
|
||||
// The attachment row and file remain scoped to payload.TenantID, while
|
||||
// parser/model dependencies may belong to a verified shared-agent source.
|
||||
resourceTenantID := payload.TenantID
|
||||
var options types.TemporaryDocumentCreateOptions
|
||||
if json.Unmarshal(document.ProcessingOptions, &options) == nil && options.ResourceTenantID != 0 {
|
||||
resourceTenantID = options.ResourceTenantID
|
||||
}
|
||||
ctx = context.WithValue(ctx, types.TenantIDContextKey, resourceTenantID)
|
||||
if tenant, tenantErr := s.tenantService.GetTenantByID(ctx, resourceTenantID); tenantErr == nil && tenant != nil {
|
||||
ctx = context.WithValue(ctx, types.TenantInfoContextKey, tenant)
|
||||
}
|
||||
content, images, metadata, parseErr := s.parse(ctx, document)
|
||||
|
||||
@@ -188,7 +188,8 @@ func (h *KnowledgeHandler) resolveKnowledgeAndValidateKBAccess(c *gin.Context, k
|
||||
if h.agentShareService != nil && requiredPermission == types.OrgRoleViewer {
|
||||
agentID := c.Query("agent_id")
|
||||
if agentID != "" {
|
||||
agent, err := h.agentShareService.GetSharedAgentForTenant(ctx, tenantID, callerTenantRole, agentID)
|
||||
sourceTenantID, _ := strconv.ParseUint(c.Query("agent_source_tenant_id"), 10, 64)
|
||||
agent, err := h.agentShareService.GetSharedAgentForTenant(ctx, tenantID, callerTenantRole, agentID, sourceTenantID)
|
||||
if err == nil && agent != nil {
|
||||
if knowledge.TenantID != agent.TenantID {
|
||||
return nil, ctx, errors.NewForbiddenError("Permission denied to access this knowledge")
|
||||
@@ -1384,9 +1385,10 @@ func (h *KnowledgeHandler) PreviewKnowledgeFile(c *gin.Context) {
|
||||
|
||||
// GetKnowledgeBatchRequest defines parameters for batch knowledge retrieval
|
||||
type GetKnowledgeBatchRequest struct {
|
||||
IDs []string `form:"ids" binding:"required"` // List of knowledge IDs
|
||||
KBID string `form:"kb_id"` // Optional: scope to this KB (validates access and uses effective tenant for shared KB)
|
||||
AgentID string `form:"agent_id"` // Optional: when using a shared agent, use agent's tenant for retrieval (validates shared agent access)
|
||||
IDs []string `form:"ids" binding:"required"` // List of knowledge IDs
|
||||
KBID string `form:"kb_id"` // Optional: scope to this KB (validates access and uses effective tenant for shared KB)
|
||||
AgentID string `form:"agent_id"` // Optional: when using a shared agent, use agent's tenant for retrieval (validates shared agent access)
|
||||
AgentSourceTenantID uint64 `form:"agent_source_tenant_id"` // Optional source selector, verified against the share relation
|
||||
}
|
||||
|
||||
// GetKnowledgeBatch godoc
|
||||
@@ -1439,7 +1441,7 @@ func (h *KnowledgeHandler) GetKnowledgeBatch(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
callerTenantRole := types.TenantRoleFromContext(ctx)
|
||||
agent, err := h.agentShareService.GetSharedAgentForTenant(ctx, currentTenantID, callerTenantRole, agentID)
|
||||
agent, err := h.agentShareService.GetSharedAgentForTenant(ctx, currentTenantID, callerTenantRole, agentID, req.AgentSourceTenantID)
|
||||
if err != nil || agent == nil {
|
||||
logger.Warnf(ctx, "GetKnowledgeBatch: invalid or inaccessible shared agent %s: %v", agentID, err)
|
||||
c.Error(errors.NewForbiddenError("Invalid or inaccessible shared agent").WithDetails(err.Error()))
|
||||
@@ -1992,7 +1994,8 @@ func (h *KnowledgeHandler) SearchKnowledge(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
callerTenantRole := types.TenantRoleFromContext(ctx)
|
||||
agent, err := h.agentShareService.GetSharedAgentForTenant(ctx, currentTenantID, callerTenantRole, agentID)
|
||||
requestedSourceTenantID, _ := strconv.ParseUint(c.Query("agent_source_tenant_id"), 10, 64)
|
||||
agent, err := h.agentShareService.GetSharedAgentForTenant(ctx, currentTenantID, callerTenantRole, agentID, requestedSourceTenantID)
|
||||
if err != nil {
|
||||
if goerrors.Is(err, service.ErrAgentShareNotFound) || goerrors.Is(err, service.ErrAgentSharePermission) || goerrors.Is(err, service.ErrAgentNotFoundForShare) {
|
||||
c.Error(errors.NewForbiddenError("no permission for this shared agent"))
|
||||
|
||||
@@ -474,7 +474,8 @@ func (h *KnowledgeBaseHandler) validateAndGetKnowledgeBase(c *gin.Context) (*typ
|
||||
currentTenantID := tenantID.(uint64)
|
||||
agentID := c.Query("agent_id")
|
||||
if agentID != "" {
|
||||
agent, err := h.agentShareService.GetSharedAgentForTenant(ctx, currentTenantID, callerTenantRole, agentID)
|
||||
sourceTenantID, _ := strconv.ParseUint(c.Query("agent_source_tenant_id"), 10, 64)
|
||||
agent, err := h.agentShareService.GetSharedAgentForTenant(ctx, currentTenantID, callerTenantRole, agentID, sourceTenantID)
|
||||
if err == nil && agent != nil {
|
||||
if kb.TenantID != agent.TenantID {
|
||||
logger.Warnf(ctx, "Shared agent workspace mismatch, KB %s tenant: %d, agent tenant: %d", id, kb.TenantID, agent.TenantID)
|
||||
@@ -581,7 +582,8 @@ func (h *KnowledgeBaseHandler) ListKnowledgeBases(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
callerTenantRole := types.TenantRoleFromContext(ctx)
|
||||
agent, err := h.agentShareService.GetSharedAgentForTenant(ctx, currentTenantID, callerTenantRole, agentID)
|
||||
requestedSourceTenantID, _ := strconv.ParseUint(c.Query("agent_source_tenant_id"), 10, 64)
|
||||
agent, err := h.agentShareService.GetSharedAgentForTenant(ctx, currentTenantID, callerTenantRole, agentID, requestedSourceTenantID)
|
||||
if err != nil {
|
||||
if stderrors.Is(err, service.ErrAgentShareNotFound) || stderrors.Is(err, service.ErrAgentSharePermission) || stderrors.Is(err, service.ErrAgentNotFoundForShare) {
|
||||
c.Error(apperrors.NewForbiddenError("no permission for this shared agent"))
|
||||
|
||||
@@ -43,6 +43,7 @@ type qaRequestContext struct {
|
||||
webSearchEnabled bool
|
||||
mentionedItems types.MentionedItems
|
||||
effectiveTenantID uint64 // when using shared agent, tenant ID for model/KB/MCP resolution; 0 = use context tenant
|
||||
sharedAgentReadOnly bool // access was granted by a read-only agent share
|
||||
images []ImageAttachment // Uploaded images with analysis text
|
||||
userMessageID string // Created user message ID (populated after createUserMessage)
|
||||
channel string // Source channel: "web", "api", "im", etc.
|
||||
@@ -62,21 +63,22 @@ type qaRequestContext struct {
|
||||
func (rc *qaRequestContext) buildQARequest() *types.QARequest {
|
||||
imageURLs, imageDescription := extractImageURLsAndOCRText(rc.images)
|
||||
return &types.QARequest{
|
||||
Session: rc.session,
|
||||
Query: rc.query,
|
||||
AssistantMessageID: rc.assistantMessage.ID,
|
||||
SummaryModelID: rc.summaryModelID,
|
||||
CustomAgent: rc.customAgent,
|
||||
KnowledgeBaseIDs: rc.knowledgeBaseIDs,
|
||||
KnowledgeIDs: rc.knowledgeIDs,
|
||||
TagScopes: rc.tagScopes,
|
||||
MCPServiceIDs: rc.mcpServiceIDs,
|
||||
SkillNames: rc.skillNames,
|
||||
ImageURLs: imageURLs,
|
||||
ImageDescription: imageDescription,
|
||||
UserMessageID: rc.userMessageID,
|
||||
WebSearchEnabled: rc.webSearchEnabled,
|
||||
Attachments: rc.attachments,
|
||||
Session: rc.session,
|
||||
Query: rc.query,
|
||||
AssistantMessageID: rc.assistantMessage.ID,
|
||||
SummaryModelID: rc.summaryModelID,
|
||||
CustomAgent: rc.customAgent,
|
||||
SharedAgentReadOnly: rc.sharedAgentReadOnly,
|
||||
KnowledgeBaseIDs: rc.knowledgeBaseIDs,
|
||||
KnowledgeIDs: rc.knowledgeIDs,
|
||||
TagScopes: rc.tagScopes,
|
||||
MCPServiceIDs: rc.mcpServiceIDs,
|
||||
SkillNames: rc.skillNames,
|
||||
ImageURLs: imageURLs,
|
||||
ImageDescription: imageDescription,
|
||||
UserMessageID: rc.userMessageID,
|
||||
WebSearchEnabled: rc.webSearchEnabled,
|
||||
Attachments: rc.attachments,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +140,10 @@ func (h *Handler) parseQARequest(c *gin.Context, logPrefix string) (*qaRequestCo
|
||||
}
|
||||
|
||||
// Get custom agent if agent_id is provided. Backend resolves shared agent from share relation (no client-provided tenant).
|
||||
customAgent, effectiveTenantID := h.resolveAgent(ctx, c, request.AgentID)
|
||||
customAgent, effectiveTenantID, sharedAgentReadOnly := h.resolveAgent(ctx, c, request.AgentID, request.AgentSourceTenantID)
|
||||
if request.AgentSourceTenantID != 0 && customAgent == nil {
|
||||
return nil, nil, errors.NewNotFoundError("Shared agent not found")
|
||||
}
|
||||
|
||||
// Merge @mentioned items into knowledge_base_ids and knowledge_ids
|
||||
kbIDs, knowledgeIDs := mergeKnowledgeTargets(request.KnowledgeBaseIDs, request.KnowledgeIds, request.MentionedItems)
|
||||
@@ -160,6 +165,7 @@ func (h *Handler) parseQARequest(c *gin.Context, logPrefix string) (*qaRequestCo
|
||||
); scopedTenantID != 0 {
|
||||
customAgent = scopedAgent
|
||||
effectiveTenantID = scopedTenantID
|
||||
sharedAgentReadOnly = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +211,10 @@ func (h *Handler) parseQARequest(c *gin.Context, logPrefix string) (*qaRequestCo
|
||||
}
|
||||
|
||||
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
||||
attachmentRuntimeCtx := ctx
|
||||
if effectiveTenantID != 0 {
|
||||
attachmentRuntimeCtx = context.WithValue(ctx, types.TenantIDContextKey, effectiveTenantID)
|
||||
}
|
||||
|
||||
// Use ASR only when the agent has audio upload enabled.
|
||||
asrModelID := ""
|
||||
@@ -229,7 +239,7 @@ func (h *Handler) parseQARequest(c *gin.Context, logPrefix string) (*qaRequestCo
|
||||
}
|
||||
|
||||
processed, err := h.attachmentProcessor.ProcessAttachment(
|
||||
ctx, data, att.FileName, att.FileSize, tenantID, asrModelID,
|
||||
attachmentRuntimeCtx, data, att.FileName, att.FileSize, tenantID, asrModelID,
|
||||
)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("attachment %d processing failed: %w", idx+1, err)
|
||||
@@ -341,6 +351,7 @@ func (h *Handler) parseQARequest(c *gin.Context, logPrefix string) (*qaRequestCo
|
||||
webSearchEnabled: request.WebSearchEnabled,
|
||||
mentionedItems: convertMentionedItems(request.MentionedItems),
|
||||
effectiveTenantID: effectiveTenantID,
|
||||
sharedAgentReadOnly: sharedAgentReadOnly,
|
||||
images: request.Images,
|
||||
channel: request.Channel,
|
||||
attachments: processedAttachments,
|
||||
@@ -447,9 +458,14 @@ func cloneTagScopes(scopes []types.TagScope) []types.TagScope {
|
||||
|
||||
// resolveAgent resolves the custom agent by ID, trying shared agent first, then own agent.
|
||||
// Returns (nil, 0) if agentID is empty or not found.
|
||||
func (h *Handler) resolveAgent(ctx context.Context, c *gin.Context, agentID string) (*types.CustomAgent, uint64) {
|
||||
func (h *Handler) resolveAgent(
|
||||
ctx context.Context,
|
||||
c *gin.Context,
|
||||
agentID string,
|
||||
sourceTenantID uint64,
|
||||
) (*types.CustomAgent, uint64, bool) {
|
||||
if agentID == "" {
|
||||
return nil, 0
|
||||
return nil, 0, false
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Resolving agent, agent ID: %s", secutils.SanitizeForLog(agentID))
|
||||
@@ -457,21 +473,26 @@ func (h *Handler) resolveAgent(ctx context.Context, c *gin.Context, agentID stri
|
||||
// Try shared agent first
|
||||
var customAgent *types.CustomAgent
|
||||
var effectiveTenantID uint64
|
||||
var sharedAgentReadOnly bool
|
||||
userIDVal, _ := c.Get(types.UserIDContextKey.String())
|
||||
currentTenantID := c.GetUint64(types.TenantIDContextKey.String())
|
||||
if h.agentShareService != nil && userIDVal != nil && currentTenantID != 0 {
|
||||
callerTenantRole := types.TenantRoleFromContext(ctx)
|
||||
agent, err := h.agentShareService.GetSharedAgentForTenant(ctx, currentTenantID, callerTenantRole, agentID)
|
||||
var agent *types.CustomAgent
|
||||
var err error
|
||||
agent, err = h.agentShareService.GetSharedAgentForTenant(ctx, currentTenantID, callerTenantRole, agentID, sourceTenantID)
|
||||
if err == nil && agent != nil {
|
||||
effectiveTenantID = agent.TenantID
|
||||
customAgent = agent
|
||||
sharedAgentReadOnly = true
|
||||
logger.Infof(ctx, "Using shared agent: ID=%s, Name=%s, effectiveTenantID=%d (retrieval scope)",
|
||||
customAgent.ID, customAgent.Name, effectiveTenantID)
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to own agent
|
||||
if customAgent == nil {
|
||||
// Fall back to an own agent only when no source workspace was requested.
|
||||
// A rejected shared selector must not silently run a same-ID local builtin.
|
||||
if customAgent == nil && sourceTenantID == 0 {
|
||||
agent, err := h.customAgentService.GetAgentByID(ctx, agentID)
|
||||
if err == nil {
|
||||
customAgent = agent
|
||||
@@ -486,7 +507,7 @@ func (h *Handler) resolveAgent(ctx context.Context, c *gin.Context, agentID stri
|
||||
customAgent.ID, customAgent.Name, customAgent.IsBuiltin, customAgent.Config.AgentMode, effectiveTenantID)
|
||||
}
|
||||
|
||||
return customAgent, effectiveTenantID
|
||||
return customAgent, effectiveTenantID, sharedAgentReadOnly
|
||||
}
|
||||
|
||||
// mergeKnowledgeTargets merges request KB/knowledge IDs with @mentioned items into deduplicated slices.
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
apperrors "github.com/Tencent/WeKnora/internal/errors"
|
||||
@@ -40,10 +41,16 @@ func (h *Handler) UploadTemporaryDocument(c *gin.Context) {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
agent, _ := h.resolveAgent(ctx, c, c.PostForm("agent_id"))
|
||||
sourceTenantID, _ := strconv.ParseUint(strings.TrimSpace(c.PostForm("agent_source_tenant_id")), 10, 64)
|
||||
agent, resourceTenantID, _ := h.resolveAgent(ctx, c, c.PostForm("agent_id"), sourceTenantID)
|
||||
if sourceTenantID != 0 && agent == nil {
|
||||
c.Error(apperrors.NewNotFoundError("Shared agent not found"))
|
||||
return
|
||||
}
|
||||
ext := strings.TrimPrefix(strings.ToLower(filepath.Ext(fileHeader.Filename)), ".")
|
||||
options := types.TemporaryDocumentCreateOptions{ParserEngine: strings.TrimSpace(c.PostForm("parser_engine"))}
|
||||
if agent != nil {
|
||||
options.ResourceTenantID = resourceTenantID
|
||||
if len(agent.Config.SupportedFileTypes) > 0 && !containsFileType(agent.Config.SupportedFileTypes, ext) {
|
||||
c.Error(apperrors.NewBadRequestError("file type is not supported by this agent"))
|
||||
return
|
||||
|
||||
@@ -47,6 +47,7 @@ type CreateKnowledgeQARequest struct {
|
||||
KnowledgeIds []string `json:"knowledge_ids"` // Selected knowledge ID for this request
|
||||
AgentEnabled bool `json:"agent_enabled"` // Whether agent mode is enabled for this request
|
||||
AgentID string `json:"agent_id"` // Selected custom agent ID (backend resolves shared agent and its workspace from share relation)
|
||||
AgentSourceTenantID uint64 `json:"agent_source_tenant_id,omitempty"` // Optional disambiguator; backend still verifies the share relation
|
||||
WebSearchEnabled bool `json:"web_search_enabled"` // Whether web search is enabled for this request
|
||||
SummaryModelID string `json:"summary_model_id"` // Optional summary model ID for this request (overrides session default)
|
||||
MCPServiceIDs []string `json:"mcp_service_ids"` // Per-request MCP services selected via @mention
|
||||
|
||||
@@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"strconv"
|
||||
|
||||
apprepo "github.com/Tencent/WeKnora/internal/application/repository"
|
||||
"github.com/Tencent/WeKnora/internal/config"
|
||||
@@ -399,7 +400,8 @@ func resolveSharedAgentAccess(
|
||||
) *KBAccess {
|
||||
agentID := c.Query("agent_id")
|
||||
if agentID != "" {
|
||||
agent, err := agentShareService.GetSharedAgentForTenant(ctx, tenantID, callerTenantRole, agentID)
|
||||
sourceTenantID, _ := strconv.ParseUint(c.Query("agent_source_tenant_id"), 10, 64)
|
||||
agent, err := agentShareService.GetSharedAgentForTenant(ctx, tenantID, callerTenantRole, agentID, sourceTenantID)
|
||||
if err != nil || agent == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ type stubAgentShareForGuard struct {
|
||||
kbsViaSomeAgent map[string]bool
|
||||
}
|
||||
|
||||
func (s *stubAgentShareForGuard) GetSharedAgentForTenant(_ context.Context, _ uint64, _ types.TenantRole, agentID string) (*types.CustomAgent, error) {
|
||||
func (s *stubAgentShareForGuard) GetSharedAgentForTenant(_ context.Context, _ uint64, _ types.TenantRole, agentID string, _ ...uint64) (*types.CustomAgent, error) {
|
||||
return s.agents[agentID], nil
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,10 @@ type AgentConfig struct {
|
||||
// Per-request @mention pins (runtime only; injected as <must_use> in the user message).
|
||||
PinnedMCPServiceIDs []string `json:"-"`
|
||||
PinnedSkillNames []string `json:"-"`
|
||||
// SharedAgentReadOnly prevents a shared agent from mutating resources in
|
||||
// its source workspace. It is set from the verified share relation, never
|
||||
// inferred from a client-provided tenant ID.
|
||||
SharedAgentReadOnly bool `json:"-"`
|
||||
// LLM call timeout in seconds (default: 120). Controls the maximum time for a single LLM call.
|
||||
LLMCallTimeout int `json:"llm_call_timeout,omitempty"`
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ type OrganizationRepository interface {
|
||||
// (not user). The 3-dimension cap is applied inside CheckTenantKBPermission
|
||||
// and is the canonical permission gate for shared KBs:
|
||||
//
|
||||
// effective = min(share.Permission, tenant_org_role, tenant_role_cap)
|
||||
// effective = min(share.Permission, tenant_org_role, tenant_role_cap)
|
||||
//
|
||||
// where tenant_role_cap pins tenant Viewers to OrgRoleViewer regardless
|
||||
// of the org-level grant — Viewer in your own tenant must always be
|
||||
@@ -181,7 +181,7 @@ type AgentShareService interface {
|
||||
// SetSharedAgentDisabledByMe sets whether the current tenant has "disabled" this shared agent for their conversation dropdown (per-tenant preference; will be revisited in a follow-up PR).
|
||||
SetSharedAgentDisabledByMe(ctx context.Context, tenantID uint64, agentID string, sourceTenantID uint64, disabled bool) error
|
||||
// GetSharedAgentForTenant returns the shared agent by agentID if the caller's tenant has access; used to resolve KB scope for @ mention.
|
||||
GetSharedAgentForTenant(ctx context.Context, tenantID uint64, callerTenantRole types.TenantRole, agentID string) (*types.CustomAgent, error)
|
||||
GetSharedAgentForTenant(ctx context.Context, tenantID uint64, callerTenantRole types.TenantRole, agentID string, sourceTenantID ...uint64) (*types.CustomAgent, error)
|
||||
// TenantCanAccessKBViaSomeSharedAgent returns true if the caller's tenant has at least one shared agent that can access the given KB (for opening KB detail from "通过智能体可见" list without passing agent_id).
|
||||
TenantCanAccessKBViaSomeSharedAgent(ctx context.Context, tenantID uint64, callerTenantRole types.TenantRole, kb *types.KnowledgeBase) (bool, error)
|
||||
GetShare(ctx context.Context, shareID string) (*types.AgentShare, error)
|
||||
@@ -205,6 +205,7 @@ type AgentShareRepository interface {
|
||||
ListByOrganization(ctx context.Context, orgID string) ([]*types.AgentShare, error)
|
||||
ListByOrganizations(ctx context.Context, orgIDs []string) ([]*types.AgentShare, error)
|
||||
ListSharedAgentsForTenant(ctx context.Context, tenantID uint64) ([]*types.AgentShare, error)
|
||||
GetShareByAgentIDAndSourceForTenant(ctx context.Context, tenantID uint64, agentID string, sourceTenantID uint64) (*types.AgentShare, error)
|
||||
CountByOrganizations(ctx context.Context, orgIDs []string) (map[string]int64, error)
|
||||
// GetShareByAgentIDForTenant returns one share for the given agentID that the tenant can access (tenant in org), excluding source_tenant_id == excludeTenantID.
|
||||
GetShareByAgentIDForTenant(ctx context.Context, tenantID uint64, agentID string, excludeTenantID uint64) (*types.AgentShare, error)
|
||||
|
||||
@@ -265,6 +265,10 @@ type SharedAgentInfo struct {
|
||||
SharedAt time.Time `json:"shared_at"`
|
||||
SharedByUserID string `json:"shared_by_user_id,omitempty"`
|
||||
SharedByUsername string `json:"shared_by_username,omitempty"`
|
||||
// WebSearchReady is resolved against the source workspace without exposing
|
||||
// its provider list or credentials. Receivers must not compare the agent's
|
||||
// provider ID with their own workspace resources.
|
||||
WebSearchReady bool `json:"web_search_ready"`
|
||||
// DisabledByMe: current tenant has hidden this shared agent from their conversation dropdown (per-user preference)
|
||||
DisabledByMe bool `json:"disabled_by_me"`
|
||||
}
|
||||
|
||||
@@ -4,20 +4,21 @@ package types
|
||||
// replacing the previous 14-parameter method signatures.
|
||||
// EventBus is passed separately to avoid circular dependency with the event package.
|
||||
type QARequest struct {
|
||||
Session *Session // The conversation session
|
||||
Query string // User query text
|
||||
AssistantMessageID string // Pre-created assistant message ID
|
||||
SummaryModelID string // Optional model override; empty = use agent/KB default
|
||||
CustomAgent *CustomAgent // Optional custom agent for config override
|
||||
KnowledgeBaseIDs []string // Knowledge base IDs to search (from request + @mentions)
|
||||
KnowledgeIDs []string // Specific knowledge (file) IDs to search
|
||||
TagScopes []TagScope // Tag-constrained KB scopes from @mentions
|
||||
MCPServiceIDs []string // Per-request MCP service IDs from @mentions
|
||||
SkillNames []string // Per-request preloaded skill names from @mentions
|
||||
ImageURLs []string // Image URLs for multimodal input
|
||||
ImageDescription string // VLM-generated image description (fallback for non-vision models)
|
||||
UserMessageID string // Created user message ID
|
||||
WebSearchEnabled bool // Whether web search is enabled for this request
|
||||
QuotedContext string // Quoted message content from IM quote-reply (appended at LLM prompt stage, not used for retrieval)
|
||||
Attachments MessageAttachments // File attachments (processed and ready for prompt injection)
|
||||
Session *Session // The conversation session
|
||||
Query string // User query text
|
||||
AssistantMessageID string // Pre-created assistant message ID
|
||||
SummaryModelID string // Optional model override; empty = use agent/KB default
|
||||
CustomAgent *CustomAgent // Optional custom agent for config override
|
||||
SharedAgentReadOnly bool // True only when access came from an agent share; source-workspace writes are forbidden
|
||||
KnowledgeBaseIDs []string // Knowledge base IDs to search (from request + @mentions)
|
||||
KnowledgeIDs []string // Specific knowledge (file) IDs to search
|
||||
TagScopes []TagScope // Tag-constrained KB scopes from @mentions
|
||||
MCPServiceIDs []string // Per-request MCP service IDs from @mentions
|
||||
SkillNames []string // Per-request preloaded skill names from @mentions
|
||||
ImageURLs []string // Image URLs for multimodal input
|
||||
ImageDescription string // VLM-generated image description (fallback for non-vision models)
|
||||
UserMessageID string // Created user message ID
|
||||
WebSearchEnabled bool // Whether web search is enabled for this request
|
||||
QuotedContext string // Quoted message content from IM quote-reply (appended at LLM prompt stage, not used for retrieval)
|
||||
Attachments MessageAttachments // File attachments (processed and ready for prompt injection)
|
||||
}
|
||||
|
||||
@@ -92,8 +92,11 @@ type TemporaryDocumentTaskPayload struct {
|
||||
}
|
||||
|
||||
type TemporaryDocumentCreateOptions struct {
|
||||
ASRModelID string `json:"asr_model_id,omitempty"`
|
||||
ParserEngine string `json:"parser_engine,omitempty"`
|
||||
// ResourceTenantID is the verified agent source workspace used to resolve
|
||||
// parser/model dependencies. The document itself remains owned by TenantID.
|
||||
ResourceTenantID uint64 `json:"resource_tenant_id,omitempty"`
|
||||
ASRModelID string `json:"asr_model_id,omitempty"`
|
||||
ParserEngine string `json:"parser_engine,omitempty"`
|
||||
// VLMModelID enables image understanding (caption/OCR) during async parse.
|
||||
// Images use it to produce real text content; scanned/image-only documents
|
||||
// use it as an OCR fallback when ImageUnderstanding is on.
|
||||
|
||||
Reference in New Issue
Block a user