From 4bc801d943a91ca6ed4602fed3d9da4843505adf Mon Sep 17 00:00:00 2001 From: Drol Date: Wed, 15 Jul 2026 21:50:54 +0800 Subject: [PATCH 1/3] fix(plugin-ai): preserve pasted file attachment source (#10106) --- .../chatbox/uploadAttachment.test.ts | 46 +++++++++++++++++++ .../client/ai-employees/chatbox/Sender.tsx | 6 +-- .../chatbox/hooks/useUploadFiles.ts | 7 +-- .../src/client/ai-employees/chatbox/utils.ts | 17 +++++++ 4 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 packages/plugins/@nocobase/plugin-ai/src/client/__tests__/chatbox/uploadAttachment.test.ts diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/__tests__/chatbox/uploadAttachment.test.ts b/packages/plugins/@nocobase/plugin-ai/src/client/__tests__/chatbox/uploadAttachment.test.ts new file mode 100644 index 00000000000..123ef59c2ac --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/__tests__/chatbox/uploadAttachment.test.ts @@ -0,0 +1,46 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { describe, expect, it } from 'vitest'; +import { normalizeAIFileUploadAttachment } from '../../ai-employees/chatbox/utils'; + +describe('normalizeAIFileUploadAttachment', () => { + it('promotes upload response meta source to attachment source', () => { + const source = { + dataSourceKey: 'main', + collectionName: 'aiFiles', + }; + const attachment = { + id: 1, + filename: 'paste.png', + meta: { + source, + }, + }; + + expect(normalizeAIFileUploadAttachment(attachment, 'done')).toEqual({ + ...attachment, + source, + status: 'done', + }); + }); + + it('keeps response data when source is missing', () => { + const attachment = { + id: 2, + filename: 'paste.txt', + meta: {}, + }; + + expect(normalizeAIFileUploadAttachment(attachment, 'done')).toEqual({ + ...attachment, + status: 'done', + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Sender.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Sender.tsx index b89711816f5..dcdf0f92f4e 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Sender.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Sender.tsx @@ -20,6 +20,7 @@ import { useChatMessageActions } from './hooks/useChatMessageActions'; import { useChatBoxStore } from './stores/chat-box'; import { useChatBoxActions } from './hooks/useChatBoxActions'; import { useUploadFiles } from './hooks/useUploadFiles'; +import { normalizeAIFileUploadAttachment } from './utils'; import _ from 'lodash'; const useSendMessage = () => { @@ -168,10 +169,7 @@ export const Sender: React.FC = () => { response, }; } - return { - ...fileData, - status: 'done', - }; + return normalizeAIFileUploadAttachment(fileData, 'done'); } return item; }), diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useUploadFiles.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useUploadFiles.ts index b5e9ac977cc..a478553a273 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useUploadFiles.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useUploadFiles.ts @@ -12,6 +12,7 @@ import PluginFileManagerClient from '@nocobase/plugin-file-manager/client'; import { useAISettingsContext } from '../../AISettingsProvider'; import { useChat } from '../hooks/useChat'; import { useChatConversationsStore } from '../stores/chat-conversations'; +import { normalizeAIFileUploadAttachment } from '../utils'; export function useStorage(storage: string) { const name = storage ?? ''; @@ -94,11 +95,7 @@ export const useUploadFiles = () => { if (!file?.response?.data) { return file; } - return { - ...file.response.data, - ...(file.response.data.meta?.source ? { source: file.response.data.meta.source } : {}), - status: file.status, - }; + return normalizeAIFileUploadAttachment(file.response.data, file.status); } return file; }), diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/utils.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/utils.ts index 0e0b7eb557a..104d751ad86 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/utils.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/utils.ts @@ -15,6 +15,23 @@ import PluginAIClient from '../..'; dayjs.extend(duration); +function isRecord(value: unknown): value is Record { + return Boolean(value && typeof value === 'object' && !Array.isArray(value)); +} + +export function normalizeAIFileUploadAttachment(fileData: unknown, status: string) { + if (!isRecord(fileData)) { + return fileData; + } + const meta = isRecord(fileData.meta) ? fileData.meta : undefined; + const source = isRecord(meta?.source) ? meta.source : undefined; + return { + ...fileData, + ...(source ? { source } : {}), + status, + }; +} + async function replaceVariables(template, variables, localVariables = {}) { const regex = /\{\{\s*(.*?)\s*\}\}/g; let result = template; From cd63615260b0bdcd1e800f56a4949d03b1fcb2b6 Mon Sep 17 00:00:00 2001 From: Junyi Date: Wed, 15 Jul 2026 22:00:00 +0800 Subject: [PATCH 2/3] refactor(plugin-file-manager): use permanent url for files (#10103) * refactor(plugin-file-manager): use permanent url for files * fix(plugin-file-manager): fix issues * refactor(plugin-file-manager): not to change legacy api * refactor(plugin-file-manager): simplify api * fix(plugin-file-manager): fix issues * fix(plugin-public-forms): fix data source name * fix(plugin-file-manager): fix types * refactor(plugin-file-manager): change token name style * docs(plugin-file-manager): add docs for stable url * fix(plugin-file-manager): fix type and local field * fix(plugin-file-previewer-office): fix type * fix(plugin-file-manager): fix test cases * fix(core): fix nginx.conf.tpl * fix(plugin-data-source-main): add hidden as filter flag for skipping fields * fix(plugin-file-manager): fix thumbnail for attachment url field * fix(plugin-file-manager): fix preview field * docs(plugin-file-manager): add docs * fix(plugin-file-manager): fix test cases * fix(plugin-file-manager): fix type * fix(plugin-file-manager): fix issues from review * fix(resourcer): fix types * refactor(plugin-file-manager): fix issues from review * docs(plugin-file-manager): adjust docs * fix(plugin-acl): fix test cases * fix: issues from review * fix(plugin-file-manager): fix virtual field local --- docs/docs/cn/file-manager/_meta.json | 5 + docs/docs/cn/file-manager/stable-url.md | 146 +++ .../cn/get-started/deployment/production.md | 24 +- .../get-started/installation/docker-caddy.mdx | 3 + .../get-started/installation/docker-nginx.mdx | 3 + .../cn/get-started/installation/docker.mdx | 30 +- docs/docs/cn/get-started/installation/env.md | 33 + docs/docs/cn/nocobase-cli/production/index.md | 10 + .../production/reverse-proxy/caddy.md | 17 +- .../production/reverse-proxy/index.md | 2 +- .../production/reverse-proxy/nginx.md | 18 +- docs/docs/de/file-manager/_meta.json | 5 + docs/docs/de/file-manager/stable-url.md | 57 + .../de/get-started/deployment/production.md | 26 +- .../get-started/installation/docker-caddy.mdx | 3 + .../get-started/installation/docker-nginx.mdx | 3 + .../de/get-started/installation/docker.mdx | 30 +- docs/docs/de/get-started/installation/env.md | 33 + docs/docs/de/nocobase-cli/production/index.md | 10 + .../production/reverse-proxy/caddy.md | 17 +- .../production/reverse-proxy/index.md | 2 +- .../production/reverse-proxy/nginx.md | 18 +- docs/docs/en/file-manager/_meta.json | 5 + docs/docs/en/file-manager/stable-url.md | 87 ++ .../en/get-started/deployment/production.md | 26 +- .../get-started/installation/docker-caddy.mdx | 3 + .../get-started/installation/docker-nginx.mdx | 3 + .../en/get-started/installation/docker.mdx | 30 +- docs/docs/en/get-started/installation/env.md | 33 + docs/docs/en/nocobase-cli/production/index.md | 10 + .../production/reverse-proxy/caddy.md | 17 +- .../production/reverse-proxy/index.md | 2 +- .../production/reverse-proxy/nginx.md | 18 +- docs/docs/es/file-manager/_meta.json | 5 + docs/docs/es/file-manager/stable-url.md | 57 + .../es/get-started/deployment/production.md | 26 +- .../get-started/installation/docker-caddy.mdx | 3 + .../get-started/installation/docker-nginx.mdx | 3 + .../es/get-started/installation/docker.mdx | 30 +- docs/docs/es/get-started/installation/env.md | 33 + docs/docs/es/nocobase-cli/production/index.md | 10 + .../production/reverse-proxy/caddy.md | 17 +- .../production/reverse-proxy/index.md | 2 +- .../production/reverse-proxy/nginx.md | 18 +- docs/docs/fr/file-manager/_meta.json | 5 + docs/docs/fr/file-manager/stable-url.md | 57 + .../fr/get-started/deployment/production.md | 26 +- .../get-started/installation/docker-caddy.mdx | 3 + .../get-started/installation/docker-nginx.mdx | 3 + .../fr/get-started/installation/docker.mdx | 30 +- docs/docs/fr/get-started/installation/env.md | 33 + docs/docs/fr/nocobase-cli/production/index.md | 10 + .../production/reverse-proxy/caddy.md | 17 +- .../production/reverse-proxy/index.md | 2 +- .../production/reverse-proxy/nginx.md | 18 +- docs/docs/id/file-manager/_meta.json | 5 + docs/docs/id/file-manager/stable-url.md | 57 + .../id/get-started/deployment/production.md | 24 +- .../get-started/installation/docker-caddy.mdx | 3 + .../get-started/installation/docker-nginx.mdx | 3 + .../id/get-started/installation/docker.mdx | 30 +- docs/docs/id/get-started/installation/env.md | 33 + docs/docs/id/nocobase-cli/production/index.md | 10 + .../production/reverse-proxy/caddy.md | 17 +- .../production/reverse-proxy/index.md | 2 +- .../production/reverse-proxy/nginx.md | 18 +- docs/docs/ja/file-manager/_meta.json | 5 + docs/docs/ja/file-manager/stable-url.md | 57 + .../ja/get-started/deployment/production.md | 26 +- .../get-started/installation/docker-caddy.mdx | 3 + .../get-started/installation/docker-nginx.mdx | 3 + .../ja/get-started/installation/docker.mdx | 30 +- docs/docs/ja/get-started/installation/env.md | 33 + docs/docs/ja/nocobase-cli/production/index.md | 10 + .../production/reverse-proxy/caddy.md | 17 +- .../production/reverse-proxy/index.md | 2 +- .../production/reverse-proxy/nginx.md | 18 +- docs/docs/pt/file-manager/_meta.json | 5 + docs/docs/pt/file-manager/stable-url.md | 57 + .../pt/get-started/deployment/production.md | 26 +- .../get-started/installation/docker-caddy.mdx | 3 + .../get-started/installation/docker-nginx.mdx | 3 + .../pt/get-started/installation/docker.mdx | 30 +- docs/docs/pt/get-started/installation/env.md | 33 + docs/docs/pt/nocobase-cli/production/index.md | 10 + .../production/reverse-proxy/caddy.md | 17 +- .../production/reverse-proxy/index.md | 2 +- .../production/reverse-proxy/nginx.md | 18 +- docs/docs/ru/file-manager/_meta.json | 5 + docs/docs/ru/file-manager/stable-url.md | 57 + .../ru/get-started/deployment/production.md | 26 +- .../get-started/installation/docker-caddy.mdx | 3 + .../get-started/installation/docker-nginx.mdx | 3 + .../ru/get-started/installation/docker.mdx | 30 +- docs/docs/ru/get-started/installation/env.md | 33 + docs/docs/ru/nocobase-cli/production/index.md | 10 + .../production/reverse-proxy/caddy.md | 17 +- .../production/reverse-proxy/index.md | 2 +- .../production/reverse-proxy/nginx.md | 18 +- docs/docs/vi/file-manager/_meta.json | 5 + docs/docs/vi/file-manager/stable-url.md | 57 + .../vi/get-started/deployment/production.md | 24 +- .../get-started/installation/docker-caddy.mdx | 3 + .../get-started/installation/docker-nginx.mdx | 3 + .../vi/get-started/installation/docker.mdx | 30 +- docs/docs/vi/get-started/installation/env.md | 33 + docs/docs/vi/nocobase-cli/production/index.md | 10 + .../production/reverse-proxy/caddy.md | 17 +- .../production/reverse-proxy/index.md | 2 +- .../production/reverse-proxy/nginx.md | 18 +- packages/core/acl/src/__tests__/acl.test.ts | 146 ++- packages/core/acl/src/acl.ts | 279 +++-- packages/core/app/client-v2/rsbuild.config.ts | 5 + packages/core/app/client/rsbuild.config.ts | 5 + .../auth/src/__tests__/auth-manager.test.ts | 70 ++ .../auth/src/__tests__/middleware.test.ts | 229 ++++ packages/core/auth/src/actions.ts | 32 +- packages/core/auth/src/auth-manager.ts | 83 +- packages/core/auth/src/auth.ts | 26 +- packages/core/auth/src/base/auth.ts | 130 ++ packages/core/cli-v1/nocobase.conf.tpl | 17 + .../cli-v1/src/commands/create-nginx-conf.js | 15 + .../core/cli/src/__tests__/env-proxy.test.ts | 18 +- packages/core/cli/src/lib/env-proxy.ts | 30 +- packages/core/client-v2/src/APIClient.ts | 11 +- packages/core/client-v2/src/Application.tsx | 4 + .../core/client-v2/src/__tests__/app.test.tsx | 18 + .../nocobase-buildin-plugin-auth.test.tsx | 28 + .../src/nocobase-buildin-plugin/index.tsx | 10 + .../client/src/application/Application.tsx | 4 + .../__tests__/Application.test.tsx | 18 + .../schema-component/antd/upload/Upload.tsx | 76 +- .../antd/upload/__tests__/placeholder.test.ts | 30 + .../antd/upload/__tests__/shared.test.ts | 98 +- .../antd/upload/__tests__/upload.test.tsx | 6 +- .../antd/upload/placeholder.ts | 19 +- .../schema-component/antd/upload/shared.ts | 145 ++- .../schema-component/antd/upload/type.d.ts | 6 + .../client/src/user/CurrentUserProvider.tsx | 11 +- .../__tests__/CurrentUserProvider.test.tsx | 67 +- packages/core/resourcer/package.json | 4 + packages/core/resourcer/src/resourcer.ts | 5 +- packages/core/sdk/package.json | 1 + packages/core/sdk/src/APIClient.ts | 2 + packages/core/sdk/src/Auth.ts | 24 + .../core/sdk/src/__tests__/api-client.test.ts | 111 ++ packages/core/sdk/src/auth-cookie.ts | 36 + packages/core/sdk/src/index.ts | 1 + .../core/server/src/__tests__/gateway.test.ts | 64 + .../gateway/static-file-security.test.ts | 5 + packages/core/server/src/application.ts | 3 +- packages/core/server/src/gateway/index.ts | 41 +- .../src/gateway/static-file-security.ts | 10 +- packages/core/server/src/helper.ts | 48 +- .../core/utils/src/__tests__/cors.test.ts | 65 + packages/core/utils/src/auth-cookie-name.ts | 21 + packages/core/utils/src/auth-cookie.ts | 37 + packages/core/utils/src/client.ts | 1 + packages/core/utils/src/cors.ts | 57 + packages/core/utils/src/index.ts | 2 + .../src/server/__tests__/acl-meta.test.ts | 4 +- .../src/server/__tests__/actions.test.ts | 8 +- .../server/__tests__/setCurrentRole.test.ts | 53 + .../@nocobase/plugin-acl/src/server/enum.ts | 2 + .../src/server/middlewares/setCurrentRole.ts | 119 +- .../@nocobase/plugin-acl/src/server/server.ts | 2 +- .../server/__tests__/export-to-xlsx.test.ts | 2 +- .../plugin-auth/src/locale/de-DE.json | 3 +- .../plugin-auth/src/locale/en-US.json | 4 +- .../plugin-auth/src/locale/es-ES.json | 3 +- .../plugin-auth/src/locale/fr-FR.json | 3 +- .../plugin-auth/src/locale/hu-HU.json | 3 +- .../plugin-auth/src/locale/id-ID.json | 3 +- .../plugin-auth/src/locale/it-IT.json | 3 +- .../plugin-auth/src/locale/ja-JP.json | 3 +- .../plugin-auth/src/locale/ko-KR.json | 3 +- .../plugin-auth/src/locale/nl-NL.json | 3 +- .../plugin-auth/src/locale/pt-BR.json | 3 +- .../plugin-auth/src/locale/ru-RU.json | 3 +- .../plugin-auth/src/locale/tr-TR.json | 3 +- .../plugin-auth/src/locale/uk-UA.json | 3 +- .../plugin-auth/src/locale/vi-VN.json | 3 +- .../plugin-auth/src/locale/zh-CN.json | 4 +- .../plugin-auth/src/locale/zh-TW.json | 3 +- .../src/server/__tests__/actions.test.ts | 170 +++ .../server/__tests__/token-controller.test.ts | 13 +- .../plugin-auth/src/server/locale/en-US.json | 3 +- .../plugin-auth/src/server/locale/zh-CN.json | 5 +- .../plugin-auth/src/server/plugin.ts | 4 +- .../__tests__/http-api/modeling-apply.test.ts | 1 + .../src/server/modeling/capabilities.ts | 1 + .../src/client-v2/AttachmentURLFieldModel.tsx | 110 +- .../__tests__/AttachmentURLFieldModel.test.ts | 169 +++ .../src/client/__tests__/hook.test.ts | 111 ++ .../src/client/hook/index.ts | 100 +- .../src/client-v2/components/Edit.tsx | 5 - .../src/client/components/Edit.tsx | 8 - .../src/server/__tests__/vditor-check.test.ts | 8 +- .../src/server/plugin.ts | 5 +- .../plugin-file-manager/package.json | 4 +- .../src/client-v2/index.tsx | 9 +- .../models/DisplayPreviewFieldModel.tsx | 153 ++- .../client-v2/models/UploadActionModel.tsx | 59 +- .../src/client-v2/models/UploadFieldModel.tsx | 58 +- .../DisplayPreviewFieldModel.test.tsx | 67 + .../__tests__/UploadActionModel.test.ts | 57 + .../src/client-v2/plugin.tsx | 18 +- .../client-v2/previewer/filePreviewTypes.tsx | 417 ++++++- .../src/client/hooks/useStorageRules.ts | 12 +- .../src/client/hooks/useStorageUploadProps.ts | 28 +- .../__tests__/filePreviewTypes.test.ts | 231 ++++ .../src/server/__tests__/action.test.ts | 22 +- .../src/server/__tests__/index.ts | 10 +- .../src/server/__tests__/server.test.ts | 1108 ++++++++++++++++- .../server/__tests__/storages/ali-oss.test.ts | 22 +- .../__tests__/storages/download-url.test.ts | 81 ++ .../src/server/__tests__/storages/s3.test.ts | 22 +- .../server/__tests__/storages/tx-cos.test.ts | 10 +- .../src/server/__tests__/utils.test.ts | 26 +- .../src/server/actions/get-file.ts | 139 +++ .../src/server/actions/index.ts | 11 +- .../src/server/commands/repair-filenames.ts | 73 +- .../src/server/file-access.ts | 123 ++ .../plugin-file-manager/src/server/index.ts | 9 +- .../src/server/resolve-file-access-filter.ts | 30 + .../plugin-file-manager/src/server/server.ts | 149 ++- .../src/server/storages/ali-oss.ts | 15 +- .../src/server/storages/index.ts | 32 +- .../src/server/storages/local.ts | 15 +- .../src/server/storages/s3.ts | 25 +- .../src/server/storages/tx-cos.ts | 30 +- .../src/server/temporary-access.ts | 156 +++ .../plugin-file-manager/src/server/utils.ts | 82 ++ .../src/client-v2/OfficeInlinePreviewer.tsx | 106 +- .../src/client-v2/__tests__/utils.test.ts | 174 ++- .../src/client-v2/utils.ts | 118 +- .../src/client/index.tsx | 36 +- .../src/locale/en-US.json | 7 +- .../src/locale/zh-CN.json | 7 +- .../plugin-public-forms/package.json | 3 +- .../src/server/__tests__/plugin.test.ts | 391 ++++++ .../plugin-public-forms/src/server/plugin.ts | 286 ++++- 242 files changed, 9092 insertions(+), 622 deletions(-) create mode 100644 docs/docs/cn/file-manager/stable-url.md create mode 100644 docs/docs/de/file-manager/stable-url.md create mode 100644 docs/docs/en/file-manager/stable-url.md create mode 100644 docs/docs/es/file-manager/stable-url.md create mode 100644 docs/docs/fr/file-manager/stable-url.md create mode 100644 docs/docs/id/file-manager/stable-url.md create mode 100644 docs/docs/ja/file-manager/stable-url.md create mode 100644 docs/docs/pt/file-manager/stable-url.md create mode 100644 docs/docs/ru/file-manager/stable-url.md create mode 100644 docs/docs/vi/file-manager/stable-url.md create mode 100644 packages/core/client/src/schema-component/antd/upload/__tests__/placeholder.test.ts create mode 100644 packages/core/sdk/src/auth-cookie.ts create mode 100644 packages/core/utils/src/__tests__/cors.test.ts create mode 100644 packages/core/utils/src/auth-cookie-name.ts create mode 100644 packages/core/utils/src/auth-cookie.ts create mode 100644 packages/core/utils/src/cors.ts create mode 100644 packages/plugins/@nocobase/plugin-field-attachment-url/src/client-v2/__tests__/AttachmentURLFieldModel.test.ts create mode 100644 packages/plugins/@nocobase/plugin-field-attachment-url/src/client/__tests__/hook.test.ts create mode 100644 packages/plugins/@nocobase/plugin-file-manager/src/client-v2/models/__tests__/DisplayPreviewFieldModel.test.tsx create mode 100644 packages/plugins/@nocobase/plugin-file-manager/src/client-v2/models/__tests__/UploadActionModel.test.ts create mode 100644 packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/storages/download-url.test.ts create mode 100644 packages/plugins/@nocobase/plugin-file-manager/src/server/actions/get-file.ts create mode 100644 packages/plugins/@nocobase/plugin-file-manager/src/server/file-access.ts create mode 100644 packages/plugins/@nocobase/plugin-file-manager/src/server/resolve-file-access-filter.ts create mode 100644 packages/plugins/@nocobase/plugin-file-manager/src/server/temporary-access.ts diff --git a/docs/docs/cn/file-manager/_meta.json b/docs/docs/cn/file-manager/_meta.json index 928e9cdab13..6877f1e0f2f 100644 --- a/docs/docs/cn/file-manager/_meta.json +++ b/docs/docs/cn/file-manager/_meta.json @@ -59,6 +59,11 @@ "label": "附件字段", "link": "/file-manager/field-attachment" }, + { + "type": "custom-link", + "label": "稳定 URL", + "link": "/file-manager/stable-url" + }, { "type": "custom-link", "label": "文件预览", diff --git a/docs/docs/cn/file-manager/stable-url.md b/docs/docs/cn/file-manager/stable-url.md new file mode 100644 index 00000000000..265f7c9c2c7 --- /dev/null +++ b/docs/docs/cn/file-manager/stable-url.md @@ -0,0 +1,146 @@ +--- +pkg: '@nocobase/plugin-file-manager' +title: "稳定 URL(代理 URL)" +description: "介绍 NocoBase 文件稳定 URL 的格式、访问权限,以及它在附件字段、文件表、Markdown、文件预览和 HTTP API 中的表现。" +keywords: "稳定 URL,代理 URL,永久 URL,文件访问,文件权限,Office 预览,NocoBase" +--- + +# 稳定 URL + +在 NocoBase 中,由存储引擎托管的文件会通过**稳定 URL(Stable URL)**访问。这个地址先进入 NocoBase,再由 NocoBase 检查文件记录和访问权限,最后重定向到存储引擎生成的实际地址。 + +## URL 格式 + +文件记录返回的地址通常是: + +```text +/files//// +``` + +比如: + +```text +/files/main/main/attachments/42.pdf +``` + +如果 NocoBase 配置了 `APP_PUBLIC_PATH=/nocobase`,地址会自动带上该前缀: + +```text +/nocobase/files/main/main/attachments/42.pdf +``` + +其中: + +- `app` 是应用名称 +- `dataSource` 是数据源标识 +- `collection` 是附件表或文件表名称 +- `id` 是文件记录的 ID +- `extname` 是文件扩展名,没有扩展名时不会追加 + +文件创建后,`id` 和 `extname` 不允许修改,因此同一条文件记录的地址可以保持稳定。 + +## 不同用途的地址 + +同一条文件记录会根据用途使用不同的 query 参数: + +| 用途 | 地址形式 | 表现 | +|---|---|---| +| 打开或内嵌文件 | `/files/.../42.pdf` | 检查权限后重定向到文件实际地址 | +| 预览图片等内容 | `/files/.../42.png?preview=1` | 检查权限后重定向到存储引擎的预览地址;存储引擎支持缩略图规则时会使用缩略图 | +| 下载文件 | `/files/.../42.pdf?download=1` | 检查权限后返回带下载语义的实际地址 | +| Office 在线预览 | `/files/.../42.xlsx?temporaryAccessToken=...` | 供 Microsoft Office Online Viewer 短期读取,不作为普通文件地址保存 | + +:::tip + +业务代码通常只需要使用接口返回的 `url` 和 `preview`,不需要自己拼接 `/files` 地址或 query 参数。 + +::: + +## 在各个地方的表现 + +### 附件字段和文件表 + +附件字段与文件表中的系统托管文件,上传、查询和关联读取后返回的 `url` 都是稳定 URL,`preview` 则是在同一地址上增加 `preview=1`。 + +图片、PDF、音视频和文本等文件仍可在 NocoBase 中预览。刚上传的本地图片会优先使用浏览器生成的临时本地预览,数据重新读取后再使用稳定 URL,避免上传完成时出现重复请求或缩略图闪烁。 + +### HTTP API + +通过 [HTTP API](./http-api.md) 上传或查询文件时,响应中的 `url` / `preview` 不再暴露本地路径、对象存储域名或预签名下载地址。调用方访问稳定 URL 时需要携带对应应用的登录凭证。 + +稳定 URL 返回 `302` 重定向,不直接代理文件流。如果使用 `curl` 等客户端读取文件内容,需要允许跟随重定向: + +```bash +curl -L \ + -H "Authorization: Bearer " \ + "https://example.com/files/main/main/attachments/42.pdf" +``` + +浏览器直接打开文件时通常使用登录 cookie。`GET` 和 `HEAD` 可以访问稳定 URL,其他 HTTP 方法会返回 `405`。 + +### Markdown 编辑器 + +在 Markdown 编辑器中上传文件后,写入 Markdown 内容的是稳定 URL。私有 S3、OSS、COS 或 S3 Pro 存储也可以使用这种方式,不需要把存储空间调整为公开读取。 + +如果 Markdown 内容会展示在未登录页面,那么查看者仍需要拥有文件查看权限。仅把稳定 URL 写入 Markdown,不会自动把文件公开。 + +### 附件 URL 字段 + +附件 URL 字段上传到 NocoBase 存储引擎后,字段中保存的是稳定 URL。图片缩略图会使用对应的 `preview` 地址。 + +如果字段保存的是手工输入的外部 URL,并且文件记录没有 `storageId`,NocoBase 会继续保留并返回原始外部 URL。这类文件不经过稳定 URL 的权限检查和重定向流程。 + +### 普通文件预览 + +图片、PDF、音频、视频和文本预览会直接使用稳定 URL。浏览器请求会携带 NocoBase 登录 cookie,再按当前角色检查附件表或文件表的 `get/view` 权限。 + +对于对象存储中的 PDF 等文件,最终预览方式还会受到存储服务 CORS 配置的影响。如果自定义前端通过 `fetch()` 读取重定向后的对象存储地址,也需要确保对象存储允许当前站点跨域访问。 + +### Office 文件预览 + +Microsoft Office Online Viewer 由 Microsoft 服务端拉取文件,无法携带用户浏览器中的 NocoBase cookie。因此,用户真正打开 Office 预览时,NocoBase 会先检查该文件的查看权限,再签发一个短期临时 URL。 + +临时 URL 默认有效 10 分钟,可以通过 `TEMPORARY_FILE_ACCESS_EXPIRES_IN` 配置为 5 到 10 分钟。它只绑定当前文件,过期后无法继续使用。重新打开预览时会重新申请,加载失败时预览器也可能重新申请一次。 + +:::warning 注意 + +临时 URL 只用于外部预览服务读取文件。不要把它写回附件字段、Markdown 或业务表,也不要把它当成长期分享链接。 + +::: + +### 公开表单 + +访客在公开表单中上传文件后,NocoBase 会在当前公开表单会话中记录这些文件。上传者可以继续看到自己刚上传的图片或附件。 + +这个访问范围只服务于当前公开表单会话,并不是通用的公开文件链接。把地址复制到其他浏览器或当前会话失效后,仍可能无法访问。 + +## 权限和重定向 + +访问稳定 URL 时,NocoBase 会按照 URL 中的应用、数据源、文件表和记录 ID 定位文件。其中: + +1. 已登录用户使用当前应用的登录 cookie 或认证信息,并检查当前角色的文件查看权限。 +2. 公开表单等插件可以对特定文件提供额外的受限授权。 +3. 检查通过后,NocoBase 返回 `302`,跳转到本地存储或对象存储生成的实际地址。 + +因此,稳定 URL 隔离了业务数据和存储实现。切换存储域名、更新对象存储签名或调整缩略图规则时,业务字段中保存的地址通常不需要跟着修改。 + +## 使用注意 + +- 稳定不等于公开。复制链接给其他人后,对方仍需要登录并拥有文件查看权限 +- 稳定不等于永不失效。删除文件记录、删除文件、变更应用或数据源标识、移动到另一张文件表后,原地址会失效 +- 不要持久化 `temporaryAccessToken`。它是短期凭证,也可能进入浏览器历史和访问日志 +- 不要缓存 `302 Location` 作为永久地址。对象存储签名可能过期,应该每次从稳定 URL 重新解析 +- 不要自行替换 URL 中的 `app`、`dataSource`、`collection`、`id` 或扩展名。路径与文件记录不一致时会被拒绝 +- 反向代理需要把 `APP_PUBLIC_PATH` 下的 `/files/` 路径转发到 NocoBase。使用子路径部署时,还应保留根路径 `/files/` 的兼容转发规则。使用 NocoBase CLI 生成的代理配置时会自动包含这些规则 +- 页面跨源访问 API 的部署(配置了指向其他源的 `API_BASE_URL`)需要把页面来源加入 `CORS_ORIGIN_WHITELIST`,否则登录 cookie 无法写入,稳定 URL 会因缺少凭证返回 `403`,详见[环境变量](../get-started/installation/env.md#api_base_url) +- 部署多个彼此独立的 NocoBase 服务时,应为每个服务使用不同的 `hostname`,不要只通过端口区分。浏览器 cookie 不按端口隔离,详细说明见[生产环境部署](../get-started/deployment/production.md) +- 同一个 NocoBase 部署环境内的子应用会按应用名区分 cookie,不需要单独配置 `hostname`;不过另一个端口上的独立服务如果包含同名主应用或子应用,仍需要通过不同的 `hostname` 隔离 +- 如果通过 ``、`