From de9c52be6d6af48b89cf4cb00109cbc2926f2ece Mon Sep 17 00:00:00 2001 From: liangxu Date: Sun, 31 May 2026 15:01:14 +0800 Subject: [PATCH] feat(nginx): configure client max body size and update timeouts for API proxy feat(clipboard): implement copyTextToClipboard utility function feat(desktop): add upload timeout for desktop client release API feat(k3s): add nginx config and update kustomization for ops-web --- apps/ops-web/nginx.conf | 5 +- apps/ops-web/src/lib/clipboard.ts | 44 +++++++++++++++++ .../src/lib/desktop-client-releases.ts | 6 ++- .../src/views/DesktopClientReleasesView.vue | 21 ++++++-- deploy/k3s/apps.yaml | 9 ++++ deploy/k3s/config/ops-web-nginx.conf | 49 +++++++++++++++++++ deploy/k3s/kustomization.yaml | 3 ++ 7 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 apps/ops-web/src/lib/clipboard.ts create mode 100644 deploy/k3s/config/ops-web-nginx.conf diff --git a/apps/ops-web/nginx.conf b/apps/ops-web/nginx.conf index a15e770..a8ee2a6 100644 --- a/apps/ops-web/nginx.conf +++ b/apps/ops-web/nginx.conf @@ -8,6 +8,7 @@ real_ip_recursive on; server { listen 80; server_name _; + client_max_body_size 600M; root /usr/share/nginx/html; index index.html; @@ -30,8 +31,8 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 120s; - proxy_send_timeout 120s; + proxy_read_timeout 900s; + proxy_send_timeout 900s; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { diff --git a/apps/ops-web/src/lib/clipboard.ts b/apps/ops-web/src/lib/clipboard.ts new file mode 100644 index 0000000..ae5c9e1 --- /dev/null +++ b/apps/ops-web/src/lib/clipboard.ts @@ -0,0 +1,44 @@ +export async function copyTextToClipboard(text: string): Promise { + if (!text) { + throw new Error('clipboard_empty_text') + } + + if (window.isSecureContext && navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(text) + return + } catch { + // Fall back to execCommand for browsers that expose clipboard but deny it. + } + } + + if (copyTextWithSelection(text)) { + return + } + + throw new Error('clipboard_write_failed') +} + +function copyTextWithSelection(text: string): boolean { + const textarea = document.createElement('textarea') + const activeElement = document.activeElement instanceof HTMLElement ? document.activeElement : null + + textarea.value = text + textarea.setAttribute('readonly', '') + textarea.style.position = 'fixed' + textarea.style.top = '0' + textarea.style.left = '-9999px' + textarea.style.opacity = '0' + + document.body.appendChild(textarea) + textarea.focus() + textarea.select() + textarea.setSelectionRange(0, text.length) + + try { + return document.execCommand('copy') + } finally { + document.body.removeChild(textarea) + activeElement?.focus({ preventScroll: true }) + } +} diff --git a/apps/ops-web/src/lib/desktop-client-releases.ts b/apps/ops-web/src/lib/desktop-client-releases.ts index 4680227..ce0b12b 100644 --- a/apps/ops-web/src/lib/desktop-client-releases.ts +++ b/apps/ops-web/src/lib/desktop-client-releases.ts @@ -75,6 +75,8 @@ export interface DesktopClientReleaseDownloadURLResult { expires_in?: number | null } +const DESKTOP_CLIENT_RELEASE_UPLOAD_TIMEOUT_MS = 15 * 60 * 1000 + export const platformOptions = [ { label: 'macOS', value: 'darwin' }, { label: 'Windows', value: 'win32' }, @@ -138,7 +140,9 @@ export const desktopClientReleaseApi = { code: number message: string data: DesktopClientReleaseUploadResult - }>('/desktop-client/releases/upload', form) + }>('/desktop-client/releases/upload', form, { + timeout: DESKTOP_CLIENT_RELEASE_UPLOAD_TIMEOUT_MS, + }) return response.data.data }, resolveDownloadURL(id: number) { diff --git a/apps/ops-web/src/views/DesktopClientReleasesView.vue b/apps/ops-web/src/views/DesktopClientReleasesView.vue index e2b53de..d215ac4 100644 --- a/apps/ops-web/src/views/DesktopClientReleasesView.vue +++ b/apps/ops-web/src/views/DesktopClientReleasesView.vue @@ -357,6 +357,7 @@ import { type DesktopReleasePlatform, type DesktopReleaseSource, } from '@/lib/desktop-client-releases' +import { copyTextToClipboard } from '@/lib/clipboard' import { showOpsError } from '@/lib/errors' const enabledOptions = [ @@ -763,19 +764,31 @@ async function deleteRelease(row: DesktopClientRelease) { } async function copyDownloadURL(row: DesktopClientRelease) { + let downloadURL = '' try { const result = await desktopClientReleaseApi.resolveDownloadURL(row.id) - await navigator.clipboard.writeText(result.download_url) - message.success('下载地址已复制') + downloadURL = result.download_url } catch (error) { showOpsError(error) + return + } + + try { + await copyTextToClipboard(downloadURL) + message.success('下载地址已复制') + } catch { + message.error('复制失败,请在接口返回中手动复制下载地址') } } async function copyFormDownloadURL() { if (form.download_url) { - await navigator.clipboard.writeText(form.download_url) - message.success('下载地址已复制') + try { + await copyTextToClipboard(form.download_url) + message.success('下载地址已复制') + } catch { + message.error('复制失败,请手动复制下载地址') + } return } if (!editingRow.value?.id) return diff --git a/deploy/k3s/apps.yaml b/deploy/k3s/apps.yaml index b71876f..043d8ee 100644 --- a/deploy/k3s/apps.yaml +++ b/deploy/k3s/apps.yaml @@ -684,3 +684,12 @@ spec: limits: cpu: 500m memory: 256Mi + volumeMounts: + - name: ops-web-nginx-config + mountPath: /etc/nginx/http.d/default.conf + subPath: default.conf + readOnly: true + volumes: + - name: ops-web-nginx-config + configMap: + name: ops-web-nginx-config diff --git a/deploy/k3s/config/ops-web-nginx.conf b/deploy/k3s/config/ops-web-nginx.conf new file mode 100644 index 0000000..a8ee2a6 --- /dev/null +++ b/deploy/k3s/config/ops-web-nginx.conf @@ -0,0 +1,49 @@ +set_real_ip_from 127.0.0.1; +set_real_ip_from 10.42.0.0/16; +set_real_ip_from 10.43.0.0/16; +set_real_ip_from 172.16.0.0/12; +real_ip_header X-Forwarded-For; +real_ip_recursive on; + +server { + listen 80; + server_name _; + client_max_body_size 600M; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + # Never cache the SPA entrypoint — chunk hashes change every deploy and a + # stale index.html keeps users stuck on 404'd chunks even after reload. + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate" always; + expires -1; + } + + location /api { + proxy_pass http://ops-api:8090; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 900s; + proxy_send_timeout 900s; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + gzip on; + gzip_static on; + gzip_vary on; + gzip_types text/plain text/css application/json application/javascript text/xml; + + brotli_static on; +} diff --git a/deploy/k3s/kustomization.yaml b/deploy/k3s/kustomization.yaml index 9f1e28d..d82416c 100644 --- a/deploy/k3s/kustomization.yaml +++ b/deploy/k3s/kustomization.yaml @@ -29,6 +29,9 @@ configMapGenerator: - config.yaml=config/config.yaml - prompts.yml=config/prompts.yml - ops-config.yaml=config/ops-config.yaml + - name: ops-web-nginx-config + files: + - default.conf=config/ops-web-nginx.conf images: - name: geo-rankly/migrate