feat(nginx): configure client max body size and update timeouts for API proxy
Deployment Config CI / Deployment Config (push) Successful in 15s
Frontend CI / Frontend (push) Successful in 2m16s

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
This commit is contained in:
2026-05-31 15:01:14 +08:00
parent 9dfc3746c3
commit de9c52be6d
7 changed files with 130 additions and 7 deletions
+3 -2
View File
@@ -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?)$ {
+44
View File
@@ -0,0 +1,44 @@
export async function copyTextToClipboard(text: string): Promise<void> {
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 })
}
}
@@ -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) {
@@ -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
+9
View File
@@ -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
+49
View File
@@ -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;
}
+3
View File
@@ -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