Skip to content

Commit bcc1dbd

Browse files
committed
feat: add MCP settings and admin header actions
1 parent fdb0f3d commit bcc1dbd

15 files changed

Lines changed: 525 additions & 169 deletions

File tree

‎src/components/common/FileDetailModal.vue‎

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,8 @@
6565
</button>
6666
</div>
6767
<div v-else class="ml-2 flex flex-wrap gap-2">
68-
<a
69-
v-if="isHtmlPreviewable(record)"
70-
:href="getHtmlPreviewUrl(record)"
71-
target="_blank"
72-
rel="noopener noreferrer"
73-
class="rounded-xl px-4 py-2 text-sm font-semibold transition duration-300"
74-
:class="isDarkMode ? 'bg-zinc-200 text-zinc-950 hover:bg-zinc-100' : 'bg-zinc-800 text-white hover:bg-zinc-900'"
75-
>
76-
{{ t('fileRecord.preview') }}
77-
</a>
7868
<button
79-
v-else-if="isPreviewable(record)"
69+
v-if="isPreviewable(record)"
8070
type="button"
8171
class="rounded-xl px-4 py-2 text-sm font-semibold transition duration-300"
8272
:class="isDarkMode ? 'bg-zinc-200 text-zinc-950 hover:bg-zinc-100' : 'bg-zinc-800 text-white hover:bg-zinc-900'"
@@ -138,7 +128,7 @@ import { useI18n } from 'vue-i18n'
138128
import { FileIcon, CalendarIcon, HardDriveIcon, DownloadIcon } from 'lucide-vue-next'
139129
import QRCode from 'qrcode.vue'
140130
import type { ReceivedFileRecord } from '@/types'
141-
import { buildAbsoluteUrl, buildDownloadUrl, buildReceivedRecordQrValue } from '@/utils/share-url'
131+
import { buildDownloadUrl, buildReceivedRecordQrValue } from '@/utils/share-url'
142132
143133
interface Props {
144134
visible: boolean
@@ -160,13 +150,6 @@ const getDownloadUrl = (record: ReceivedFileRecord) => {
160150
return buildDownloadUrl(record.downloadUrl)
161151
}
162152
163-
const isHtmlPreviewable = (record: ReceivedFileRecord) => {
164-
const extension = record.filename.split('.').pop()?.toLowerCase() || ''
165-
return Boolean(record.downloadUrl) && ['html', 'htm'].includes(extension)
166-
}
167-
168-
const getHtmlPreviewUrl = (record: ReceivedFileRecord) =>
169-
buildAbsoluteUrl(`/share/preview/html/${encodeURIComponent(record.code)}`)
170153
171154
const getQRCodeValue = (record: ReceivedFileRecord) => {
172155
return buildReceivedRecordQrValue(record)
@@ -193,8 +176,6 @@ const isPreviewable = (record: ReceivedFileRecord) => {
193176
'm4a',
194177
'flac',
195178
'pdf',
196-
'html',
197-
'htm'
198179
].includes(extension)
199180
}
200181
</script>

‎src/components/common/MediaPreviewModal.vue‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ import { XIcon } from 'lucide-vue-next'
8787
import { useI18n } from 'vue-i18n'
8888
import { useInjectedDarkMode } from '@/composables'
8989
import type { ReceivedFileRecord } from '@/types'
90-
import { buildAbsoluteUrl, buildDownloadUrl } from '@/utils/share-url'
90+
import { buildDownloadUrl } from '@/utils/share-url'
9191
9292
interface Props {
9393
visible: boolean
@@ -106,12 +106,7 @@ const extension = computed(() => props.record?.filename.split('.').pop()?.toLowe
106106
const downloadUrl = computed(() =>
107107
props.record?.downloadUrl ? buildDownloadUrl(props.record.downloadUrl) : ''
108108
)
109-
const previewUrl = computed(() => {
110-
if (['html', 'htm'].includes(extension.value) && props.record?.code) {
111-
return buildAbsoluteUrl(`/share/preview/html/${encodeURIComponent(props.record.code)}`)
112-
}
113-
return downloadUrl.value
114-
})
109+
const previewUrl = computed(() => downloadUrl.value)
115110
const mediaType = computed(() => {
116111
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'].includes(extension.value)) return 'image'
117112
if (['mp4', 'webm', 'ogg', 'mov', 'm4v'].includes(extension.value)) return 'video'

‎src/composables/index.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@ export { useRouteLoading } from './useRouteLoading'
1111
export { useSendFlow } from './useSendFlow'
1212
export { useLocalShare } from './useLocalShare'
1313
export { useSystemConfig } from './useSystemConfig'
14+
export { useMcpSettings } from './useMcpSettings'
1415
export { useTheme } from './useTheme'
16+
export { provideAdminHeaderActions, useAdminHeaderActions } from './useAdminHeaderActions'
17+
export type { AdminHeaderAction, AdminHeaderConfig } from './useAdminHeaderActions'
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {
2+
inject,
3+
onUnmounted,
4+
provide,
5+
shallowRef,
6+
watch,
7+
type Component,
8+
type ComputedRef,
9+
type InjectionKey
10+
} from 'vue'
11+
12+
export interface AdminHeaderAction {
13+
key: string
14+
label: string
15+
icon: Component
16+
onClick: () => void
17+
variant?: 'primary' | 'secondary' | 'danger' | 'success' | 'outline'
18+
loading?: boolean
19+
disabled?: boolean
20+
}
21+
22+
export interface AdminHeaderConfig {
23+
subtitle?: string
24+
status?: string
25+
statusTone?: 'muted' | 'warning'
26+
actions: AdminHeaderAction[]
27+
}
28+
29+
interface AdminHeaderRegistry {
30+
state: ReturnType<typeof shallowRef<AdminHeaderConfig | null>>
31+
register: (owner: symbol, config: AdminHeaderConfig) => void
32+
clear: (owner: symbol) => void
33+
}
34+
35+
const ADMIN_HEADER_KEY: InjectionKey<AdminHeaderRegistry> = Symbol('admin-header-actions')
36+
37+
export function provideAdminHeaderActions() {
38+
const state = shallowRef<AdminHeaderConfig | null>(null)
39+
let currentOwner: symbol | null = null
40+
41+
provide(ADMIN_HEADER_KEY, {
42+
state,
43+
register(owner, config) {
44+
currentOwner = owner
45+
state.value = config
46+
},
47+
clear(owner) {
48+
if (currentOwner === owner) {
49+
currentOwner = null
50+
state.value = null
51+
}
52+
}
53+
})
54+
55+
return state
56+
}
57+
58+
export function useAdminHeaderActions(config: ComputedRef<AdminHeaderConfig>) {
59+
const registry = inject(ADMIN_HEADER_KEY, null)
60+
const owner = Symbol('admin-header-owner')
61+
const stop = watch(
62+
config,
63+
(value) => registry?.register(owner, value),
64+
{ immediate: true }
65+
)
66+
67+
onUnmounted(() => {
68+
stop()
69+
registry?.clear(owner)
70+
})
71+
}

‎src/composables/useMcpSettings.ts‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { ref } from 'vue'
2+
import { useI18n } from 'vue-i18n'
3+
import { ConfigService } from '@/services'
4+
import { useAlertStore } from '@/stores/alertStore'
5+
import { copyToClipboard } from '@/utils/clipboard'
6+
import { getErrorMessage } from '@/utils/common'
7+
8+
export function useMcpSettings() {
9+
const { t } = useI18n()
10+
const alertStore = useAlertStore()
11+
const isLoading = ref(false)
12+
const isSaving = ref(false)
13+
const requireToken = ref(0)
14+
const guestUploadOpen = ref(1)
15+
16+
const loadSettings = async () => {
17+
isLoading.value = true
18+
try {
19+
const response = await ConfigService.getConfig()
20+
if (response.code !== 200 || !response.detail) {
21+
throw new Error(t('admin.mcp.loadFailed'))
22+
}
23+
requireToken.value = response.detail.mcp_require_token === 1 ? 1 : 0
24+
guestUploadOpen.value = response.detail.open_upload === 0 ? 0 : 1
25+
} catch (error) {
26+
alertStore.showAlert(getErrorMessage(error, t('admin.mcp.loadFailed')), 'error')
27+
} finally {
28+
isLoading.value = false
29+
}
30+
}
31+
32+
const toggleTokenRequirement = async () => {
33+
if (isSaving.value || isLoading.value) return
34+
const nextValue = requireToken.value === 1 ? 0 : 1
35+
isSaving.value = true
36+
try {
37+
const response = await ConfigService.updateConfig({ mcp_require_token: nextValue })
38+
if (response.code !== 200) throw new Error(t('admin.mcp.saveFailed'))
39+
requireToken.value = nextValue
40+
alertStore.showAlert(t('admin.mcp.saveSuccess'), 'success')
41+
} catch (error) {
42+
alertStore.showAlert(getErrorMessage(error, t('admin.mcp.saveFailed')), 'error')
43+
} finally {
44+
isSaving.value = false
45+
}
46+
}
47+
48+
const copyText = (value: string) =>
49+
copyToClipboard(value, {
50+
successMsg: t('admin.mcp.copySuccess'),
51+
errorMsg: t('admin.mcp.copyFailed'),
52+
notify: (message, type) => alertStore.showAlert(message, type)
53+
})
54+
55+
return {
56+
isLoading,
57+
isSaving,
58+
requireToken,
59+
guestUploadOpen,
60+
loadSettings,
61+
toggleTokenRequirement,
62+
copyText
63+
}
64+
}

‎src/constants/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export const ROUTES = {
7171
DASHBOARD: '/admin/dashboard',
7272
FILE_MANAGE: '/admin/files',
7373
LOCAL_SHARE: '/admin/local',
74+
MCP: '/admin/mcp',
7475
SETTINGS: '/admin/settings'
7576
} as const
7677

@@ -82,6 +83,7 @@ export const ROUTE_NAMES = {
8283
DASHBOARD: 'Dashboard',
8384
FILE_MANAGE: 'FileManage',
8485
LOCAL_SHARE: 'LocalShare',
86+
MCP: 'Mcp',
8587
SETTINGS: 'Settings'
8688
} as const
8789

‎src/i18n/locales/en-US.ts‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,36 @@ export default {
5858
loggedIn: 'Signed in, open admin console',
5959
loggedOut: 'Sign in to admin console'
6060
},
61+
mcp: {
62+
title: 'MCP Quick Share',
63+
subtitle: 'Connect FileCodeBox to an AI client to create text and file shares through chat.',
64+
refresh: 'Refresh settings',
65+
endpointLabel: 'MCP endpoint',
66+
transport: 'Transport: Streamable HTTP',
67+
copyEndpoint: 'Copy endpoint',
68+
securityTitle: 'Access control',
69+
securityHelp: 'When enabled, MCP uploads require an admin login token. If guest uploads are disabled, the server always requires a token.',
70+
requireToken: 'Require admin token',
71+
tokenRequiredStatus: 'MCP uploads currently require an admin Bearer token.',
72+
tokenOptionalStatus: 'Tokenless uploads are allowed only while guest uploads are enabled.',
73+
shareTextTitle: 'Share text',
74+
shareTextHelp: 'Create a FileCodeBox text share and return its retrieval link.',
75+
shareFileTitle: 'Share a file',
76+
shareFileHelp: 'Upload Base64 file content within the site file-type, size, and expiration limits.',
77+
aiSetupTitle: 'Configure with AI',
78+
aiSetupHelp: 'Copy setup instructions and paste them into your AI assistant to configure its MCP client.',
79+
aiSetupButton: 'Copy AI setup instructions',
80+
aiSetupFallback: 'Clipboard unavailable. Select and copy the setup text below.',
81+
aiSetupPrivacy: 'Your admin token is never copied. If authentication is required, let the AI ask you for it; do not send it to an untrusted service.',
82+
aiAuthRequired: 'Authorization: Bearer with an admin login token is required',
83+
aiAuthOptional: 'No token required unless guest uploads are disabled',
84+
aiSetupPrompt: 'Please connect FileCodeBox MCP to my current AI client. Use Streamable HTTP and the client format it currently supports.\nEndpoint: {endpoint}\nAuthentication: {authentication}\nTools: share_text (create a text share), share_file (upload a Base64 file and create a share). If a token is needed, ask me for my admin login token and set it only as Authorization: Bearer. Do not guess or reveal the token.',
85+
loadFailed: 'Failed to load MCP settings',
86+
saveFailed: 'Failed to save MCP settings',
87+
saveSuccess: 'MCP settings saved',
88+
copySuccess: 'Copied to clipboard',
89+
copyFailed: 'Could not copy; check browser clipboard permissions'
90+
},
6191
dashboard: {
6292
title: 'Dashboard',
6393
totalFiles: 'Total Files',
@@ -752,8 +782,6 @@ export default {
752782
allowedFileTypesPlaceholder: '* or .rar, .zip, image/*',
753783
allowedFileTypesHelp:
754784
'Separate multiple types with commas. * allows all; extensions and MIME wildcards are supported.',
755-
mcpRequireToken: 'Require admin token for MCP uploads',
756-
mcpRequireTokenHelp: 'When enabled, pass your admin login token as Authorization: Bearer when calling /mcp.',
757785
codeGenerateType: 'Passcode Rule',
758786
codeGenerateNumber: 'Numeric Passcode',
759787
codeGenerateSecret: 'Secret Passcode',

‎src/i18n/locales/zh-CN.ts‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,36 @@ export default {
5858
loggedIn: '已登录,进入管理后台',
5959
loggedOut: '登录管理后台'
6060
},
61+
mcp: {
62+
title: 'MCP 快捷分享',
63+
subtitle: '把 FileCodeBox 接入 AI 客户端,用对话快速创建文本或文件分享。',
64+
refresh: '刷新配置',
65+
endpointLabel: 'MCP 接口地址',
66+
transport: '传输协议:Streamable HTTP',
67+
copyEndpoint: '复制地址',
68+
securityTitle: '访问控制',
69+
securityHelp: '开启后,调用 MCP 上传工具需要管理员登录 Token。关闭游客上传时,服务仍会强制验证 Token。',
70+
requireToken: '要求管理员 Token',
71+
tokenRequiredStatus: '当前 MCP 上传需要管理员 Bearer Token。',
72+
tokenOptionalStatus: '当前允许免 Token 上传(仅在已开启游客上传时生效)。',
73+
shareTextTitle: '分享文本',
74+
shareTextHelp: 'AI 可将文本创建为 FileCodeBox 分享,并返回取件链接。',
75+
shareFileTitle: '分享文件',
76+
shareFileHelp: 'AI 可上传 Base64 文件内容,遵循本站类型、大小和过期限制。',
77+
aiSetupTitle: '一键让 AI 配置',
78+
aiSetupHelp: '复制连接说明,粘贴给你使用的 AI 助手,让它按当前客户端格式配置 MCP。',
79+
aiSetupButton: '复制给 AI 配置',
80+
aiSetupFallback: '剪贴板不可用,可在下方文本框中手动选择并复制。',
81+
aiSetupPrivacy: '不会复制管理员 Token。需要认证时,AI 应向你索取,不要把 Token 发给不可信的服务。',
82+
aiAuthRequired: '需要 Authorization: Bearer 管理员登录 Token',
83+
aiAuthOptional: '当前无需 Token;若游客上传关闭则仍需管理员 Token',
84+
aiSetupPrompt: '请帮我把 FileCodeBox MCP 接入当前 AI 客户端。请使用 Streamable HTTP,并按客户端当前支持的格式配置。\n服务地址:{endpoint}\n认证要求:{authentication}\n工具:share_text(创建文本分享)、share_file(上传 Base64 文件并创建分享)。如果需要 Token,请先向我索取管理员登录 Token,只设置为 Authorization: Bearer,不要猜测或输出 Token。',
85+
loadFailed: '读取 MCP 配置失败',
86+
saveFailed: '保存 MCP 配置失败',
87+
saveSuccess: 'MCP 配置已保存',
88+
copySuccess: '已复制到剪贴板',
89+
copyFailed: '复制失败,请检查浏览器剪贴板权限'
90+
},
6191
dashboard: {
6292
title: '仪表盘',
6393
totalFiles: '总文件数',
@@ -771,8 +801,6 @@ export default {
771801
allowedFileTypes: '允许上传类型',
772802
allowedFileTypesPlaceholder: '* 或 .rar, .zip, image/*',
773803
allowedFileTypesHelp: '多个类型用英文逗号分隔,* 表示不限制;支持扩展名和 MIME 通配。',
774-
mcpRequireToken: 'MCP 上传需要管理员 Token',
775-
mcpRequireTokenHelp: '启用后调用 /mcp 上传时,需在 Authorization: Bearer 中提供管理员登录 Token。',
776804
codeGenerateType: '取件码规则',
777805
codeGenerateNumber: '数字取件码',
778806
codeGenerateSecret: 'Secret 取件码',

0 commit comments

Comments
 (0)