fix(knowledge): make protected file downloads WebView-safe

This commit is contained in:
2026-07-24 13:31:30 +08:00
parent 83c497c5f1
commit 442ba66848
9 changed files with 233 additions and 20 deletions
+27 -14
View File
@@ -229,7 +229,7 @@
<view class="resource-copy">
<text class="resource-title">{{ resource.title }}</text>
<text class="resource-meta">
{{ resource.loading ? '正在安全读取…' : resource.type === 'VIDEO' ? (resource.localUrl ? '已加载,可直接播放' : '点击加载操作视频') : '点击查看或下载原文件' }}
{{ resource.loading ? (resource.type === 'VIDEO' ? '正在安全读取…' : '正在生成安全下载链接…') : resource.type === 'VIDEO' ? (resource.localUrl ? '已加载,可直接播放' : '点击加载操作视频') : '点击下载原文件' }}
</text>
</view>
<text class="resource-arrow">›</text>
@@ -423,6 +423,7 @@ import {
chooseKnowledgeMedia,
generateSummaryCard,
queryKnowledgeTool,
requestKnowledgeResourceDownloadLink,
searchKnowledge,
searchKnowledgeWithMedia,
submitAnswerFeedback
@@ -1206,24 +1207,36 @@ const openKnowledgeResource = async (resource: KnowledgeResource) => {
if (resource.type === 'VIDEO' && resource.localUrl) return;
resource.loading = true;
try {
if (resource.type === 'FILE') {
const downloadUrl = await requestKnowledgeResourceDownloadLink(resource.attachmentId);
if (typeof document !== 'undefined') {
const link = document.createElement('a');
link.href = downloadUrl;
link.target = '_blank';
link.rel = 'noreferrer';
document.body.appendChild(link);
link.click();
link.remove();
return;
}
uni.downloadFile({
url: downloadUrl,
success: (response) => {
if (response.statusCode >= 200 && response.statusCode < 300 && response.tempFilePath) {
uni.openDocument({ filePath: response.tempFilePath, showMenu: true });
return;
}
uni.showToast({ title: '文件下载失败,请稍后重试', icon: 'none' });
},
fail: () => uni.showToast({ title: '文件下载失败,请稍后重试', icon: 'none' })
});
return;
}
const localUrl = await authenticatedFileUrl(resource.contentUrl);
if (resource.localUrl && resource.localUrl !== localUrl) {
releaseAuthenticatedFileUrl(resource.localUrl);
}
resource.localUrl = localUrl;
if (resource.type === 'FILE') {
if (typeof document !== 'undefined') {
const link = document.createElement('a');
link.href = localUrl;
link.download = resource.title || '资料文件';
link.target = '_blank';
document.body.appendChild(link);
link.click();
link.remove();
} else {
uni.openDocument({ filePath: localUrl, showMenu: true });
}
}
} catch (error) {
uni.showToast({ title: requestErrorText(error, '资料读取失败'), icon: 'none' });
} finally {
+11
View File
@@ -167,6 +167,17 @@ export const queryKnowledgeTool = async (
return normalizeUnifiedResponse(response);
};
export const requestKnowledgeResourceDownloadLink = async (attachmentId: number) => {
const response = await apiRequest<{ url: string }>({
url: `/api/knowledge/resources/${encodeURIComponent(String(attachmentId))}/download-link`,
method: 'POST'
});
if (!response.url?.startsWith('/api/knowledge/resources/')) {
throw new Error('资料下载链接无效');
}
return apiUrl(response.url);
};
const normalizeUnifiedResponse = (response: UnifiedKnowledgeResponse): KnowledgeSearchResponse => {
const legacy = response.legacy || ({} as KnowledgeSearchResponse);
const citationSnippets = (response.citations || []).map((item) => normalizeSnippet({
+4
View File
@@ -24,9 +24,13 @@ test('问师傅支持显式新对话、版本回写和受控原文件或视频
assert.match(header, /class="new-chat-text">新对话/);
assert.match(page, /const contextVersion = ref\(0\)/);
assert.match(page, /applyConversationState\(nextResult\)/);
assert.match(page, /requestKnowledgeResourceDownloadLink\(resource\.attachmentId\)/);
assert.match(page, /authenticatedFileUrl\(resource\.contentUrl\)/);
assert.match(page, /releaseAuthenticatedFileUrl\(resource\.localUrl\)/);
assert.match(page, /resource\.type === 'VIDEO' && resource\.localUrl/);
assert.doesNotMatch(page, /link\.href = localUrl/);
assert.match(page, /link\.rel = 'noreferrer'/);
assert.match(page, /点击下载原文件/);
assert.match(page, /msg\.result\.rewrittenQuery !== msg\.result\.queryText/);
assert.match(page, /已结合本次对话理解/);
});