{ "info": { "id": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "slug": "lucky-pixel", "projectID": "0d4685396cc353e0f47ffbe712b4e8d0545f5bad", "directory": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "title": "Forwarding import, ny-compatible format, no entry point; switch to tunnel selection", "version": "1.2.15", "summary": { "additions": 1305, "deletions": 18, "files": 7 }, "time": { "created": 1772527573838, "updated": 1772527948300 } }, "messages": [ { "info": { "role": "user", "time": { "created": 1772527574015 }, "summary": { "diffs": [] }, "agent": "plan", "model": { "providerID": "api", "modelID": "gpt-5.3-codex" }, "variant": "xhigh", "id": "msg_cb2e05be3001YeVD71ZRqZLLrP", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "text", "text": "转发导入,ny兼容格式,不要选择入口,改成选择隧道,修改兼容一下", "id": "prt_cb2e05be3002bDMX2wqUxIfAgw", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e05be3001YeVD71ZRqZLLrP" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527574022, "completed": 1772527585255 }, "parentID": "msg_cb2e05be3001YeVD71ZRqZLLrP", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "plan", "agent": "plan", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 14475, "input": 14108, "output": 367, "reasoning": 198, "cache": { "read": 0, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e05c06002DHfMlzdcLNz4Z5", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "id": "prt_cb2e07f93001mifuPW7o3hXrSO", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e05c06002DHfMlzdcLNz4Z5" }, { "type": "reasoning", "text": "**Inspecting code for forwarding import**", "time": { "start": 1772527583123, "end": 1772527585170 }, "id": "prt_cb2e07f93002MtjpN3koc3B7Li", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e05c06002DHfMlzdcLNz4Z5" }, { "type": "tool", "callID": "call_rtCIlNXtQJoOQCg7J2WaxggG", "tool": "glob", "state": { "status": "completed", "input": { "pattern": "**/*import*", "path": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src" }, "output": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts\n/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts", "title": "vite-frontend/src", "metadata": { "count": 2, "truncated": false }, "time": { "start": 1772527585159, "end": 1772527585183 } }, "id": "prt_cb2e08771001Z79fK99lQ1aa3n", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e05c06002DHfMlzdcLNz4Z5" }, { "type": "tool", "callID": "call_QnfXc4ZCG9o06YaZcwm8rtnN", "tool": "grep", "state": { "status": "completed", "input": { "pattern": "ny|NY|兼容|导入|import", "path": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src", "include": "*.{ts,tsx,js,jsx}" }, "output": "Found 662 matches (showing first 100)\n/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx:\n Line 1: import type { SpeedLimitApiItem } from \"@/api/types\";\n Line 3: import { useState, useEffect, useMemo, useRef } from \"react\";\n Line 4: import toast from \"react-hot-toast\";\n Line 5: import {\n Line 15: import {\n Line 22: import { useSortable } from \"@dnd-kit/sortable\";\n Line 23: import { CSS } from \"@dnd-kit/utilities\";\n Line 25: import { SearchBar } from \"@/components/search-bar\";\n Line 26: import { AnimatedPage } from \"@/components/animated-page\";\n Line 27: import { Card, CardBody, CardHeader } from \"@/shadcn-bridge/heroui/card\";\n Line 28: import { Button } from \"@/shadcn-bridge/heroui/button\";\n Line 29: import { Input } from \"@/shadcn-bridge/heroui/input\";\n Line 30: import { Textarea } from \"@/shadcn-bridge/heroui/input\";\n Line 31: import { Select, SelectItem } from \"@/shadcn-bridge/heroui/select\";\n Line 32: import {\n Line 40: import {\n Line 47: import { Chip } from \"@/shadcn-bridge/heroui/chip\";\n Line 48: import { Spinner } from \"@/shadcn-bridge/heroui/spinner\";\n Line 49: import { Switch } from \"@/shadcn-bridge/heroui/switch\";\n Line 50: import { Alert } from \"@/shadcn-bridge/heroui/alert\";\n Line 51: import { Checkbox } from \"@/shadcn-bridge/heroui/checkbox\";\n Line 52: import {\n Line 71: import {\n Line 78: import {\n Line 83: import { diagnoseForwardStream } from \"@/api/diagnosis-stream\";\n Line 84: import {\n Line 90: import {\n Line 93: } from \"@/pages/forward/import-format\";\n Line 94: import { buildForwardOrder, FORWARD_ORDER_KEY } from \"@/pages/forward/order\";\n Line 95: import { PageLoadingState } from \"@/components/page-state\";\n Line 96: import { useMobileBreakpoint } from \"@/hooks/useMobileBreakpoint\";\n Line 97: import { useLocalStorageState } from \"@/hooks/use-local-storage-state\";\n Line 98: import { saveOrder } from \"@/utils/order-storage\";\n Line 99: import { JwtUtil } from \"@/utils/jwt\";\n Line 560: // 导入相关状态\n Line 561: type ImportFormat = \"flvx\" | \"ny\";\n Line 562: const [importModalOpen, setImportModalOpen] = useState(false);\n Line 563: const [importData, setImportData] = useState(\"\");\n Line 564: const [importLoading, setImportLoading] = useState(false);\n Line 565: const [importFormat, setImportFormat] = useState(\"flvx\");\n Line 569: const [importResults, setImportResults] = useState<\n Line 1144: forwardsRes.data?.map((forward: any) => ({\n Line 1785: // 导入转发数据\n Line 1793: // 执行导入\n Line 1795: if (!importData.trim()) {\n Line 1796: toast.error(\"请输入要导入的数据\");\n Line 1802: toast.error(\"请选择要导入的隧道\");\n Line 1811: if (importFormat === \"ny\") {\n Line 1812: const parsedItems = parseNyFormatData(importData);\n Line 1815: toast.error(\"未解析到有效的ny格式数据\");\n Line 1850: const nyForwardInput = convertNyItemToForwardInput(parsedNyItem);\n Line 1854: name: nyForwardInput.name,\n Line 1856: inPort: nyForwardInput.inPort,\n Line 1857: remoteAddr: nyForwardInput.remoteAddr,\n Line 1858: strategy: nyForwardInput.strategy,\n Line 1867: forwardName: nyForwardInput.name,\n Line 1893: const lines = importData\n Line 2008: toast.success(\"导入执行完成\");\n Line 2012: toast.error(\"导入过程中发生错误\");\n Line 2808: }: any) => {\n Line 2904: color={strategyDisplay.color as any}\n Line 3015: }: any) => {\n Line 3121: color={strategyDisplay.color as any}\n Line 3216: const renderForwardCard = (forward: Forward, listeners?: any) => {\n Line 3272: color={statusDisplay.color as any}\n Line 3371: color={strategyDisplay.color as any}\n Line 3674: {/* 导入按钮 */}\n Line 3681: 导入\n Line 4599: {/* 导入数据模态框 */}\n Line 4602: isOpen={importModalOpen}\n Line 4610:

导入转发数据

\n Line 4611: {importFormat === \"flvx\" ? (\n Line 4623: ny格式:JSON对象,支持多个目标地址(负载均衡)\n Line 4636: label=\"导入格式\"\n Line 4637: placeholder=\"请选择导入格式\"\n Line 4638: selectedKeys={[importFormat]}\n Line 4654: \n Line 4655: ny格式(JSON)\n Line 4662: label=\"选择导入隧道\"\n Line 4663: placeholder=\"请选择要导入的隧道\"\n Line 4693: label=\"导入数据\"\n Line 4697: importFormat === \"flvx\"\n Line 4698: ? \"请输入要导入的转发数据,格式:目标地址|转发名称|入口端口\"\n Line 4699: : '请输入ny格式数据,每行一个JSON对象,如:{\"dest\":[\"1.2.3.4:80\"],\"listen_port\":8080,\"name\":\"转发1\"}'\n Line 4701: value={importData}\n Line 4706: {/* 导入结果 */}\n Line 4707: {importResults.length > 0 && (\n Line 4710:

导入结果

\n Line 4712: 成功:{importResults.filter((r) => r.success).length} /\n Line 4713: 总计:{importResults.length}\n Line 4724: {importResults.map((result, index) => (\n Line 4803: isDisabled={!importData.trim() || !selectedTunnelForImport}\n Line 4804: isLoading={importLoading}\n Line 4807: 开始导入\n Line 5086: color={quality.color as any}\n Line 5267: color={quality.color as any}\n\n/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts:\n Line 1: import test from \"node:test\";\n Line 2: import assert from \"node:assert/strict\";\n Line 4: import {\n Line 7: } from \"./import-format.ts\";\n\n(Results truncated: showing 100 of 662 matches (562 hidden). Consider using a more specific path or pattern.)", "title": "ny|NY|兼容|导入|import", "metadata": { "matches": 662, "truncated": true }, "time": { "start": 1772527585167, "end": 1772527585191 } }, "id": "prt_cb2e0878a001KkGgXQA1u6Zpzj", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e05c06002DHfMlzdcLNz4Z5" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "cost": 0, "tokens": { "total": 14475, "input": 14108, "output": 367, "reasoning": 198, "cache": { "read": 0, "write": 0 } }, "id": "prt_cb2e087a8001jzWC7ktvj3iN1E", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e05c06002DHfMlzdcLNz4Z5" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527585255, "completed": 1772527590551 }, "parentID": "msg_cb2e05be3001YeVD71ZRqZLLrP", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "plan", "agent": "plan", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 16426, "input": 16178, "output": 248, "reasoning": 85, "cache": { "read": 0, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e087e700227pxWUXR9DsDdN", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "id": "prt_cb2e093f4001hmGM1xpgQqtyyV", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e087e700227pxWUXR9DsDdN" }, { "type": "reasoning", "text": "**Scoping inspection and plan approach**", "time": { "start": 1772527588340, "end": 1772527590474 }, "id": "prt_cb2e093f40026mxIntikIwhGtf", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e087e700227pxWUXR9DsDdN" }, { "type": "tool", "callID": "call_MOBp1vg7krJ10ww72NjeZ27d", "tool": "read", "state": { "status": "completed", "input": { "filePath": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts", "offset": 1, "limit": 400 }, "output": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts\nfile\n1: export interface NyImportItem {\n2: dest: string[];\n3: listen_port: number;\n4: name: string;\n5: }\n6: \n7: export interface ParsedNyImportLine {\n8: line: string;\n9: parsed?: NyImportItem;\n10: error?: string;\n11: }\n12: \n13: const ADDRESS_PATTERN = /^[^:]+:\\d+$/;\n14: \n15: const isValidListenPort = (value: unknown): value is number => {\n16: return (\n17: typeof value === \"number\" &&\n18: Number.isFinite(value) &&\n19: value >= 1 &&\n20: value <= 65535\n21: );\n22: };\n23: \n24: const validateNyItem = (line: string, value: unknown): ParsedNyImportLine => {\n25: if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n26: return { line, error: \"JSON结构错误\" };\n27: }\n28: \n29: const item = value as Record;\n30: const dest = item.dest;\n31: const listenPort = item.listen_port;\n32: const name = item.name;\n33: \n34: if (!Array.isArray(dest) || dest.length === 0) {\n35: return { line, error: \"dest数组为空或格式错误\" };\n36: }\n37: \n38: if (typeof name !== \"string\" || name.trim() === \"\") {\n39: return { line, error: \"name不能为空\" };\n40: }\n41: \n42: if (!isValidListenPort(listenPort)) {\n43: return { line, error: \"listen_port必须为1-65535之间的数字\" };\n44: }\n45: \n46: const normalizedDest = dest.map((itemValue) =>\n47: typeof itemValue === \"string\" ? itemValue.trim() : \"\",\n48: );\n49: \n50: if (normalizedDest.some((itemValue) => itemValue === \"\")) {\n51: return { line, error: \"dest中包含空地址\" };\n52: }\n53: \n54: const invalid = normalizedDest.find(\n55: (itemValue) => !ADDRESS_PATTERN.test(itemValue),\n56: );\n57: \n58: if (invalid) {\n59: return { line, error: `目标地址格式错误: ${invalid}` };\n60: }\n61: \n62: return {\n63: line,\n64: parsed: {\n65: dest: normalizedDest,\n66: listen_port: listenPort,\n67: name: name.trim(),\n68: },\n69: };\n70: };\n71: \n72: const splitConcatenatedJsonObjects = (input: string): string[] => {\n73: const result: string[] = [];\n74: let depth = 0;\n75: let start = -1;\n76: let inString = false;\n77: let escaping = false;\n78: \n79: for (let i = 0; i < input.length; i += 1) {\n80: const char = input[i];\n81: \n82: if (escaping) {\n83: escaping = false;\n84: continue;\n85: }\n86: \n87: if (char === \"\\\\\") {\n88: escaping = true;\n89: continue;\n90: }\n91: \n92: if (char === '\"') {\n93: inString = !inString;\n94: continue;\n95: }\n96: \n97: if (inString) {\n98: continue;\n99: }\n100: \n101: if (char === \"{\") {\n102: if (depth === 0) {\n103: start = i;\n104: }\n105: depth += 1;\n106: continue;\n107: }\n108: \n109: if (char === \"}\") {\n110: depth -= 1;\n111: if (depth === 0 && start >= 0) {\n112: result.push(input.slice(start, i + 1));\n113: start = -1;\n114: }\n115: }\n116: }\n117: \n118: return result;\n119: };\n120: \n121: export const parseNyFormatData = (input: string): ParsedNyImportLine[] => {\n122: const trimmed = input.trim();\n123: \n124: if (!trimmed) {\n125: return [];\n126: }\n127: \n128: const parsedResults: ParsedNyImportLine[] = [];\n129: const objectChunks = splitConcatenatedJsonObjects(trimmed);\n130: \n131: if (objectChunks.length > 0) {\n132: objectChunks.forEach((chunk) => {\n133: try {\n134: const parsed = JSON.parse(chunk);\n135: \n136: parsedResults.push(validateNyItem(chunk, parsed));\n137: } catch {\n138: parsedResults.push({ line: chunk, error: \"JSON解析失败\" });\n139: }\n140: });\n141: \n142: return parsedResults;\n143: }\n144: \n145: trimmed\n146: .split(\"\\n\")\n147: .map((line) => line.trim())\n148: .filter((line) => line !== \"\")\n149: .forEach((line) => {\n150: try {\n151: const parsed = JSON.parse(line);\n152: \n153: parsedResults.push(validateNyItem(line, parsed));\n154: } catch {\n155: parsedResults.push({ line, error: \"JSON解析失败\" });\n156: }\n157: });\n158: \n159: return parsedResults;\n160: };\n161: \n162: export const convertNyItemToForwardInput = (item: NyImportItem) => {\n163: return {\n164: name: item.name.trim(),\n165: inPort: item.listen_port,\n166: remoteAddr: item.dest.join(\",\"),\n167: strategy: \"fifo\" as const,\n168: };\n169: };\n\n(End of file - total 169 lines)\n\n\n\nInstructions from: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/AGENTS.md\n# VITE FRONTEND KNOWLEDGE BASE\n\n**Generated:** Thu Feb 26 2026\n**Commit:** 21008cc\n**Branch:** main\n**Tag:** 2.1.5-rc15\n\n## OVERVIEW\nWeb management console for FLVX.\n**Stack:** React 18, rolldown-vite, TypeScript, Tailwind CSS v4, shadcn/radix primitives with HeroUI-compatible bridge.\n\n## STRUCTURE\n```\nvite-frontend/\n├── src/\n│ ├── api/ # Axios wrapper + typed endpoint helpers\n│ ├── components/ui/ # shadcn/radix primitive components\n│ ├── shadcn-bridge/heroui/ # HeroUI-compatible facade (23 components)\n│ ├── pages/ # Route views + page modules (forward/node/tunnel)\n│ ├── hooks/ # H5/WebView/mobile hooks\n│ ├── styles/\n│ │ ├── globals.css # Base styles + imports tailwind-theme.pcss\n│ │ └── tailwind-theme.pcss # Tailwind v4 @theme inline semantic token mapping\n│ ├── App.tsx # Routes + ProtectedRoute + H5 layout selection\n│ ├── main.tsx # ReactDOM + BrowserRouter + Provider\n│ └── provider.tsx # Toast/theme/provider composition\n├── components.json # shadcn/ui config\n├── tailwind.config.js # Compatibility config for migration scaffolding\n├── vite.config.ts # base '/', host 0.0.0.0:3000; minify/treeshake disabled\n└── package.json\n```\n\n## WHERE TO LOOK\n| Task | Location | Notes |\n|------|----------|-------|\n| **Route definitions** | `src/App.tsx` | React Router v6 + ProtectedRoute |\n| **API Client/Auth header** | `src/api/network.ts` | Sends raw JWT in `Authorization` header |\n| **Login Flow** | `src/pages/index.tsx` | Calls `login()`, stores `localStorage.token` |\n| **Auth helpers** | `src/utils/auth.ts`, `src/utils/jwt.ts` | Role checks + token expiration parsing |\n| **UI bridge usage** | `src/shadcn-bridge/heroui/` | Import from bridge, not `@heroui/*` |\n| **Button parity mapping** | `src/shadcn-bridge/heroui/button.tsx` | Legacy `color`/`variant` mapped to shadcn classes |\n| **Semantic theme tokens** | `src/styles/tailwind-theme.pcss` | Restores classes like `bg-primary`, `border-input` |\n| **Theme wiring** | `src/styles/globals.css` | Must import `./tailwind-theme.pcss` |\n\n## CONVENTIONS\n- **Auth Header**: Use raw JWT token (no `Bearer` prefix).\n- **API Envelope**: Responses follow `{code, msg, data, ts}`.\n- **UI Imports**: Use `src/shadcn-bridge/heroui/*` in app pages/layouts for compatibility.\n- **Semantic Colors**: Keep `globals.css -> tailwind-theme.pcss` import intact or semantic classes break.\n- **Build profile**: `minify: false`, `treeshake: false` for debugging.\n- **Layout mode**: H5/mobile mode controlled by existing route/query and hook logic.\n\n## ANTI-PATTERNS\n- **DO NOT ADD** `Bearer` to auth header in frontend requests.\n- **DO NOT REINTRODUCE** `@heroui/*` or `@nextui-org/*` dependencies.\n- **DO NOT REMOVE** `src/styles/tailwind-theme.pcss` import from `src/styles/globals.css`.\n- **DO NOT ADD** frontend tests; no Vitest/Jest setup exists.\n\n## NOTES\n- Uses `rolldown-vite` (experimental Rust bundler) instead of standard Vite.\n- Build outputs are non-minified (debugging mode).\n- No test infrastructure exists (Vitest/Jest not configured).\n\n## COMMANDS\n```bash\ncd vite-frontend\nnpm run dev\nnpm run build\nnpm run lint\n```\n\n", "title": "vite-frontend/src/pages/forward/import-format.ts", "metadata": { "preview": "export interface NyImportItem {\n dest: string[];\n listen_port: number;\n name: string;\n}\n\nexport interface ParsedNyImportLine {\n line: string;\n parsed?: NyImportItem;\n error?: string;\n}\n\nconst ADDRESS_PATTERN = /^[^:]+:\\d+$/;\n\nconst isValidListenPort = (value: unknown): value is number => {\n return (\n typeof value === \"number\" &&\n Number.isFinite(value) &&\n value >= 1 &&\n value <= 65535", "truncated": false, "loaded": [ "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/AGENTS.md" ] }, "time": { "start": 1772527590435, "end": 1772527590443 } }, "id": "prt_cb2e09c1a001Wg1jNC1a2f1nxL", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e087e700227pxWUXR9DsDdN" }, { "type": "tool", "callID": "call_KBkEkpc3sSZwIcqGeecIbABc", "tool": "read", "state": { "status": "completed", "input": { "filePath": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx", "offset": 540, "limit": 360 }, "output": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx\nfile\n540: useState(null);\n541: const [diagnosisProgress, setDiagnosisProgress] = useState({\n542: total: 0,\n543: completed: 0,\n544: success: 0,\n545: failed: 0,\n546: timedOut: false,\n547: });\n548: const diagnosisAbortRef = useRef(null);\n549: const [addressModalTitle, setAddressModalTitle] = useState(\"\");\n550: const [addressList, setAddressList] = useState([]);\n551: \n552: // 导出相关状态\n553: const [exportModalOpen, setExportModalOpen] = useState(false);\n554: const [exportData, setExportData] = useState(\"\");\n555: const [exportLoading, setExportLoading] = useState(false);\n556: const [selectedTunnelForExport, setSelectedTunnelForExport] = useState<\n557: number | null\n558: >(null);\n559: \n560: // 导入相关状态\n561: type ImportFormat = \"flvx\" | \"ny\";\n562: const [importModalOpen, setImportModalOpen] = useState(false);\n563: const [importData, setImportData] = useState(\"\");\n564: const [importLoading, setImportLoading] = useState(false);\n565: const [importFormat, setImportFormat] = useState(\"flvx\");\n566: const [selectedTunnelForImport, setSelectedTunnelForImport] = useState<\n567: number | null\n568: >(null);\n569: const [importResults, setImportResults] = useState<\n570: Array<{\n571: line: string;\n572: success: boolean;\n573: message: string;\n574: forwardName?: string;\n575: }>\n576: >([]);\n577: \n578: // 表单状态\n579: const [form, setForm] = useState({\n580: name: \"\",\n581: tunnelId: null,\n582: inPort: null,\n583: inIp: \"\",\n584: remoteAddr: \"\",\n585: interfaceName: \"\",\n586: strategy: \"fifo\",\n587: speedId: null,\n588: });\n589: \n590: // 表单验证错误\n591: const [errors, setErrors] = useState<{ [key: string]: string }>({});\n592: \n593: // 批量操作相关状态\n594: const [selectMode, setSelectMode] = useState(false);\n595: const [selectedIds, setSelectedIds] = useState>(new Set());\n596: const [batchDeleteModalOpen, setBatchDeleteModalOpen] = useState(false);\n597: const [batchChangeTunnelModalOpen, setBatchChangeTunnelModalOpen] =\n598: useState(false);\n599: const [batchTargetTunnelId, setBatchTargetTunnelId] = useState(\n600: null,\n601: );\n602: const [batchLoading, setBatchLoading] = useState(false);\n603: const [groupOrderMap, setGroupOrderMap] = useState({});\n604: const [collapsedTunnelGroups, setCollapsedTunnelGroups] =\n605: useState({});\n606: const [groupPreferenceHydrated, setGroupPreferenceHydrated] = useState(false);\n607: const tokenUserId = JwtUtil.getUserIdFromToken();\n608: const tokenRoleId = JwtUtil.getRoleIdFromToken();\n609: const isAdmin = tokenRoleId === 0;\n610: \n611: const parseNodeIPs = (node?: Node): string[] => {\n612: if (!node) {\n613: return [];\n614: }\n615: \n616: const ips: string[] = [];\n617: const add = (value?: string) => {\n618: const trimmed = (value || \"\").trim();\n619: \n620: if (trimmed) {\n621: ips.push(trimmed);\n622: }\n623: };\n624: \n625: add(node.serverIpV4);\n626: add(node.serverIpV6);\n627: add(node.serverIp);\n628: \n629: (node.extraIPs || \"\")\n630: .split(\",\")\n631: .map((v) => v.trim())\n632: .filter((v) => v)\n633: .forEach((v) => ips.push(v));\n634: \n635: return Array.from(new Set(ips));\n636: };\n637: \n638: const tunnelInIpOptionMap = useMemo(() => {\n639: const map = new Map();\n640: const nodeMap = new Map(nodes.map((n) => [n.id, n]));\n641: \n642: for (const tunnel of allTunnels) {\n643: const collected: string[] = [];\n644: const entryNodes = tunnel.inNodeId || [];\n645: \n646: for (const entry of entryNodes) {\n647: collected.push(...parseNodeIPs(nodeMap.get(entry.nodeId)));\n648: }\n649: \n650: if (collected.length === 0) {\n651: (tunnel.inIp || \"\")\n652: .split(\",\")\n653: .map((v) => v.trim())\n654: .filter((v) => v)\n655: .forEach((v) => collected.push(v));\n656: }\n657: \n658: map.set(tunnel.id, Array.from(new Set(collected)));\n659: }\n660: \n661: return map;\n662: }, [allTunnels, nodes]);\n663: \n664: const currentTunnelIpOptions = useMemo(() => {\n665: if (!form.tunnelId) {\n666: return [];\n667: }\n668: \n669: return tunnelInIpOptionMap.get(form.tunnelId) || [];\n670: }, [form.tunnelId, tunnelInIpOptionMap]);\n671: \n672: useEffect(() => {\n673: return () => {\n674: diagnosisAbortRef.current?.abort();\n675: diagnosisAbortRef.current = null;\n676: };\n677: }, []);\n678: \n679: const persistGroupOrderToLocal = (nextOrderMap: ForwardGroupOrderMap) => {\n680: if (tokenUserId === null) {\n681: return;\n682: }\n683: \n684: try {\n685: localStorage.setItem(\n686: buildForwardGroupOrderLocalKey(tokenUserId),\n687: JSON.stringify(nextOrderMap),\n688: );\n689: } catch {}\n690: };\n691: \n692: const persistGroupCollapsedToLocal = (\n693: nextCollapsedMap: ForwardGroupCollapsedMap,\n694: ) => {\n695: if (tokenUserId === null) {\n696: return;\n697: }\n698: \n699: try {\n700: localStorage.setItem(\n701: buildForwardGroupCollapsedLocalKey(tokenUserId),\n702: JSON.stringify(nextCollapsedMap),\n703: );\n704: } catch {}\n705: };\n706: \n707: const persistGroupOrderToGlobal = async (\n708: nextOrderMap: ForwardGroupOrderMap,\n709: ): Promise => {\n710: if (!isAdmin || tokenUserId === null) {\n711: return;\n712: }\n713: \n714: try {\n715: const currentRes = await getConfigByName(FORWARD_GROUP_ORDER_CONFIG_KEY);\n716: const globalMap =\n717: parsePreferenceMap(\n718: currentRes.code === 0 && typeof currentRes.data?.value === \"string\"\n719: ? currentRes.data.value\n720: : null,\n721: ) || {};\n722: \n723: globalMap[tokenUserId.toString()] = nextOrderMap;\n724: \n725: const saveRes = await updateConfig(\n726: FORWARD_GROUP_ORDER_CONFIG_KEY,\n727: JSON.stringify(globalMap),\n728: );\n729: \n730: if (saveRes.code !== 0) {\n731: toast.error(saveRes.msg || \"保存分组排序失败\");\n732: }\n733: } catch {\n734: toast.error(\"保存分组排序失败\");\n735: }\n736: };\n737: \n738: const persistGroupCollapsedToGlobal = async (\n739: nextCollapsedMap: ForwardGroupCollapsedMap,\n740: ): Promise => {\n741: if (!isAdmin || tokenUserId === null) {\n742: return;\n743: }\n744: \n745: try {\n746: const currentRes = await getConfigByName(\n747: FORWARD_GROUP_COLLAPSED_CONFIG_KEY,\n748: );\n749: const globalMap =\n750: parsePreferenceMap(\n751: currentRes.code === 0 && typeof currentRes.data?.value === \"string\"\n752: ? currentRes.data.value\n753: : null,\n754: ) || {};\n755: \n756: globalMap[tokenUserId.toString()] = nextCollapsedMap;\n757: \n758: const saveRes = await updateConfig(\n759: FORWARD_GROUP_COLLAPSED_CONFIG_KEY,\n760: JSON.stringify(globalMap),\n761: );\n762: \n763: if (saveRes.code !== 0) {\n764: toast.error(saveRes.msg || \"保存分组折叠状态失败\");\n765: }\n766: } catch {\n767: toast.error(\"保存分组折叠状态失败\");\n768: }\n769: };\n770: \n771: useEffect(() => {\n772: let cancelled = false;\n773: \n774: const loadGroupPreferences = async () => {\n775: if (tokenUserId === null) {\n776: if (!cancelled) {\n777: setGroupOrderMap({});\n778: setCollapsedTunnelGroups({});\n779: setGroupPreferenceHydrated(true);\n780: }\n781: \n782: return;\n783: }\n784: \n785: let localOrderMap: ForwardGroupOrderMap = {};\n786: let localCollapsedMap: ForwardGroupCollapsedMap = {};\n787: \n788: try {\n789: localOrderMap = parseGroupOrderMap(\n790: localStorage.getItem(buildForwardGroupOrderLocalKey(tokenUserId)),\n791: );\n792: } catch {\n793: localOrderMap = {};\n794: }\n795: \n796: try {\n797: localCollapsedMap = parseGroupCollapsedMap(\n798: localStorage.getItem(buildForwardGroupCollapsedLocalKey(tokenUserId)),\n799: );\n800: } catch {\n801: localCollapsedMap = {};\n802: }\n803: \n804: if (isAdmin) {\n805: try {\n806: const [globalOrderRes, globalCollapsedRes] = await Promise.all([\n807: getConfigByName(FORWARD_GROUP_ORDER_CONFIG_KEY),\n808: getConfigByName(FORWARD_GROUP_COLLAPSED_CONFIG_KEY),\n809: ]);\n810: \n811: const globalOrderMap = parsePreferenceMap(\n812: globalOrderRes.code === 0 &&\n813: typeof globalOrderRes.data?.value === \"string\"\n814: ? globalOrderRes.data.value\n815: : null,\n816: );\n817: const globalCollapsedMap =\n818: parsePreferenceMap(\n819: globalCollapsedRes.code === 0 &&\n820: typeof globalCollapsedRes.data?.value === \"string\"\n821: ? globalCollapsedRes.data.value\n822: : null,\n823: );\n824: \n825: const globalOrderBucket = globalOrderMap?.[tokenUserId.toString()];\n826: const globalCollapsedBucket =\n827: globalCollapsedMap?.[tokenUserId.toString()];\n828: \n829: if (\n830: globalOrderBucket &&\n831: typeof globalOrderBucket === \"object\" &&\n832: !Array.isArray(globalOrderBucket)\n833: ) {\n834: localOrderMap = parseGroupOrderMap(\n835: JSON.stringify(globalOrderBucket),\n836: );\n837: }\n838: \n839: if (\n840: globalCollapsedBucket &&\n841: typeof globalCollapsedBucket === \"object\" &&\n842: !Array.isArray(globalCollapsedBucket)\n843: ) {\n844: localCollapsedMap = parseGroupCollapsedMap(\n845: JSON.stringify(globalCollapsedBucket),\n846: );\n847: }\n848: } catch {}\n849: }\n850: \n851: if (cancelled) {\n852: return;\n853: }\n854: \n855: setGroupOrderMap(localOrderMap);\n856: setCollapsedTunnelGroups(localCollapsedMap);\n857: persistGroupOrderToLocal(localOrderMap);\n858: persistGroupCollapsedToLocal(localCollapsedMap);\n859: setGroupPreferenceHydrated(true);\n860: };\n861: \n862: setGroupPreferenceHydrated(false);\n863: loadGroupPreferences();\n864: \n865: return () => {\n866: cancelled = true;\n867: };\n868: }, [tokenUserId, isAdmin]);\n869: \n870: useEffect(() => {\n871: const loadForwardCompactMode = async () => {\n872: try {\n873: const response = await getConfigByName(FORWARD_COMPACT_MODE_CONFIG_KEY);\n874: const enabled =\n875: response.code === 0 &&\n876: typeof response.data?.value === \"string\" &&\n877: response.data.value === \"true\";\n878: \n879: setCompactMode(enabled);\n880: } catch {\n881: setCompactMode(false);\n882: }\n883: };\n884: \n885: const handleCompactModeChanged = (event: Event) => {\n886: const customEvent = event as CustomEvent<{ enabled?: boolean }>;\n887: \n888: if (typeof customEvent.detail?.enabled === \"boolean\") {\n889: setCompactMode(customEvent.detail.enabled);\n890: }\n891: };\n892: \n893: loadForwardCompactMode();\n894: window.addEventListener(\n895: FORWARD_COMPACT_MODE_EVENT,\n896: handleCompactModeChanged,\n897: );\n898: \n899: return () => {\n\n(Showing lines 540-899 of 5560. Use offset=900 to continue.)\n", "title": "vite-frontend/src/pages/forward.tsx", "metadata": { "preview": " useState(null);\n const [diagnosisProgress, setDiagnosisProgress] = useState({\n total: 0,\n completed: 0,\n success: 0,\n failed: 0,\n timedOut: false,\n });\n const diagnosisAbortRef = useRef(null);\n const [addressModalTitle, setAddressModalTitle] = useState(\"\");\n const [addressList, setAddressList] = useState([]);\n\n // 导出相关状态\n const [exportModalOpen, setExportModalOpen] = useState(false);\n const [exportData, setExportData] = useState(\"\");\n const [exportLoading, setExportLoading] = useState(false);\n const [selectedTunnelForExport, setSelectedTunnelForExport] = useState<\n number | null\n >(null);\n", "truncated": true, "loaded": [] }, "time": { "start": 1772527590471, "end": 1772527590480 } }, "id": "prt_cb2e09c440010zt2M9aGC1p465", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e087e700227pxWUXR9DsDdN" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "cost": 0, "tokens": { "total": 16426, "input": 16178, "output": 248, "reasoning": 85, "cache": { "read": 0, "write": 0 } }, "id": "prt_cb2e09c52001NGzAKlZmIAso5C", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e087e700227pxWUXR9DsDdN" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527590551, "completed": 1772527602122 }, "parentID": "msg_cb2e05be3001YeVD71ZRqZLLrP", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "plan", "agent": "plan", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 22182, "input": 8041, "output": 189, "reasoning": 25, "cache": { "read": 13952, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e09c97002r8dw52LXkoHY5k", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "id": "prt_cb2e0bb2d001JcpPEsoMn3CM26", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e09c97002r8dw52LXkoHY5k" }, { "type": "reasoning", "text": "**Examining key code sections**", "time": { "start": 1772527598381, "end": 1772527602061 }, "id": "prt_cb2e0bb2d002OIPx5srN12OXjw", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e09c97002r8dw52LXkoHY5k" }, { "type": "tool", "callID": "call_K7dtloVosgokzxnYOFitWC4u", "tool": "read", "state": { "status": "completed", "input": { "filePath": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx", "offset": 1760, "limit": 320 }, "output": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx\nfile\n1760: setExportLoading(false);\n1761: \n1762: return;\n1763: }\n1764: \n1765: // 格式化导出数据:remoteAddr|name|inPort\n1766: const exportLines = forwardsToExport.map((forward) => {\n1767: return `${forward.remoteAddr}|${forward.name}|${forward.inPort}`;\n1768: });\n1769: \n1770: const exportText = exportLines.join(\"\\n\");\n1771: \n1772: setExportData(exportText);\n1773: } catch {\n1774: toast.error(\"导出失败\");\n1775: } finally {\n1776: setExportLoading(false);\n1777: }\n1778: };\n1779: \n1780: // 复制导出数据\n1781: const copyExportData = async () => {\n1782: await copyToClipboard(exportData, \"转发数据\");\n1783: };\n1784: \n1785: // 导入转发数据\n1786: const handleImport = () => {\n1787: setImportData(\"\");\n1788: setImportResults([]);\n1789: setSelectedTunnelForImport(null);\n1790: setImportModalOpen(true);\n1791: };\n1792: \n1793: // 执行导入\n1794: const executeImport = async () => {\n1795: if (!importData.trim()) {\n1796: toast.error(\"请输入要导入的数据\");\n1797: \n1798: return;\n1799: }\n1800: \n1801: if (!selectedTunnelForImport) {\n1802: toast.error(\"请选择要导入的隧道\");\n1803: \n1804: return;\n1805: }\n1806: \n1807: setImportLoading(true);\n1808: setImportResults([]);\n1809: \n1810: try {\n1811: if (importFormat === \"ny\") {\n1812: const parsedItems = parseNyFormatData(importData);\n1813: \n1814: if (parsedItems.length === 0) {\n1815: toast.error(\"未解析到有效的ny格式数据\");\n1816: \n1817: setImportLoading(false);\n1818: \n1819: return;\n1820: }\n1821: \n1822: for (const item of parsedItems) {\n1823: if (item.error) {\n1824: setImportResults((prev) => [\n1825: {\n1826: line: item.line,\n1827: success: false,\n1828: message: item.error || \"解析失败\",\n1829: },\n1830: ...prev,\n1831: ]);\n1832: \n1833: continue;\n1834: }\n1835: \n1836: if (!item.parsed) {\n1837: setImportResults((prev) => [\n1838: {\n1839: line: item.line,\n1840: success: false,\n1841: message: \"解析失败\",\n1842: },\n1843: ...prev,\n1844: ]);\n1845: \n1846: continue;\n1847: }\n1848: \n1849: const parsedNyItem = item.parsed;\n1850: const nyForwardInput = convertNyItemToForwardInput(parsedNyItem);\n1851: \n1852: try {\n1853: const response = await createForward({\n1854: name: nyForwardInput.name,\n1855: tunnelId: selectedTunnelForImport,\n1856: inPort: nyForwardInput.inPort,\n1857: remoteAddr: nyForwardInput.remoteAddr,\n1858: strategy: nyForwardInput.strategy,\n1859: });\n1860: \n1861: if (response.code === 0) {\n1862: setImportResults((prev) => [\n1863: {\n1864: line: item.line,\n1865: success: true,\n1866: message: `创建成功 (${parsedNyItem.dest.length}个目标)`,\n1867: forwardName: nyForwardInput.name,\n1868: },\n1869: ...prev,\n1870: ]);\n1871: } else {\n1872: setImportResults((prev) => [\n1873: {\n1874: line: item.line,\n1875: success: false,\n1876: message: response.msg || \"创建失败\",\n1877: },\n1878: ...prev,\n1879: ]);\n1880: }\n1881: } catch {\n1882: setImportResults((prev) => [\n1883: {\n1884: line: item.line,\n1885: success: false,\n1886: message: \"网络错误,创建失败\",\n1887: },\n1888: ...prev,\n1889: ]);\n1890: }\n1891: }\n1892: } else {\n1893: const lines = importData\n1894: .trim()\n1895: .split(\"\\n\")\n1896: .filter((line) => line.trim());\n1897: \n1898: for (let i = 0; i < lines.length; i++) {\n1899: const line = lines[i].trim();\n1900: const parts = line.split(\"|\");\n1901: \n1902: if (parts.length < 2) {\n1903: setImportResults((prev) => [\n1904: {\n1905: line,\n1906: success: false,\n1907: message: \"格式错误:需要至少包含目标地址和转发名称\",\n1908: },\n1909: ...prev,\n1910: ]);\n1911: continue;\n1912: }\n1913: \n1914: const [remoteAddr, name, inPort] = parts;\n1915: \n1916: if (!remoteAddr.trim() || !name.trim()) {\n1917: setImportResults((prev) => [\n1918: {\n1919: line,\n1920: success: false,\n1921: message: \"目标地址和转发名称不能为空\",\n1922: },\n1923: ...prev,\n1924: ]);\n1925: continue;\n1926: }\n1927: \n1928: const addresses = remoteAddr.trim().split(\",\");\n1929: const addressPattern = /^[^:]+:\\d+$/;\n1930: const isValidFormat = addresses.every((addr) =>\n1931: addressPattern.test(addr.trim()),\n1932: );\n1933: \n1934: if (!isValidFormat) {\n1935: setImportResults((prev) => [\n1936: {\n1937: line,\n1938: success: false,\n1939: message:\n1940: \"目标地址格式错误,应为 地址:端口 格式,多个地址用逗号分隔\",\n1941: },\n1942: ...prev,\n1943: ]);\n1944: continue;\n1945: }\n1946: \n1947: try {\n1948: let portNumber: number | null = null;\n1949: \n1950: if (inPort && inPort.trim()) {\n1951: const port = parseInt(inPort.trim());\n1952: \n1953: if (isNaN(port) || port < 1 || port > 65535) {\n1954: setImportResults((prev) => [\n1955: {\n1956: line,\n1957: success: false,\n1958: message: \"入口端口格式错误,应为1-65535之间的数字\",\n1959: },\n1960: ...prev,\n1961: ]);\n1962: continue;\n1963: }\n1964: portNumber = port;\n1965: }\n1966: \n1967: const response = await createForward({\n1968: name: name.trim(),\n1969: tunnelId: selectedTunnelForImport,\n1970: inPort: portNumber,\n1971: remoteAddr: remoteAddr.trim(),\n1972: strategy: \"fifo\",\n1973: });\n1974: \n1975: if (response.code === 0) {\n1976: setImportResults((prev) => [\n1977: {\n1978: line,\n1979: success: true,\n1980: message: \"创建成功\",\n1981: forwardName: name.trim(),\n1982: },\n1983: ...prev,\n1984: ]);\n1985: } else {\n1986: setImportResults((prev) => [\n1987: {\n1988: line,\n1989: success: false,\n1990: message: response.msg || \"创建失败\",\n1991: },\n1992: ...prev,\n1993: ]);\n1994: }\n1995: } catch {\n1996: setImportResults((prev) => [\n1997: {\n1998: line,\n1999: success: false,\n2000: message: \"网络错误,创建失败\",\n2001: },\n2002: ...prev,\n2003: ]);\n2004: }\n2005: }\n2006: }\n2007: \n2008: toast.success(\"导入执行完成\");\n2009: \n2010: await loadData(false);\n2011: } catch {\n2012: toast.error(\"导入过程中发生错误\");\n2013: } finally {\n2014: setImportLoading(false);\n2015: }\n2016: };\n2017: \n2018: // 获取状态显示\n2019: const getStatusDisplay = (status: number) => {\n2020: switch (status) {\n2021: case 1:\n2022: return { color: \"success\", text: \"正常\" };\n2023: case 0:\n2024: return { color: \"warning\", text: \"暂停\" };\n2025: case -1:\n2026: return { color: \"danger\", text: \"异常\" };\n2027: default:\n2028: return { color: \"default\", text: \"未知\" };\n2029: }\n2030: };\n2031: \n2032: // 获取策略显示\n2033: const getStrategyDisplay = (strategy: string) => {\n2034: switch (strategy) {\n2035: case \"fifo\":\n2036: return { color: \"primary\", text: \"主备\" };\n2037: case \"round\":\n2038: return { color: \"success\", text: \"轮询\" };\n2039: case \"rand\":\n2040: return { color: \"warning\", text: \"随机\" };\n2041: default:\n2042: return { color: \"default\", text: \"未知\" };\n2043: }\n2044: };\n2045: \n2046: // 获取地址数量\n2047: const getAddressCount = (addressString: string): number => {\n2048: if (!addressString) return 0;\n2049: const addresses = addressString\n2050: .split(\"\\n\")\n2051: .map((addr) => addr.trim())\n2052: .filter((addr) => addr);\n2053: \n2054: return addresses.length;\n2055: };\n2056: \n2057: // 处理拖拽结束\n2058: const handleDragEnd = async (event: DragEndEvent) => {\n2059: const { active, over } = event;\n2060: \n2061: if (!active || !over || active.id === over.id) return;\n2062: \n2063: const activeGroup = parseTunnelGroupSortableId(active.id);\n2064: const overGroup = parseTunnelGroupSortableId(over.id);\n2065: \n2066: if (activeGroup && overGroup) {\n2067: if (compactMode || !groupPreferenceHydrated) {\n2068: return;\n2069: }\n2070: \n2071: if (activeGroup.userId !== overGroup.userId) {\n2072: return;\n2073: }\n2074: \n2075: const userIdKey = activeGroup.userId.toString();\n2076: const currentOrder = groupOrderMap[userIdKey] || [];\n2077: const oldIndex = currentOrder.indexOf(activeGroup.tunnelKey);\n2078: const newIndex = currentOrder.indexOf(overGroup.tunnelKey);\n2079: \n\n(Showing lines 1760-2079 of 5560. Use offset=2080 to continue.)\n", "title": "vite-frontend/src/pages/forward.tsx", "metadata": { "preview": " setExportLoading(false);\n\n return;\n }\n\n // 格式化导出数据:remoteAddr|name|inPort\n const exportLines = forwardsToExport.map((forward) => {\n return `${forward.remoteAddr}|${forward.name}|${forward.inPort}`;\n });\n\n const exportText = exportLines.join(\"\\n\");\n\n setExportData(exportText);\n } catch {\n toast.error(\"导出失败\");\n } finally {\n setExportLoading(false);\n }\n };\n", "truncated": true, "loaded": [] }, "time": { "start": 1772527602016, "end": 1772527602031 } }, "id": "prt_cb2e0c95d001tZIO881fNB42p9", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e09c97002r8dw52LXkoHY5k" }, { "type": "tool", "callID": "call_CS2LngBXkoryDRon3uPktUOu", "tool": "read", "state": { "status": "completed", "input": { "filePath": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx", "offset": 4588, "limit": 280 }, "output": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx\nfile\n4588: )}\n4589: \n4590: \n4591: \n4592: \n4595: \n4596: \n4597: \n4598: \n4599: {/* 导入数据模态框 */}\n4600: setImportModalOpen(false)}\n4607: >\n4608: \n4609: \n4610:

导入转发数据

\n4611: {importFormat === \"flvx\" ? (\n4612: <>\n4613:

\n4614: 格式:目标地址|转发名称|入口端口,每行一个,入口端口留空将自动分配可用端口\n4615:

\n4616:

\n4617: 目标地址支持单个地址(如:example.com:8080)或多个地址用逗号分隔(如:3.3.3.3:3,4.4.4.4:4)\n4618:

\n4619: \n4620: ) : (\n4621: <>\n4622:

\n4623: ny格式:JSON对象,支持多个目标地址(负载均衡)\n4624:

\n4625:

\n4626: 格式:{"dest":["地址:端口"],"listen_port":端口,"name":"名称"}\n4627:

\n4628: \n4629: )}\n4630:
\n4631: \n4632:
\n4633: {/* 格式选择 */}\n4634: {\n4641: const selectedKey = Array.from(keys)[0] as ImportFormat;\n4642: \n4643: if (selectedKey) {\n4644: setImportFormat(selectedKey);\n4645: setSelectedTunnelForImport(null);\n4646: setImportData(\"\");\n4647: setImportResults([]);\n4648: }\n4649: }}\n4650: >\n4651: \n4652: flvx格式(管道分隔)\n4653: \n4654: \n4655: ny格式(JSON)\n4656: \n4657: \n4658: \n4659: {/* 隧道选择 - 两种格式都需要 */}\n4660: {\n4671: const selectedKey = Array.from(keys)[0] as string;\n4672: \n4673: setSelectedTunnelForImport(\n4674: selectedKey ? parseInt(selectedKey) : null,\n4675: );\n4676: }}\n4677: >\n4678: {tunnels.map((tunnel) => (\n4679: \n4683: {tunnel.name}\n4684: \n4685: ))}\n4686: \n4687: \n4688: {/* 输入区域 */}\n4689: setImportData(e.target.value)}\n4704: />\n4705: \n4706: {/* 导入结果 */}\n4707: {importResults.length > 0 && (\n4708:
\n4709:
\n4710:

导入结果

\n4711: \n4712: 成功:{importResults.filter((r) => r.success).length} /\n4713: 总计:{importResults.length}\n4714: \n4715:
\n4716: \n4717: \n4724: {importResults.map((result, index) => (\n4725: \n4733:
\n4734: {result.success ? (\n4735: \n4741: \n4746: \n4747: ) : (\n4748: \n4754: \n4759: \n4760: )}\n4761:
\n4762:
\n4763: \n4770: {result.success ? \"成功\" : \"失败\"}\n4771: \n4772: \n4773: |\n4774: \n4775: \n4776: {result.line}\n4777: \n4778:
\n4779: \n4786: {result.message}\n4787:
\n4788:
\n4789:
\n4790:
\n4791: ))}\n4792: \n4793: \n4794: )}\n4795: \n4796:
\n4797: \n4798: \n4801: \n4807: 开始导入\n4808: \n4809: \n4810:
\n4811: \n4812: \n4813: {/* 诊断结果模态框 */}\n4814: {\n4827: setDiagnosisModalOpen(open);\n4828: if (!open) {\n4829: diagnosisAbortRef.current?.abort();\n4830: diagnosisAbortRef.current = null;\n4831: setDiagnosisLoading(false);\n4832: }\n4833: }}\n4834: >\n4835: \n4836: {(onClose) => (\n4837: <>\n4838: \n4839:

转发诊断结果

\n4840: {currentDiagnosisForward && (\n4841:
\n4842: \n4843: {currentDiagnosisForward.name}\n4844: \n4845: \n4851: 转发服务\n4852: \n4853:
\n4854: )}\n4855:
\n4856: \n4857: {diagnosisResult ? (\n4858:
\n4859: {diagnosisLoading && (\n4860:
\n4861:
\n4862: \n4863: \n4864: 正在诊断 {diagnosisProgress.completed}/\n4865: {diagnosisProgress.total > 0\n4866: ? diagnosisProgress.total\n4867: : \"?\"}\n\n(Showing lines 4588-4867 of 5560. Use offset=4868 to continue.)\n", "title": "vite-frontend/src/pages/forward.tsx", "metadata": { "preview": " )}\n
\n \n \n \n \n \n \n\n {/* 导入数据模态框 */}\n setImportModalOpen(false)}\n >", "truncated": true, "loaded": [] }, "time": { "start": 1772527602052, "end": 1772527602060 } }, "id": "prt_cb2e0c97c001mrqs9AXBOBPMYX", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e09c97002r8dw52LXkoHY5k" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "cost": 0, "tokens": { "total": 22182, "input": 8041, "output": 189, "reasoning": 25, "cache": { "read": 13952, "write": 0 } }, "id": "prt_cb2e0c98e001kEHAQrbtcMhmuz", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e09c97002r8dw52LXkoHY5k" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527602122, "completed": 1772527607692 }, "parentID": "msg_cb2e05be3001YeVD71ZRqZLLrP", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "plan", "agent": "plan", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 28434, "input": 28289, "output": 145, "reasoning": 82, "cache": { "read": 0, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e0c9ca002luZwjSQSyPuOpZ", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "id": "prt_cb2e0dd690014Cozer20V0PRpY", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e0c9ca002luZwjSQSyPuOpZ" }, { "type": "reasoning", "text": "**Inspecting plans directory**", "time": { "start": 1772527607145, "end": 1772527607619 }, "id": "prt_cb2e0dd69002n5ph9ONbcrxLsQ", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e0c9ca002luZwjSQSyPuOpZ" }, { "type": "tool", "callID": "call_A4E6cXP8uiawvQ2ALrncaWp4", "tool": "glob", "state": { "status": "completed", "input": { "pattern": "plans/*.md", "path": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "output": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/plans/001-fix-211-connectip-full-chain.md", "title": "", "metadata": { "count": 1, "truncated": false }, "time": { "start": 1772527607572, "end": 1772527607599 } }, "id": "prt_cb2e0dd7c0018FsXKrdL9JolbB", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e0c9ca002luZwjSQSyPuOpZ" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "cost": 0, "tokens": { "total": 28434, "input": 28289, "output": 145, "reasoning": 82, "cache": { "read": 0, "write": 0 } }, "id": "prt_cb2e0df45001SpVH3lw3Z68mJI", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e0c9ca002luZwjSQSyPuOpZ" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527607692, "completed": 1772527613014 }, "parentID": "msg_cb2e05be3001YeVD71ZRqZLLrP", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "plan", "agent": "plan", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 28644, "input": 28412, "output": 232, "reasoning": 154, "cache": { "read": 0, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e0df8c002Gtp1EKncT9qMTM", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "id": "prt_cb2e0f1510015USHzOUjcJ7BQE", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e0df8c002Gtp1EKncT9qMTM" }, { "type": "reasoning", "text": "**Planning read-only response with clarification**", "time": { "start": 1772527612241, "end": 1772527612926 }, "id": "prt_cb2e0f1510024lqiBR7fHvBvnB", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e0df8c002Gtp1EKncT9qMTM" }, { "type": "tool", "callID": "call_KZaOqnniiTHJdzoS2YftnO1i", "tool": "read", "state": { "status": "completed", "input": { "filePath": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts", "offset": 1, "limit": 260 }, "output": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts\nfile\n1: import test from \"node:test\";\n2: import assert from \"node:assert/strict\";\n3: \n4: import {\n5: convertNyItemToForwardInput,\n6: parseNyFormatData,\n7: } from \"./import-format.ts\";\n8: \n9: test(\"parseNyFormatData parses concatenated ny JSON objects\", () => {\n10: const input =\n11: '{\"dest\":[\"151.241.129.52:23609\"],\"listen_port\":20224,\"name\":\"灵玥-JP-Lpt【三网通用】\"}{\"dest\":[\"64.81.33.2:24577\"],\"listen_port\":41034,\"name\":\"Yolo-US-Lpt【三网通用】\"}';\n12: \n13: const result = parseNyFormatData(input);\n14: \n15: assert.equal(result.length, 2);\n16: assert.equal(result[0].error, undefined);\n17: assert.equal(result[1].error, undefined);\n18: assert.deepEqual(result[0].parsed?.dest, [\"151.241.129.52:23609\"]);\n19: assert.equal(result[0].parsed?.listen_port, 20224);\n20: assert.equal(result[1].parsed?.name, \"Yolo-US-Lpt【三网通用】\");\n21: });\n22: \n23: test(\"parseNyFormatData parses newline-separated ny JSON objects\", () => {\n24: const input = [\n25: '{\"dest\":[\"1.1.1.1:1000\",\"2.2.2.2:2000\"],\"listen_port\":3000,\"name\":\"A\"}',\n26: '{\"dest\":[\"3.3.3.3:4000\"],\"listen_port\":5000,\"name\":\"B\"}',\n27: ].join(\"\\n\");\n28: \n29: const result = parseNyFormatData(input);\n30: \n31: assert.equal(result.length, 2);\n32: assert.equal(result[0].error, undefined);\n33: assert.equal(result[1].error, undefined);\n34: assert.deepEqual(result[0].parsed?.dest, [\"1.1.1.1:1000\", \"2.2.2.2:2000\"]);\n35: assert.equal(result[0].parsed?.listen_port, 3000);\n36: });\n37: \n38: test(\"parseNyFormatData returns validation errors for invalid fields\", () => {\n39: const input =\n40: '{\"dest\":[],\"listen_port\":0,\"name\":\"\"}{\"dest\":[\"bad-address\"],\"listen_port\":80,\"name\":\"ok\"}';\n41: \n42: const result = parseNyFormatData(input);\n43: \n44: assert.equal(result.length, 2);\n45: assert.match(result[0].error || \"\", /dest数组为空|listen_port|name/);\n46: assert.match(result[1].error || \"\", /目标地址格式错误/);\n47: });\n48: \n49: test(\"convertNyItemToForwardInput maps ny fields correctly\", () => {\n50: const mapped = convertNyItemToForwardInput({\n51: dest: [\"1.1.1.1:1111\", \"2.2.2.2:2222\"],\n52: listen_port: 3333,\n53: name: \" Forward Name \",\n54: });\n55: \n56: assert.deepEqual(mapped, {\n57: name: \"Forward Name\",\n58: inPort: 3333,\n59: remoteAddr: \"1.1.1.1:1111,2.2.2.2:2222\",\n60: strategy: \"fifo\",\n61: });\n62: });\n\n(End of file - total 62 lines)\n", "title": "vite-frontend/src/pages/forward/import-format.test.ts", "metadata": { "preview": "import test from \"node:test\";\nimport assert from \"node:assert/strict\";\n\nimport {\n convertNyItemToForwardInput,\n parseNyFormatData,\n} from \"./import-format.ts\";\n\ntest(\"parseNyFormatData parses concatenated ny JSON objects\", () => {\n const input =\n '{\"dest\":[\"151.241.129.52:23609\"],\"listen_port\":20224,\"name\":\"灵玥-JP-Lpt【三网通用】\"}{\"dest\":[\"64.81.33.2:24577\"],\"listen_port\":41034,\"name\":\"Yolo-US-Lpt【三网通用】\"}';\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.equal(result[0].error, undefined);\n assert.equal(result[1].error, undefined);\n assert.deepEqual(result[0].parsed?.dest, [\"151.241.129.52:23609\"]);\n assert.equal(result[0].parsed?.listen_port, 20224);\n assert.equal(result[1].parsed?.name, \"Yolo-US-Lpt【三网通用】\");", "truncated": false, "loaded": [] }, "time": { "start": 1772527612826, "end": 1772527612829 } }, "id": "prt_cb2e0f1c5001jy2vGqxvNRVU8B", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e0df8c002Gtp1EKncT9qMTM" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "cost": 0, "tokens": { "total": 28644, "input": 28412, "output": 232, "reasoning": 154, "cache": { "read": 0, "write": 0 } }, "id": "prt_cb2e0f400001FNeNnkNC3ZhdWB", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e0df8c002Gtp1EKncT9qMTM" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527613014, "completed": 1772527627898 }, "parentID": "msg_cb2e05be3001YeVD71ZRqZLLrP", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "plan", "agent": "plan", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 29868, "input": 8387, "output": 489, "reasoning": 302, "cache": { "read": 20992, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e0f456002LC4aia1iCe0gBx", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "id": "prt_cb2e12bce001Uq1UHqISFY69xT", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e0f456002LC4aia1iCe0gBx" }, { "type": "reasoning", "text": "**Analyzing import format and UI terminology**", "time": { "start": 1772527627214, "end": 1772527627832 }, "id": "prt_cb2e12bce002sWl8C2OfeJrT66", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e0f456002LC4aia1iCe0gBx" }, { "type": "tool", "callID": "call_D0LcfHeovjqWPMtUmH5plSY8", "tool": "grep", "state": { "status": "completed", "input": { "pattern": "选择入口|入口|inNode|entry", "path": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx", "include": "*.tsx" }, "output": "Found 31 matches\n/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx:\n Line 128: inNodeId?: Array<{ nodeId: number }>;\n Line 129: inNodePortSta?: number;\n Line 130: inNodePortEnd?: number;\n Line 644: const entryNodes = tunnel.inNodeId || [];\n Line 646: for (const entry of entryNodes) {\n Line 647: collected.push(...parseNodeIPs(nodeMap.get(entry.nodeId)));\n Line 1233: // 验证入口端口(可选,如果填写则验证)\n Line 1958: message: \"入口端口格式错误,应为1-65535之间的数字\",\n Line 2877: showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n Line 3096: showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n Line 3295: showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n Line 3301: 入口:\n Line 3745: 入口\n Line 3943: 入口\n Line 4255: description=\"指定入口端口,留空则从节点可用端口中自动分配\"\n Line 4258: label=\"入口端口\"\n Line 4274: description=\"从入口节点IP中选择,留空使用默认\"\n Line 4282: ? \"选择入口监听IP\"\n Line 4283: : \"当前隧道入口节点暂无可选IP\"\n Line 4297: 默认入口IP\n Line 4460: 格式:目标地址|转发名称|入口端口\n Line 4614: 格式:目标地址|转发名称|入口端口,每行一个,入口端口留空将自动分配可用端口\n Line 4698: ? \"请输入要导入的转发数据,格式:目标地址|转发名称|入口端口\"\n Line 4928: entry: diagnosisResult.results.filter(\n Line 5109: {/* 入口测试 */}\n Line 5111: \"🚪 入口测试\",\n Line 5112: groupedResults.entry,\n Line 5141: entry: diagnosisResult.results.filter(\n Line 5304: {/* 入口测试 */}\n Line 5306: \"🚪 入口测试\",\n Line 5307: groupedResults.entry,", "title": "选择入口|入口|inNode|entry", "metadata": { "matches": 31, "truncated": false }, "time": { "start": 1772527627765, "end": 1772527627782 } }, "id": "prt_cb2e12dd9001PPUij1j5JO9dlc", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e0f456002LC4aia1iCe0gBx" }, { "type": "tool", "callID": "call_QgBOMOkx64OFTPj6HQ00TCvO", "tool": "grep", "state": { "status": "completed", "input": { "pattern": "listen_port|dest|parseNyFormatData|convertNyItemToForwardInput", "path": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages", "include": "*.{ts,tsx}" }, "output": "Found 43 matches\n/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx:\n Line 91: convertNyItemToForwardInput,\n Line 92: parseNyFormatData,\n Line 1812: const parsedItems = parseNyFormatData(importData);\n Line 1850: const nyForwardInput = convertNyItemToForwardInput(parsedNyItem);\n Line 1866: message: `创建成功 (${parsedNyItem.dest.length}个目标)`,\n Line 4626: 格式:{"dest":["地址:端口"],"listen_port":端口,"name":"名称"}\n Line 4699: : '请输入ny格式数据,每行一个JSON对象,如:{\"dest\":[\"1.2.3.4:80\"],\"listen_port\":8080,\"name\":\"转发1\"}'\n\n/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts:\n Line 2: dest: string[];\n Line 3: listen_port: number;\n Line 30: const dest = item.dest;\n Line 31: const listenPort = item.listen_port;\n Line 34: if (!Array.isArray(dest) || dest.length === 0) {\n Line 35: return { line, error: \"dest数组为空或格式错误\" };\n Line 43: return { line, error: \"listen_port必须为1-65535之间的数字\" };\n Line 46: const normalizedDest = dest.map((itemValue) =>\n Line 51: return { line, error: \"dest中包含空地址\" };\n Line 65: dest: normalizedDest,\n Line 66: listen_port: listenPort,\n Line 121: export const parseNyFormatData = (input: string): ParsedNyImportLine[] => {\n Line 162: export const convertNyItemToForwardInput = (item: NyImportItem) => {\n Line 165: inPort: item.listen_port,\n Line 166: remoteAddr: item.dest.join(\",\"),\n\n/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts:\n Line 5: convertNyItemToForwardInput,\n Line 6: parseNyFormatData,\n Line 9: test(\"parseNyFormatData parses concatenated ny JSON objects\", () => {\n Line 11: '{\"dest\":[\"151.241.129.52:23609\"],\"listen_port\":20224,\"name\":\"灵玥-JP-Lpt【三网通用】\"}{\"dest\":[\"64.81.33.2:24577\"],\"listen_port\":41034,\"name\":\"Yolo-US-Lpt【三网通用】\"}';\n Line 13: const result = parseNyFormatData(input);\n Line 18: assert.deepEqual(result[0].parsed?.dest, [\"151.241.129.52:23609\"]);\n Line 19: assert.equal(result[0].parsed?.listen_port, 20224);\n Line 23: test(\"parseNyFormatData parses newline-separated ny JSON objects\", () => {\n Line 25: '{\"dest\":[\"1.1.1.1:1000\",\"2.2.2.2:2000\"],\"listen_port\":3000,\"name\":\"A\"}',\n Line 26: '{\"dest\":[\"3.3.3.3:4000\"],\"listen_port\":5000,\"name\":\"B\"}',\n Line 29: const result = parseNyFormatData(input);\n Line 34: assert.deepEqual(result[0].parsed?.dest, [\"1.1.1.1:1000\", \"2.2.2.2:2000\"]);\n Line 35: assert.equal(result[0].parsed?.listen_port, 3000);\n Line 38: test(\"parseNyFormatData returns validation errors for invalid fields\", () => {\n Line 40: '{\"dest\":[],\"listen_port\":0,\"name\":\"\"}{\"dest\":[\"bad-address\"],\"listen_port\":80,\"name\":\"ok\"}';\n Line 42: const result = parseNyFormatData(input);\n Line 45: assert.match(result[0].error || \"\", /dest数组为空|listen_port|name/);\n Line 49: test(\"convertNyItemToForwardInput maps ny fields correctly\", () => {\n Line 50: const mapped = convertNyItemToForwardInput({\n Line 51: dest: [\"1.1.1.1:1111\", \"2.2.2.2:2222\"],\n Line 52: listen_port: 3333,", "title": "listen_port|dest|parseNyFormatData|convertNyItemToForwardInput", "metadata": { "matches": 43, "truncated": false }, "time": { "start": 1772527627807, "end": 1772527627822 } }, "id": "prt_cb2e12e1b001uzgiO2qcjzEqrF", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e0f456002LC4aia1iCe0gBx" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "cost": 0, "tokens": { "total": 29868, "input": 8387, "output": 489, "reasoning": 302, "cache": { "read": 20992, "write": 0 } }, "id": "prt_cb2e12e39001fK5dwZ0KU6v1rs", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e0f456002LC4aia1iCe0gBx" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527627899, "completed": 1772527640616 }, "parentID": "msg_cb2e05be3001YeVD71ZRqZLLrP", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "plan", "agent": "plan", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 31723, "input": 31335, "output": 388, "reasoning": 303, "cache": { "read": 0, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e12e7b0023uHag4TX2RT0Ra", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "id": "prt_cb2e15ddf001raaFlO5x1ybGPk", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e12e7b0023uHag4TX2RT0Ra" }, { "type": "reasoning", "text": "**Planning import adjustment**", "time": { "start": 1772527640031, "end": 1772527640532 }, "id": "prt_cb2e15ddf002sP1foOBOzVZ5ux", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e12e7b0023uHag4TX2RT0Ra" }, { "type": "tool", "callID": "call_ZuFpdGZduphCvatsTCLtpalY", "tool": "grep", "state": { "status": "completed", "input": { "pattern": "listen_port|ny格式|dest\\\"", "path": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "include": "*.{ts,tsx,md,go,json}" }, "output": "Found 29 matches\n/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx:\n Line 1815: toast.error(\"未解析到有效的ny格式数据\");\n Line 4623: ny格式:JSON对象,支持多个目标地址(负载均衡)\n Line 4626: 格式:{"dest":["地址:端口"],"listen_port":端口,"name":"名称"}\n Line 4654: \n Line 4655: ny格式(JSON)\n Line 4699: : '请输入ny格式数据,每行一个JSON对象,如:{\"dest\":[\"1.2.3.4:80\"],\"listen_port\":8080,\"name\":\"转发1\"}'\n\n/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts:\n Line 3: listen_port: number;\n Line 31: const listenPort = item.listen_port;\n Line 43: return { line, error: \"listen_port必须为1-65535之间的数字\" };\n Line 66: listen_port: listenPort,\n Line 165: inPort: item.listen_port,\n\n/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts:\n Line 11: '{\"dest\":[\"151.241.129.52:23609\"],\"listen_port\":20224,\"name\":\"灵玥-JP-Lpt【三网通用】\"}{\"dest\":[\"64.81.33.2:24577\"],\"listen_port\":41034,\"name\":\"Yolo-US-Lpt【三网通用】\"}';\n Line 19: assert.equal(result[0].parsed?.listen_port, 20224);\n Line 25: '{\"dest\":[\"1.1.1.1:1000\",\"2.2.2.2:2000\"],\"listen_port\":3000,\"name\":\"A\"}',\n Line 26: '{\"dest\":[\"3.3.3.3:4000\"],\"listen_port\":5000,\"name\":\"B\"}',\n Line 35: assert.equal(result[0].parsed?.listen_port, 3000);\n Line 40: '{\"dest\":[],\"listen_port\":0,\"name\":\"\"}{\"dest\":[\"bad-address\"],\"listen_port\":80,\"name\":\"ok\"}';\n Line 45: assert.match(result[0].error || \"\", /dest数组为空|listen_port|name/);\n Line 52: listen_port: 3333,\n\n/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/.entire/metadata/ses_34d7fcdb8ffekb7lvyJwVvoS8T/context.md:\n Line 5: ny格式如下:\n Line 6: {\"dest\":[\"151.241.129.52:23609\"],\"list\n Line 13: ny格式如下:\n Line 14: {\"dest\":[\"151.241.129.52:23609\"],\"listen_port\":20224,\"name\":\"灵玥-JP-Lpt【三网通用】\"}{\"dest\":[\"64.81.33.2:24577\"],\"listen_port\":41034,\"name\":\"Yolo-US-Lpt【三网通用】\"}\n Line 25: | 特性 | flvx格式 | ny格式 |\n Line 28: | 示例 | `ip:port\\|name\\|port` | `{\"dest\":[\"ip:port\"],\"listen_port\":port,\"name\":\"name\"}` |\n Line 33: 1. **格式选择器**:手动选择\"flvx格式\"或\"ny格式\"\n Line 34: 2. **ny格式流程**:选择入口节点 → 匹配端口转发隧道 → 匹配0个报错、1个直接用、多个弹窗选\n Line 40: 3. 实现ny格式紧凑JSON解析函数\n Line 43: 6. 整合ny格式到`executeImport`函数", "title": "listen_port|ny格式|dest\\\"", "metadata": { "matches": 29, "truncated": false }, "time": { "start": 1772527640415, "end": 1772527640452 } }, "id": "prt_cb2e15e290010Jf0posT0l4DTm", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e12e7b0023uHag4TX2RT0Ra" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "cost": 0, "tokens": { "total": 31723, "input": 31335, "output": 388, "reasoning": 303, "cache": { "read": 0, "write": 0 } }, "id": "prt_cb2e15fd6001ybGK0gczE0Uvj0", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e12e7b0023uHag4TX2RT0Ra" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527640617, "completed": 1772527645600 }, "parentID": "msg_cb2e05be3001YeVD71ZRqZLLrP", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "plan", "agent": "plan", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 32643, "input": 4301, "output": 182, "reasoning": 87, "cache": { "read": 28160, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e16029002kyk9z8JjyFdg8j", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "id": "prt_cb2e16fbb001JBawQgj83dMK5f", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e16029002kyk9z8JjyFdg8j" }, { "type": "reasoning", "text": "**Planning import compatibility changes**", "time": { "start": 1772527644604, "end": 1772527645509 }, "id": "prt_cb2e16fbc001GD1XnG5ecWFrln", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e16029002kyk9z8JjyFdg8j" }, { "type": "tool", "callID": "call_42ZvQXPtETGCs5ydhmdGGv0f", "tool": "read", "state": { "status": "completed", "input": { "filePath": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/.entire/metadata/ses_34d7fcdb8ffekb7lvyJwVvoS8T/context.md", "offset": 1, "limit": 220 }, "output": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/.entire/metadata/ses_34d7fcdb8ffekb7lvyJwVvoS8T/context.md\nfile\n1: # Session Context\n2: \n3: Session ID: ses_34d7fcdb8ffekb7lvyJwVvoS8T\n4: Commit Message: 增加功能:转发规则批量导入格式,兼容ny的导入格式\n5: ny格式如下:\n6: {\"dest\":[\"151.241.129.52:23609\"],\"list\n7: \n8: ## Prompts\n9: \n10: ### Prompt 1\n11: \n12: 增加功能:转发规则批量导入格式,兼容ny的导入格式\n13: ny格式如下:\n14: {\"dest\":[\"151.241.129.52:23609\"],\"listen_port\":20224,\"name\":\"灵玥-JP-Lpt【三网通用】\"}{\"dest\":[\"64.81.33.2:24577\"],\"listen_port\":41034,\"name\":\"Yolo-US-Lpt【三网通用】\"}\n15: \n16: ny导入的时候是选择入口的,flvx转发导入是选择隧道的,需要兼容一下,请设计计划\n17: \n18: ## Summary\n19: \n20: 计划文档已设计完成。以下是计划摘要:\n21: \n22: ## 计划摘要\n23: \n24: ### 格式对比\n25: | 特性 | flvx格式 | ny格式 |\n26: |------|----------|--------|\n27: | 格式 | 管道分隔文本 | 紧凑JSON对象 |\n28: | 示例 | `ip:port\\|name\\|port` | `{\"dest\":[\"ip:port\"],\"listen_port\":port,\"name\":\"name\"}` |\n29: | 多目标 | 逗号分隔 | dest数组 |\n30: | 选择器 | 选择隧道 | 选择入口节点→匹配隧道 |\n31: \n32: ### 核心设计\n33: 1. **格式选择器**:手动选择\"flvx格式\"或\"ny格式\"\n34: 2. **ny格式流程**:选择入口节点 → 匹配端口转发隧道 → 匹配0个报错、1个直接用、多个弹窗选\n35: 3. **不自动创建隧道**:无匹配时提示用户先创建\n36: \n37: ### 任务清单\n38: 1. 新增`importFormat`、`selectedEntryNode`、`matchedTunnels`等状态\n39: 2. 重构导入模态框UI,添加格式选择器\n40: 3. 实现ny格式紧凑JSON解析函数\n41: 4. 实现入口节点选择与隧道匹配逻辑\n42: 5. 多隧道匹配时的选择器弹窗\n43: 6. 整合ny格式到`executeImport`函数\n44: 7. 更新提示文案\n45: \n46: ### 改动范围\n47: - `vite-frontend/src/pages/forward.tsx` - 主要改动文件\n48: \n49: 是否需要我调整计划内容?\n\n(End of file - total 49 lines)\n\n\n\nInstructions from: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/.entire/metadata/ses_34d7fcdb8ffekb7lvyJwVvoS8T/CONTEXT.md\n# Session Context\n\nSession ID: ses_34d7fcdb8ffekb7lvyJwVvoS8T\nCommit Message: 增加功能:转发规则批量导入格式,兼容ny的导入格式\nny格式如下:\n{\"dest\":[\"151.241.129.52:23609\"],\"list\n\n## Prompts\n\n### Prompt 1\n\n增加功能:转发规则批量导入格式,兼容ny的导入格式\nny格式如下:\n{\"dest\":[\"151.241.129.52:23609\"],\"listen_port\":20224,\"name\":\"灵玥-JP-Lpt【三网通用】\"}{\"dest\":[\"64.81.33.2:24577\"],\"listen_port\":41034,\"name\":\"Yolo-US-Lpt【三网通用】\"}\n\nny导入的时候是选择入口的,flvx转发导入是选择隧道的,需要兼容一下,请设计计划\n\n## Summary\n\n计划文档已设计完成。以下是计划摘要:\n\n## 计划摘要\n\n### 格式对比\n| 特性 | flvx格式 | ny格式 |\n|------|----------|--------|\n| 格式 | 管道分隔文本 | 紧凑JSON对象 |\n| 示例 | `ip:port\\|name\\|port` | `{\"dest\":[\"ip:port\"],\"listen_port\":port,\"name\":\"name\"}` |\n| 多目标 | 逗号分隔 | dest数组 |\n| 选择器 | 选择隧道 | 选择入口节点→匹配隧道 |\n\n### 核心设计\n1. **格式选择器**:手动选择\"flvx格式\"或\"ny格式\"\n2. **ny格式流程**:选择入口节点 → 匹配端口转发隧道 → 匹配0个报错、1个直接用、多个弹窗选\n3. **不自动创建隧道**:无匹配时提示用户先创建\n\n### 任务清单\n1. 新增`importFormat`、`selectedEntryNode`、`matchedTunnels`等状态\n2. 重构导入模态框UI,添加格式选择器\n3. 实现ny格式紧凑JSON解析函数\n4. 实现入口节点选择与隧道匹配逻辑\n5. 多隧道匹配时的选择器弹窗\n6. 整合ny格式到`executeImport`函数\n7. 更新提示文案\n\n### 改动范围\n- `vite-frontend/src/pages/forward.tsx` - 主要改动文件\n\n是否需要我调整计划内容?\n\n", "title": ".entire/metadata/ses_34d7fcdb8ffekb7lvyJwVvoS8T/context.md", "metadata": { "preview": "# Session Context\n\nSession ID: ses_34d7fcdb8ffekb7lvyJwVvoS8T\nCommit Message: 增加功能:转发规则批量导入格式,兼容ny的导入格式\nny格式如下:\n{\"dest\":[\"151.241.129.52:23609\"],\"list\n\n## Prompts\n\n### Prompt 1\n\n增加功能:转发规则批量导入格式,兼容ny的导入格式\nny格式如下:\n{\"dest\":[\"151.241.129.52:23609\"],\"listen_port\":20224,\"name\":\"灵玥-JP-Lpt【三网通用】\"}{\"dest\":[\"64.81.33.2:24577\"],\"listen_port\":41034,\"name\":\"Yolo-US-Lpt【三网通用】\"}\n\nny导入的时候是选择入口的,flvx转发导入是选择隧道的,需要兼容一下,请设计计划\n\n## Summary\n\n计划文档已设计完成。以下是计划摘要:", "truncated": false, "loaded": [ "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/.entire/metadata/ses_34d7fcdb8ffekb7lvyJwVvoS8T/CONTEXT.md" ] }, "time": { "start": 1772527645324, "end": 1772527645329 } }, "id": "prt_cb2e16fe6001Pzq3VTxE490MW8", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e16029002kyk9z8JjyFdg8j" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "cost": 0, "tokens": { "total": 32643, "input": 4301, "output": 182, "reasoning": 87, "cache": { "read": 28160, "write": 0 } }, "id": "prt_cb2e173460019EHSO9CIdUVn6N", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e16029002kyk9z8JjyFdg8j" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527645601, "completed": 1772527681555 }, "parentID": "msg_cb2e05be3001YeVD71ZRqZLLrP", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "plan", "agent": "plan", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 35449, "input": 34010, "output": 1439, "reasoning": 839, "cache": { "read": 0, "write": 0 } }, "variant": "xhigh", "finish": "stop", "id": "msg_cb2e173a1002j0CV5wEGgbaT5w", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "id": "prt_cb2e1d0aa001pmX4q2NJ1qASsJ", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e173a1002j0CV5wEGgbaT5w" }, { "type": "reasoning", "text": "**实施计划设计**", "time": { "start": 1772527669419, "end": 1772527681461 }, "id": "prt_cb2e1d0ab001V1H3YPxRui5x6M", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e173a1002j0CV5wEGgbaT5w" }, { "type": "text", "text": "我先看了现状:当前代码里 `ny` 导入流程已经是“选择隧道”而不是“选择入口节点”了,核心位置在 `vite-frontend/src/pages/forward.tsx:4660`(导入隧道下拉)和 `vite-frontend/src/pages/forward.tsx:1855`(创建时使用 `tunnelId: selectedTunnelForImport`)。\n\n你这次需求我理解为:继续把 `ny` 兼容做稳,并确保全流程只按“选隧道”走,不再有“选入口”的逻辑或文案残留。 \n下面是可直接执行的实现计划(当前是 plan 模式,我不做文件修改):\n\n**实施计划**\n- 在 `vite-frontend/src/pages/forward.tsx` 统一导入交互:`flvx/ny` 共用同一个“选择导入隧道”,切换格式时仅重置数据,不引入任何入口节点相关状态或匹配逻辑。\n- 在 `vite-frontend/src/pages/forward/import-format.ts` 扩展 `ny` 解析兼容层:保持现有紧凑 JSON/换行 JSON 支持,同时增加常见字段别名(例如 `listenPort`/`port`),并做归一化输出到现有 `NyImportItem`。\n- 保持导入落库目标一致:`createForward` 始终使用用户选中的 `selectedTunnelForImport`,避免根据 `ny` 内容去推断隧道或入口。\n- 优化错误提示文案:`ny` 解析失败时返回更明确的字段级错误(缺少目标、端口不合法、名称为空),并在 `vite-frontend/src/pages/forward.tsx:4623` 附近文案说明“按所选隧道导入”。\n- 补充/更新解析单测到 `vite-frontend/src/pages/forward/import-format.test.ts`:覆盖“字段别名兼容”“单对象/多对象/拼接对象”“非法字段报错”。\n- 新建计划文档(实施时):`plans/002-ny-import-use-tunnel-selection.md`,按仓库规则维护 checklist 并在每步完成后打勾。\n\n**我建议的默认兼容策略**\n- `ny` 里如果没有端口字段时,默认允许导入并让系统自动分配入口端口(即传 `inPort: null`),兼容性最好。\n\n一个需要你拍板的点(会影响实现细节):\n- 端口缺失时你希望哪种行为? \n1) 自动分配端口(推荐) \n2) 强制报错,必须提供端口", "time": { "start": 1772527681463, "end": 1772527681463 }, "id": "prt_cb2e1d116001huOjRn67KjikeW", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e173a1002j0CV5wEGgbaT5w" }, { "type": "step-finish", "reason": "stop", "snapshot": "2d41b4c5fe170808dea53d8d3a8a2181402ccb69", "cost": 0, "tokens": { "total": 35449, "input": 34010, "output": 1439, "reasoning": 839, "cache": { "read": 0, "write": 0 } }, "id": "prt_cb2e1ffb900160F59zrIX6np6g", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e173a1002j0CV5wEGgbaT5w" } ] }, { "info": { "role": "user", "time": { "created": 1772527727772 }, "summary": { "diffs": [] }, "agent": "plan", "model": { "providerID": "api", "modelID": "gpt-5.3-codex" }, "variant": "xhigh", "id": "msg_cb2e2b4a0001qGkmqn7H4tiCE2", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "text", "text": "自动分配端口(推荐)", "id": "prt_cb2e2b4a1001Tddn2HpXlctpI7", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e2b4a0001qGkmqn7H4tiCE2" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527727783, "completed": 1772527753801 }, "parentID": "msg_cb2e2b4a0001qGkmqn7H4tiCE2", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "plan", "agent": "plan", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 35318, "input": 34622, "output": 696, "reasoning": 251, "cache": { "read": 0, "write": 0 } }, "variant": "xhigh", "finish": "stop", "id": "msg_cb2e2b4a7002N5xtl7xiPzXnE5", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "2b8e04eb5fa138d3842c7fc8f8c70b42465c2724", "id": "prt_cb2e2f506001VD5B48OE9GSpt3", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e2b4a7002N5xtl7xiPzXnE5" }, { "type": "reasoning", "text": "**Drafting final implementation plan**", "time": { "start": 1772527744263, "end": 1772527753702 }, "id": "prt_cb2e2f507001le4IqVH6E1HZl0", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e2b4a7002N5xtl7xiPzXnE5" }, { "type": "text", "text": "收到,按“**端口缺失时自动分配**”执行。\n\n基于你这个选择,我给你一版可直接落地的实施计划(不改文件,只出方案):\n\n**实施方案**\n- 保持导入入口统一:`flvx` 和 `ny` 都走“选择隧道”,不再引入或恢复“选择入口节点”流程。\n- `ny` 解析兼容增强(`vite-frontend/src/pages/forward/import-format.ts`):\n - 继续支持“拼接 JSON 对象”与“按行 JSON”。\n - 增加字段别名兼容(例如 `listen_port` / `listenPort` / `port`,`dest` / `dst` / `target`)。\n - 端口字段缺失时输出 `inPort: null`,让后端自动分配。\n- 导入执行统一(`vite-frontend/src/pages/forward.tsx`):\n - `createForward` 始终使用 `selectedTunnelForImport` 作为 `tunnelId`。\n - `ny` 导入时若缺失端口,不报错,提交 `inPort: null`。\n- 文案和提示统一:\n - `ny` 格式说明里明确“按所选隧道导入,端口可省略自动分配”。\n - 错误提示聚焦字段合法性(地址/名称),不再出现入口选择相关提示。\n- 补充测试(`vite-frontend/src/pages/forward/import-format.test.ts`):\n - 覆盖端口缺失自动分配。\n - 覆盖字段别名兼容。\n - 覆盖非法地址与空名称报错。\n\n**计划文档**\n- 实施时会新建:`plans/002-ny-import-tunnel-selection-compat.md`\n- 文档内 checklist 将按完成进度逐项勾选,符合仓库计划规则。\n\n如果你确认,我下一步就按这个计划进入实际修改。", "time": { "start": 1772527753704, "end": 1772527753704 }, "id": "prt_cb2e2f565001KM31D04rJYFG1Q", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e2b4a7002N5xtl7xiPzXnE5" }, { "type": "step-finish", "reason": "stop", "snapshot": "2b8e04eb5fa138d3842c7fc8f8c70b42465c2724", "cost": 0, "tokens": { "total": 35318, "input": 34622, "output": 696, "reasoning": 251, "cache": { "read": 0, "write": 0 } }, "id": "prt_cb2e319ea001XF2knCddB1i0pi", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e2b4a7002N5xtl7xiPzXnE5" } ] }, { "info": { "role": "user", "time": { "created": 1772527822984 }, "summary": { "diffs": [ { "file": "plans/002-ny-import-tunnel-selection-compat.md", "before": "", "after": "- [x] Review current forward import flow and confirm ny import uses tunnel selection\n- [x] Define ny compatibility update with tunnel-first behavior and auto port assignment fallback\n- [x] Update ny parser to accept alias fields and optional `listen_port`\n- [x] Keep import execution bound to selected tunnel and remove entry-selection dependency from ux copy\n- [x] Update ny import help text to document optional port auto assignment\n- [x] Add parser tests for alias-field compatibility and missing-port auto assignment\n- [x] Validate updated import parser tests locally\n", "additions": 7, "deletions": 0, "status": "added" }, { "file": "vite-frontend/src/pages/forward.tsx", "before": "import type { SpeedLimitApiItem } from \"@/api/types\";\n\nimport { useState, useEffect, useMemo, useRef } from \"react\";\nimport toast from \"react-hot-toast\";\nimport {\n DndContext,\n closestCenter,\n KeyboardSensor,\n MouseSensor,\n TouchSensor,\n useSensor,\n useSensors,\n DragEndEvent,\n} from \"@dnd-kit/core\";\nimport {\n arrayMove,\n SortableContext,\n sortableKeyboardCoordinates,\n rectSortingStrategy,\n verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { useSortable } from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\n\nimport { SearchBar } from \"@/components/search-bar\";\nimport { AnimatedPage } from \"@/components/animated-page\";\nimport { Card, CardBody, CardHeader } from \"@/shadcn-bridge/heroui/card\";\nimport { Button } from \"@/shadcn-bridge/heroui/button\";\nimport { Input } from \"@/shadcn-bridge/heroui/input\";\nimport { Textarea } from \"@/shadcn-bridge/heroui/input\";\nimport { Select, SelectItem } from \"@/shadcn-bridge/heroui/select\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableColumn,\n TableHeader,\n TableRow,\n} from \"@/shadcn-bridge/heroui/table\";\nimport {\n Modal,\n ModalContent,\n ModalHeader,\n ModalBody,\n ModalFooter,\n} from \"@/shadcn-bridge/heroui/modal\";\nimport { Chip } from \"@/shadcn-bridge/heroui/chip\";\nimport { Spinner } from \"@/shadcn-bridge/heroui/spinner\";\nimport { Switch } from \"@/shadcn-bridge/heroui/switch\";\nimport { Alert } from \"@/shadcn-bridge/heroui/alert\";\nimport { Checkbox } from \"@/shadcn-bridge/heroui/checkbox\";\nimport {\n createForward,\n getForwardList,\n getSpeedLimitList,\n getPeerShareList,\n getPeerRemoteUsageList,\n updateForward,\n deleteForward,\n forceDeleteForward,\n userTunnel,\n getTunnelList,\n getNodeList,\n pauseForwardService,\n resumeForwardService,\n diagnoseForward,\n updateForwardOrder,\n getConfigByName,\n updateConfig,\n} from \"@/api\";\nimport {\n type ForwardAddressItem,\n formatInAddress,\n formatRemoteAddress,\n hasMultipleAddresses,\n resolveForwardAddressAction,\n} from \"@/pages/forward/address\";\nimport {\n buildForwardDiagnosisFallbackResult,\n getForwardDiagnosisQualityDisplay,\n type ForwardDiagnosisResult,\n} from \"@/pages/forward/diagnosis\";\nimport { diagnoseForwardStream } from \"@/api/diagnosis-stream\";\nimport {\n executeForwardBatchChangeTunnel,\n executeForwardBatchDelete,\n executeForwardBatchRedeploy,\n executeForwardBatchToggleService,\n} from \"@/pages/forward/batch-actions\";\nimport {\n convertNyItemToForwardInput,\n parseNyFormatData,\n} from \"@/pages/forward/import-format\";\nimport { buildForwardOrder, FORWARD_ORDER_KEY } from \"@/pages/forward/order\";\nimport { PageLoadingState } from \"@/components/page-state\";\nimport { useMobileBreakpoint } from \"@/hooks/useMobileBreakpoint\";\nimport { useLocalStorageState } from \"@/hooks/use-local-storage-state\";\nimport { saveOrder } from \"@/utils/order-storage\";\nimport { JwtUtil } from \"@/utils/jwt\";\n\ninterface Forward {\n id: number;\n name: string;\n tunnelId: number;\n tunnelName: string;\n inIp: string;\n inPort: number;\n remoteAddr: string;\n interfaceName?: string;\n strategy: string;\n status: number;\n inFlow: number;\n outFlow: number;\n serviceRunning: boolean;\n federationShareFlow?: number;\n createdTime: string;\n userName?: string;\n userId?: number;\n inx?: number;\n speedId?: number | null;\n}\n\ninterface Tunnel {\n id: number;\n name: string;\n type?: number;\n inIp?: string;\n inNodeId?: Array<{ nodeId: number }>;\n inNodePortSta?: number;\n inNodePortEnd?: number;\n}\n\ninterface Node {\n id: number;\n name?: string;\n serverIp?: string;\n serverIpV4?: string;\n serverIpV6?: string;\n extraIPs?: string;\n}\n\ninterface ForwardForm {\n id?: number;\n userId?: number;\n name: string;\n tunnelId: number | null;\n inPort: number | null;\n inIp: string;\n remoteAddr: string;\n interfaceName?: string;\n strategy: string;\n speedId: number | null;\n}\n\ninterface ForwardUserGroup {\n userId: number;\n userName: string;\n tunnels: ForwardTunnelGroup[];\n}\n\ninterface ForwardTunnelGroup {\n tunnelKey: string;\n tunnelName: string;\n items: Forward[];\n}\n\ntype ForwardGroupOrderMap = Record;\ntype ForwardGroupCollapsedMap = Record;\n\nconst UNKNOWN_FORWARD_USER_NAME = \"未知用户\";\nconst UNCATEGORIZED_FORWARD_TUNNEL_NAME = \"未分类\";\nconst FORWARD_COMPACT_MODE_CONFIG_KEY = \"forward_compact_mode\";\nconst FORWARD_COMPACT_MODE_EVENT = \"forwardCompactModeChanged\";\nconst FORWARD_GROUP_ORDER_CONFIG_KEY = \"forward_group_order_map\";\nconst FORWARD_GROUP_COLLAPSED_CONFIG_KEY = \"forward_group_collapsed_map\";\nconst FORWARD_GROUP_ORDER_LOCAL_STORAGE_PREFIX = \"forward-group-order\";\nconst FORWARD_GROUP_COLLAPSED_LOCAL_STORAGE_PREFIX = \"forward-group-collapsed\";\nconst FORWARD_TUNNEL_GROUP_SORTABLE_PREFIX = \"forward-tunnel-group\";\nconst FORWARD_GROUPED_TABLE_MIN_WIDTH_CLASS = \"min-w-[1320px]\";\nconst FORWARD_GROUPED_TABLE_COLUMN_CLASS = {\n select: \"w-14\",\n drag: \"w-10 pl-4\",\n name: \"w-[200px]\",\n inbound: \"w-[280px]\",\n target: \"w-[280px]\",\n strategy: \"w-[100px]\",\n totalFlow: \"w-[120px]\",\n status: \"w-[100px]\",\n actions: \"w-[144px] text-right\",\n} as const;\n\nconst normalizeForwardUserName = (userName?: string): string => {\n const normalized = (userName || UNKNOWN_FORWARD_USER_NAME).trim();\n\n return normalized || UNKNOWN_FORWARD_USER_NAME;\n};\n\nconst compareForwardUserNameAsc = (a: string, b: string): number => {\n return a.localeCompare(b, \"en\", {\n sensitivity: \"base\",\n numeric: true,\n });\n};\n\nconst normalizeForwardTunnelName = (tunnelName?: string): string => {\n const normalized = (tunnelName || \"\").trim();\n\n return normalized || UNCATEGORIZED_FORWARD_TUNNEL_NAME;\n};\n\nconst buildForwardTunnelGroupKey = (tunnelName?: string): string => {\n const normalized = normalizeForwardTunnelName(tunnelName);\n\n if (normalized === UNCATEGORIZED_FORWARD_TUNNEL_NAME) {\n return \"__uncategorized__\";\n }\n\n return normalized.toLocaleLowerCase();\n};\n\nconst compareForwardTunnelNameAsc = (a: string, b: string): number => {\n return a.localeCompare(b, \"en\", {\n sensitivity: \"base\",\n numeric: true,\n });\n};\n\nconst compareForwardTunnelGroupKeyAsc = (a: string, b: string): number => {\n const aIsUncategorized = a === \"__uncategorized__\";\n const bIsUncategorized = b === \"__uncategorized__\";\n\n if (aIsUncategorized !== bIsUncategorized) {\n return aIsUncategorized ? 1 : -1;\n }\n\n return compareForwardTunnelNameAsc(a, b);\n};\n\nconst buildForwardGroupOrderLocalKey = (tokenUserId: number): string => {\n return `${FORWARD_GROUP_ORDER_LOCAL_STORAGE_PREFIX}:u:${tokenUserId}`;\n};\n\nconst buildForwardGroupCollapsedLocalKey = (tokenUserId: number): string => {\n return `${FORWARD_GROUP_COLLAPSED_LOCAL_STORAGE_PREFIX}:u:${tokenUserId}`;\n};\n\nconst parsePreferenceMap = (\n raw: string | null,\n): Record | null => {\n if (!raw) {\n return null;\n }\n\n try {\n const parsed = JSON.parse(raw);\n\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return null;\n }\n\n return parsed as Record;\n } catch {\n return null;\n }\n};\n\nconst parseGroupOrderMap = (raw: string | null): ForwardGroupOrderMap => {\n const parsed = parsePreferenceMap(raw);\n\n if (!parsed) {\n return {};\n }\n\n const result: ForwardGroupOrderMap = {};\n\n Object.entries(parsed).forEach(([userId, value]) => {\n if (!Array.isArray(value)) {\n return;\n }\n\n const keys = value\n .map((item) => (typeof item === \"string\" ? item.trim() : \"\"))\n .filter((item) => item !== \"\");\n\n if (keys.length > 0) {\n result[userId] = Array.from(new Set(keys));\n }\n });\n\n return result;\n};\n\nconst parseGroupCollapsedMap = (\n raw: string | null,\n): ForwardGroupCollapsedMap => {\n const parsed = parsePreferenceMap(raw);\n\n if (!parsed) {\n return {};\n }\n\n const result: ForwardGroupCollapsedMap = {};\n\n Object.entries(parsed).forEach(([key, value]) => {\n if (typeof value === \"boolean\") {\n result[key] = value;\n }\n });\n\n return result;\n};\n\nconst sanitizeGroupOrderMap = (\n source: ForwardGroupOrderMap,\n availableTunnelKeysByUser: Map>,\n): ForwardGroupOrderMap => {\n const sanitized: ForwardGroupOrderMap = {};\n\n availableTunnelKeysByUser.forEach((availableKeys, userId) => {\n if (availableKeys.size === 0) {\n return;\n }\n\n const orderFromSource = source[userId.toString()] || [];\n const used = new Set();\n const merged: string[] = [];\n\n orderFromSource.forEach((key) => {\n if (!availableKeys.has(key) || used.has(key)) {\n return;\n }\n\n used.add(key);\n merged.push(key);\n });\n\n Array.from(availableKeys)\n .sort(compareForwardTunnelGroupKeyAsc)\n .forEach((key) => {\n if (!used.has(key)) {\n used.add(key);\n merged.push(key);\n }\n });\n\n if (merged.length > 0) {\n sanitized[userId.toString()] = merged;\n }\n });\n\n return sanitized;\n};\n\nconst sanitizeGroupCollapsedMap = (\n source: ForwardGroupCollapsedMap,\n availableCollapseKeys: Set,\n): ForwardGroupCollapsedMap => {\n const sanitized: ForwardGroupCollapsedMap = {};\n\n availableCollapseKeys.forEach((key) => {\n if (source[key] === true) {\n sanitized[key] = true;\n }\n });\n\n return sanitized;\n};\n\nconst buildTunnelGroupCollapseKey = (\n userId: number,\n tunnelKey: string,\n): string => {\n return `${userId}:${tunnelKey}`;\n};\n\nconst buildTunnelGroupSortableId = (\n userId: number,\n tunnelKey: string,\n): string => {\n return `${FORWARD_TUNNEL_GROUP_SORTABLE_PREFIX}:${userId}:${tunnelKey}`;\n};\n\nconst parseTunnelGroupSortableId = (\n value: unknown,\n): { userId: number; tunnelKey: string } | null => {\n if (typeof value !== \"string\") {\n return null;\n }\n\n if (!value.startsWith(`${FORWARD_TUNNEL_GROUP_SORTABLE_PREFIX}:`)) {\n return null;\n }\n\n const parts = value.split(\":\");\n\n if (parts.length < 3) {\n return null;\n }\n\n const userId = Number(parts[1]);\n const tunnelKey = parts.slice(2).join(\":\").trim();\n\n if (!Number.isFinite(userId) || tunnelKey === \"\") {\n return null;\n }\n\n return { userId, tunnelKey };\n};\n\nconst buildAvailableGroupData = (\n forwards: Forward[],\n): {\n availableTunnelKeysByUser: Map>;\n availableCollapseKeys: Set;\n} => {\n const availableTunnelKeysByUser = new Map>();\n const availableCollapseKeys = new Set();\n\n forwards.forEach((forward) => {\n const userId = forward.userId ?? 0;\n const tunnelKey = buildForwardTunnelGroupKey(forward.tunnelName);\n\n let set = availableTunnelKeysByUser.get(userId);\n\n if (!set) {\n set = new Set();\n availableTunnelKeysByUser.set(userId, set);\n }\n\n set.add(tunnelKey);\n availableCollapseKeys.add(buildTunnelGroupCollapseKey(userId, tunnelKey));\n });\n\n return { availableTunnelKeysByUser, availableCollapseKeys };\n};\n\nconst isSameStringArray = (a: string[], b: string[]): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n\n for (let i = 0; i < a.length; i += 1) {\n if (a[i] !== b[i]) {\n return false;\n }\n }\n\n return true;\n};\n\nconst isSameGroupOrderMap = (\n a: ForwardGroupOrderMap,\n b: ForwardGroupOrderMap,\n): boolean => {\n const aKeys = Object.keys(a).sort(compareForwardTunnelNameAsc);\n const bKeys = Object.keys(b).sort(compareForwardTunnelNameAsc);\n\n if (!isSameStringArray(aKeys, bKeys)) {\n return false;\n }\n\n for (const key of aKeys) {\n if (!isSameStringArray(a[key] || [], b[key] || [])) {\n return false;\n }\n }\n\n return true;\n};\n\nconst isSameGroupCollapsedMap = (\n a: ForwardGroupCollapsedMap,\n b: ForwardGroupCollapsedMap,\n): boolean => {\n const aKeys = Object.keys(a).sort(compareForwardTunnelNameAsc);\n const bKeys = Object.keys(b).sort(compareForwardTunnelNameAsc);\n\n if (!isSameStringArray(aKeys, bKeys)) {\n return false;\n }\n\n for (const key of aKeys) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n};\n\nexport default function ForwardPage() {\n const [loading, setLoading] = useState(true);\n const [forwards, setForwards] = useState([]);\n const [tunnels, setTunnels] = useState([]);\n const [allTunnels, setAllTunnels] = useState([]);\n const [nodes, setNodes] = useState([]);\n const [speedLimits, setSpeedLimits] = useState([]);\n const isMobile = useMobileBreakpoint();\n const [searchKeyword, setSearchKeyword] = useLocalStorageState(\n \"forward-search-keyword\",\n \"\",\n );\n const [isSearchVisible, setIsSearchVisible] = useState(false);\n const [compactMode, setCompactMode] = useState(false);\n\n // 显示模式状态 - 从localStorage读取,默认为平铺显示\n const [viewMode, setViewMode] = useState<\"grouped\" | \"direct\">(() => {\n try {\n const savedMode = localStorage.getItem(\"forward-view-mode\");\n\n return (savedMode as \"grouped\" | \"direct\") || \"direct\";\n } catch {\n return \"direct\";\n }\n });\n\n // 筛选状态\n const [filterUserId, setFilterUserId, resetFilterUserId] =\n useLocalStorageState(\"forward-filter-user-id\", \"all\");\n const [filterTunnelId, setFilterTunnelId, resetFilterTunnelId] =\n useLocalStorageState(\"forward-filter-tunnel-id\", \"all\");\n\n // 拖拽排序相关状态\n const [forwardOrder, setForwardOrder] = useState([]);\n\n // 模态框状态\n const [modalOpen, setModalOpen] = useState(false);\n const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);\n const [deleteModalOpen, setDeleteModalOpen] = useState(false);\n const [addressModalOpen, setAddressModalOpen] = useState(false);\n const [diagnosisModalOpen, setDiagnosisModalOpen] = useState(false);\n const [isEdit, setIsEdit] = useState(false);\n const [submitLoading, setSubmitLoading] = useState(false);\n const [deleteLoading, setDeleteLoading] = useState(false);\n const [diagnosisLoading, setDiagnosisLoading] = useState(false);\n const [forwardToDelete, setForwardToDelete] = useState(null);\n const [currentDiagnosisForward, setCurrentDiagnosisForward] =\n useState(null);\n const [diagnosisResult, setDiagnosisResult] =\n useState(null);\n const [diagnosisProgress, setDiagnosisProgress] = useState({\n total: 0,\n completed: 0,\n success: 0,\n failed: 0,\n timedOut: false,\n });\n const diagnosisAbortRef = useRef(null);\n const [addressModalTitle, setAddressModalTitle] = useState(\"\");\n const [addressList, setAddressList] = useState([]);\n\n // 导出相关状态\n const [exportModalOpen, setExportModalOpen] = useState(false);\n const [exportData, setExportData] = useState(\"\");\n const [exportLoading, setExportLoading] = useState(false);\n const [selectedTunnelForExport, setSelectedTunnelForExport] = useState<\n number | null\n >(null);\n\n // 导入相关状态\n type ImportFormat = \"flvx\" | \"ny\";\n const [importModalOpen, setImportModalOpen] = useState(false);\n const [importData, setImportData] = useState(\"\");\n const [importLoading, setImportLoading] = useState(false);\n const [importFormat, setImportFormat] = useState(\"flvx\");\n const [selectedTunnelForImport, setSelectedTunnelForImport] = useState<\n number | null\n >(null);\n const [importResults, setImportResults] = useState<\n Array<{\n line: string;\n success: boolean;\n message: string;\n forwardName?: string;\n }>\n >([]);\n\n // 表单状态\n const [form, setForm] = useState({\n name: \"\",\n tunnelId: null,\n inPort: null,\n inIp: \"\",\n remoteAddr: \"\",\n interfaceName: \"\",\n strategy: \"fifo\",\n speedId: null,\n });\n\n // 表单验证错误\n const [errors, setErrors] = useState<{ [key: string]: string }>({});\n\n // 批量操作相关状态\n const [selectMode, setSelectMode] = useState(false);\n const [selectedIds, setSelectedIds] = useState>(new Set());\n const [batchDeleteModalOpen, setBatchDeleteModalOpen] = useState(false);\n const [batchChangeTunnelModalOpen, setBatchChangeTunnelModalOpen] =\n useState(false);\n const [batchTargetTunnelId, setBatchTargetTunnelId] = useState(\n null,\n );\n const [batchLoading, setBatchLoading] = useState(false);\n const [groupOrderMap, setGroupOrderMap] = useState({});\n const [collapsedTunnelGroups, setCollapsedTunnelGroups] =\n useState({});\n const [groupPreferenceHydrated, setGroupPreferenceHydrated] = useState(false);\n const tokenUserId = JwtUtil.getUserIdFromToken();\n const tokenRoleId = JwtUtil.getRoleIdFromToken();\n const isAdmin = tokenRoleId === 0;\n\n const parseNodeIPs = (node?: Node): string[] => {\n if (!node) {\n return [];\n }\n\n const ips: string[] = [];\n const add = (value?: string) => {\n const trimmed = (value || \"\").trim();\n\n if (trimmed) {\n ips.push(trimmed);\n }\n };\n\n add(node.serverIpV4);\n add(node.serverIpV6);\n add(node.serverIp);\n\n (node.extraIPs || \"\")\n .split(\",\")\n .map((v) => v.trim())\n .filter((v) => v)\n .forEach((v) => ips.push(v));\n\n return Array.from(new Set(ips));\n };\n\n const tunnelInIpOptionMap = useMemo(() => {\n const map = new Map();\n const nodeMap = new Map(nodes.map((n) => [n.id, n]));\n\n for (const tunnel of allTunnels) {\n const collected: string[] = [];\n const entryNodes = tunnel.inNodeId || [];\n\n for (const entry of entryNodes) {\n collected.push(...parseNodeIPs(nodeMap.get(entry.nodeId)));\n }\n\n if (collected.length === 0) {\n (tunnel.inIp || \"\")\n .split(\",\")\n .map((v) => v.trim())\n .filter((v) => v)\n .forEach((v) => collected.push(v));\n }\n\n map.set(tunnel.id, Array.from(new Set(collected)));\n }\n\n return map;\n }, [allTunnels, nodes]);\n\n const currentTunnelIpOptions = useMemo(() => {\n if (!form.tunnelId) {\n return [];\n }\n\n return tunnelInIpOptionMap.get(form.tunnelId) || [];\n }, [form.tunnelId, tunnelInIpOptionMap]);\n\n useEffect(() => {\n return () => {\n diagnosisAbortRef.current?.abort();\n diagnosisAbortRef.current = null;\n };\n }, []);\n\n const persistGroupOrderToLocal = (nextOrderMap: ForwardGroupOrderMap) => {\n if (tokenUserId === null) {\n return;\n }\n\n try {\n localStorage.setItem(\n buildForwardGroupOrderLocalKey(tokenUserId),\n JSON.stringify(nextOrderMap),\n );\n } catch {}\n };\n\n const persistGroupCollapsedToLocal = (\n nextCollapsedMap: ForwardGroupCollapsedMap,\n ) => {\n if (tokenUserId === null) {\n return;\n }\n\n try {\n localStorage.setItem(\n buildForwardGroupCollapsedLocalKey(tokenUserId),\n JSON.stringify(nextCollapsedMap),\n );\n } catch {}\n };\n\n const persistGroupOrderToGlobal = async (\n nextOrderMap: ForwardGroupOrderMap,\n ): Promise => {\n if (!isAdmin || tokenUserId === null) {\n return;\n }\n\n try {\n const currentRes = await getConfigByName(FORWARD_GROUP_ORDER_CONFIG_KEY);\n const globalMap =\n parsePreferenceMap(\n currentRes.code === 0 && typeof currentRes.data?.value === \"string\"\n ? currentRes.data.value\n : null,\n ) || {};\n\n globalMap[tokenUserId.toString()] = nextOrderMap;\n\n const saveRes = await updateConfig(\n FORWARD_GROUP_ORDER_CONFIG_KEY,\n JSON.stringify(globalMap),\n );\n\n if (saveRes.code !== 0) {\n toast.error(saveRes.msg || \"保存分组排序失败\");\n }\n } catch {\n toast.error(\"保存分组排序失败\");\n }\n };\n\n const persistGroupCollapsedToGlobal = async (\n nextCollapsedMap: ForwardGroupCollapsedMap,\n ): Promise => {\n if (!isAdmin || tokenUserId === null) {\n return;\n }\n\n try {\n const currentRes = await getConfigByName(\n FORWARD_GROUP_COLLAPSED_CONFIG_KEY,\n );\n const globalMap =\n parsePreferenceMap(\n currentRes.code === 0 && typeof currentRes.data?.value === \"string\"\n ? currentRes.data.value\n : null,\n ) || {};\n\n globalMap[tokenUserId.toString()] = nextCollapsedMap;\n\n const saveRes = await updateConfig(\n FORWARD_GROUP_COLLAPSED_CONFIG_KEY,\n JSON.stringify(globalMap),\n );\n\n if (saveRes.code !== 0) {\n toast.error(saveRes.msg || \"保存分组折叠状态失败\");\n }\n } catch {\n toast.error(\"保存分组折叠状态失败\");\n }\n };\n\n useEffect(() => {\n let cancelled = false;\n\n const loadGroupPreferences = async () => {\n if (tokenUserId === null) {\n if (!cancelled) {\n setGroupOrderMap({});\n setCollapsedTunnelGroups({});\n setGroupPreferenceHydrated(true);\n }\n\n return;\n }\n\n let localOrderMap: ForwardGroupOrderMap = {};\n let localCollapsedMap: ForwardGroupCollapsedMap = {};\n\n try {\n localOrderMap = parseGroupOrderMap(\n localStorage.getItem(buildForwardGroupOrderLocalKey(tokenUserId)),\n );\n } catch {\n localOrderMap = {};\n }\n\n try {\n localCollapsedMap = parseGroupCollapsedMap(\n localStorage.getItem(buildForwardGroupCollapsedLocalKey(tokenUserId)),\n );\n } catch {\n localCollapsedMap = {};\n }\n\n if (isAdmin) {\n try {\n const [globalOrderRes, globalCollapsedRes] = await Promise.all([\n getConfigByName(FORWARD_GROUP_ORDER_CONFIG_KEY),\n getConfigByName(FORWARD_GROUP_COLLAPSED_CONFIG_KEY),\n ]);\n\n const globalOrderMap = parsePreferenceMap(\n globalOrderRes.code === 0 &&\n typeof globalOrderRes.data?.value === \"string\"\n ? globalOrderRes.data.value\n : null,\n );\n const globalCollapsedMap =\n parsePreferenceMap(\n globalCollapsedRes.code === 0 &&\n typeof globalCollapsedRes.data?.value === \"string\"\n ? globalCollapsedRes.data.value\n : null,\n );\n\n const globalOrderBucket = globalOrderMap?.[tokenUserId.toString()];\n const globalCollapsedBucket =\n globalCollapsedMap?.[tokenUserId.toString()];\n\n if (\n globalOrderBucket &&\n typeof globalOrderBucket === \"object\" &&\n !Array.isArray(globalOrderBucket)\n ) {\n localOrderMap = parseGroupOrderMap(\n JSON.stringify(globalOrderBucket),\n );\n }\n\n if (\n globalCollapsedBucket &&\n typeof globalCollapsedBucket === \"object\" &&\n !Array.isArray(globalCollapsedBucket)\n ) {\n localCollapsedMap = parseGroupCollapsedMap(\n JSON.stringify(globalCollapsedBucket),\n );\n }\n } catch {}\n }\n\n if (cancelled) {\n return;\n }\n\n setGroupOrderMap(localOrderMap);\n setCollapsedTunnelGroups(localCollapsedMap);\n persistGroupOrderToLocal(localOrderMap);\n persistGroupCollapsedToLocal(localCollapsedMap);\n setGroupPreferenceHydrated(true);\n };\n\n setGroupPreferenceHydrated(false);\n loadGroupPreferences();\n\n return () => {\n cancelled = true;\n };\n }, [tokenUserId, isAdmin]);\n\n useEffect(() => {\n const loadForwardCompactMode = async () => {\n try {\n const response = await getConfigByName(FORWARD_COMPACT_MODE_CONFIG_KEY);\n const enabled =\n response.code === 0 &&\n typeof response.data?.value === \"string\" &&\n response.data.value === \"true\";\n\n setCompactMode(enabled);\n } catch {\n setCompactMode(false);\n }\n };\n\n const handleCompactModeChanged = (event: Event) => {\n const customEvent = event as CustomEvent<{ enabled?: boolean }>;\n\n if (typeof customEvent.detail?.enabled === \"boolean\") {\n setCompactMode(customEvent.detail.enabled);\n }\n };\n\n loadForwardCompactMode();\n window.addEventListener(\n FORWARD_COMPACT_MODE_EVENT,\n handleCompactModeChanged,\n );\n\n return () => {\n window.removeEventListener(\n FORWARD_COMPACT_MODE_EVENT,\n handleCompactModeChanged,\n );\n };\n }, []);\n\n const parseShareIdFromTunnelName = (tunnelName: string): number | null => {\n const normalized = (tunnelName || \"\").trim();\n\n if (!normalized.startsWith(\"Share-\")) {\n return null;\n }\n\n const raw = normalized.slice(\"Share-\".length);\n const idx = raw.indexOf(\"-Port-\");\n\n if (idx <= 0) {\n return null;\n }\n\n const shareId = Number(raw.slice(0, idx).trim());\n\n return Number.isFinite(shareId) && shareId > 0 ? shareId : null;\n };\n\n const mergeFederationShareFlow = async (\n forwardsData: Forward[],\n ): Promise => {\n if (forwardsData.length === 0) {\n return forwardsData;\n }\n\n try {\n const [usageRes, localShareRes] = await Promise.all([\n getPeerRemoteUsageList(),\n getPeerShareList(),\n ]);\n\n const flowByShare = new Map();\n const shareIdsByTunnel = new Map>();\n\n if (usageRes.code === 0 && Array.isArray(usageRes.data)) {\n usageRes.data.forEach((item: Record) => {\n const shareId = Number(item.shareId || 0);\n const currentFlow = Number(item.currentFlow || 0);\n\n if (\n Number.isFinite(shareId) &&\n shareId > 0 &&\n Number.isFinite(currentFlow) &&\n currentFlow > 0\n ) {\n const prev = flowByShare.get(shareId) || 0;\n\n flowByShare.set(shareId, Math.max(prev, currentFlow));\n }\n\n if (Number.isFinite(shareId) && shareId > 0) {\n const bindings = Array.isArray(item.bindings)\n ? (item.bindings as Array>)\n : [];\n\n bindings.forEach((binding) => {\n const tunnelId = Number(binding.tunnelId || 0);\n const chainType = Number(binding.chainType || 0);\n\n if (!Number.isFinite(tunnelId) || tunnelId <= 0) {\n return;\n }\n\n if (Number.isFinite(chainType) && chainType !== 1) {\n return;\n }\n\n let shareSet = shareIdsByTunnel.get(tunnelId);\n\n if (!shareSet) {\n shareSet = new Set();\n shareIdsByTunnel.set(tunnelId, shareSet);\n }\n\n shareSet.add(shareId);\n });\n }\n });\n }\n\n if (localShareRes.code === 0 && Array.isArray(localShareRes.data)) {\n localShareRes.data.forEach((item: Record) => {\n const shareId = Number(item.id || 0);\n const currentFlow = Number(item.currentFlow || 0);\n\n if (\n Number.isFinite(shareId) &&\n shareId > 0 &&\n Number.isFinite(currentFlow) &&\n currentFlow > 0\n ) {\n const prev = flowByShare.get(shareId) || 0;\n\n flowByShare.set(shareId, Math.max(prev, currentFlow));\n }\n });\n }\n\n if (flowByShare.size === 0) {\n return forwardsData;\n }\n\n const resolveShareIdForForward = (forward: Forward): number | null => {\n const candidates = new Set();\n const shareIdFromName = parseShareIdFromTunnelName(\n forward.tunnelName || \"\",\n );\n\n if (shareIdFromName) {\n candidates.add(shareIdFromName);\n }\n\n const tunnelId = Number(forward.tunnelId || 0);\n const shareSetByTunnel = shareIdsByTunnel.get(tunnelId);\n\n if (shareSetByTunnel && shareSetByTunnel.size > 0) {\n shareSetByTunnel.forEach((shareId) => {\n if (Number.isFinite(shareId) && shareId > 0) {\n candidates.add(shareId);\n }\n });\n }\n\n if (candidates.size === 0) {\n return null;\n }\n\n let bestShareId: number | null = null;\n let bestFlow = 0;\n\n candidates.forEach((shareId) => {\n const shareFlow = flowByShare.get(shareId) || 0;\n\n if (shareFlow > bestFlow) {\n bestFlow = shareFlow;\n bestShareId = shareId;\n }\n });\n\n return bestShareId;\n };\n\n const resolvedShareByForwardId = new Map();\n\n forwardsData.forEach((forward) => {\n const shareId = resolveShareIdForForward(forward);\n\n if (shareId) {\n resolvedShareByForwardId.set(forward.id, shareId);\n }\n });\n\n const forwardCountByShare = new Map();\n\n forwardsData.forEach((forward) => {\n const shareId = resolvedShareByForwardId.get(forward.id) || null;\n\n if (!shareId || !flowByShare.has(shareId)) {\n return;\n }\n\n forwardCountByShare.set(\n shareId,\n (forwardCountByShare.get(shareId) || 0) + 1,\n );\n });\n\n return forwardsData.map((forward) => {\n const shareId = resolvedShareByForwardId.get(forward.id) || null;\n\n if (!shareId) {\n return { ...forward, federationShareFlow: undefined };\n }\n\n const shareFlow = flowByShare.get(shareId) || 0;\n\n if (shareFlow <= 0) {\n return { ...forward, federationShareFlow: undefined };\n }\n\n const directFlow = (forward.inFlow || 0) + (forward.outFlow || 0);\n\n if (directFlow > 0) {\n return { ...forward, federationShareFlow: undefined };\n }\n\n const count = forwardCountByShare.get(shareId) || 1;\n const estimated = Math.max(1, Math.floor(shareFlow / count));\n\n return { ...forward, federationShareFlow: estimated };\n });\n } catch {\n return forwardsData;\n }\n };\n\n const getForwardDisplayFlow = (forward: Forward): number => {\n const directFlow = (forward.inFlow || 0) + (forward.outFlow || 0);\n\n if (directFlow > 0) {\n return directFlow;\n }\n\n return forward.federationShareFlow || 0;\n };\n\n useEffect(() => {\n loadData();\n }, []);\n\n // 切换显示模式并保存到localStorage\n const handleViewModeChange = () => {\n const newMode = viewMode === \"grouped\" ? \"direct\" : \"grouped\";\n\n setViewMode(newMode);\n try {\n localStorage.setItem(\"forward-view-mode\", newMode);\n } catch {}\n };\n\n // 加载所有数据\n const loadData = async (lod = true) => {\n setLoading(lod);\n try {\n const [forwardsRes, tunnelsRes, speedLimitsRes] = await Promise.all([\n getForwardList(),\n userTunnel(),\n getSpeedLimitList(),\n ]);\n const [allTunnelsRes, nodesRes] = await Promise.allSettled([\n getTunnelList(),\n getNodeList(),\n ]);\n\n if (forwardsRes.code === 0) {\n const forwardsData =\n forwardsRes.data?.map((forward: any) => ({\n ...forward,\n serviceRunning: forward.status === 1,\n })) || [];\n\n const mergedForwards = await mergeFederationShareFlow(forwardsData);\n\n setForwards(mergedForwards);\n\n // 初始化拖拽排序顺序\n const currentUserId = JwtUtil.getUserIdFromToken();\n const { order, fromDatabase } = buildForwardOrder(\n mergedForwards,\n currentUserId,\n );\n\n setForwardOrder(order);\n\n if (fromDatabase) {\n saveOrder(FORWARD_ORDER_KEY, order);\n }\n } else {\n toast.error(forwardsRes.msg || \"获取转发列表失败\");\n }\n\n if (tunnelsRes.code === 0) {\n setTunnels(tunnelsRes.data || []);\n } else {\n }\n\n if (\n allTunnelsRes.status === \"fulfilled\" &&\n allTunnelsRes.value.code === 0\n ) {\n setAllTunnels((allTunnelsRes.value.data || []) as Tunnel[]);\n }\n\n if (nodesRes.status === \"fulfilled\" && nodesRes.value.code === 0) {\n setNodes((nodesRes.value.data || []) as Node[]);\n }\n\n if (speedLimitsRes.code === 0) {\n setSpeedLimits(speedLimitsRes.data || []);\n }\n } catch {\n toast.error(\"加载数据失败\");\n } finally {\n setLoading(false);\n }\n };\n\n // 表单验证\n const noLimitSpeedLimitIds = useMemo(() => {\n return new Set(\n speedLimits\n .filter((speedLimit) => speedLimit.name.trim() === \"不限速\")\n .map((speedLimit) => speedLimit.id),\n );\n }, [speedLimits]);\n\n const availableSpeedLimits = useMemo(() => {\n return speedLimits.filter(\n (speedLimit) => !noLimitSpeedLimitIds.has(speedLimit.id),\n );\n }, [speedLimits, noLimitSpeedLimitIds]);\n\n const normalizeSpeedId = (speedId?: number | null): number | null => {\n if (speedId === null || speedId === undefined) {\n return null;\n }\n\n return noLimitSpeedLimitIds.has(speedId) ? null : speedId;\n };\n\n const selectedSpeedId = normalizeSpeedId(form.speedId);\n\n const validateForm = (): boolean => {\n const newErrors: { [key: string]: string } = {};\n\n if (!form.name.trim()) {\n newErrors.name = \"请输入转发名称\";\n } else if (form.name.length < 2 || form.name.length > 50) {\n newErrors.name = \"转发名称长度应在2-50个字符之间\";\n }\n\n if (!form.tunnelId) {\n newErrors.tunnelId = \"请选择关联隧道\";\n }\n\n // 验证入口端口(可选,如果填写则验证)\n if (form.inPort !== null && form.inPort !== undefined) {\n const port = Number(form.inPort);\n\n if (isNaN(port) || port < 1 || port > 65535) {\n newErrors.inPort = \"端口必须在 1-65535 之间\";\n }\n }\n\n if (!form.remoteAddr.trim()) {\n newErrors.remoteAddr = \"请输入远程地址\";\n } else {\n // 验证地址格式\n const addresses = form.remoteAddr\n .split(\"\\n\")\n .map((addr) => addr.trim())\n .filter((addr) => addr);\n const ipv4Pattern =\n /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):\\d+$/;\n const ipv6FullPattern =\n /^\\[((([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:))|(([0-9a-fA-F]{1,4}:){6}(:[0-9a-fA-F]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9a-fA-F]{1,4}:){5}(((:[0-9a-fA-F]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9a-fA-F]{1,4}:){4}(((:[0-9a-fA-F]{1,4}){1,3})|((:[0-9a-fA-F]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){3}(((:[0-9a-fA-F]{1,4}){1,4})|((:[0-9a-fA-F]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){2}(((:[0-9a-fA-F]{1,4}){1,5})|((:[0-9a-fA-F]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){1}(((:[0-9a-fA-F]{1,4}){1,6})|((:[0-9a-fA-F]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9a-fA-F]{1,4}){1,7})|((:[0-9a-fA-F]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))\\]:\\d+$/;\n const domainPattern =\n /^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*:\\d+$/;\n\n for (let i = 0; i < addresses.length; i++) {\n const addr = addresses[i];\n\n if (\n !ipv4Pattern.test(addr) &&\n !ipv6FullPattern.test(addr) &&\n !domainPattern.test(addr)\n ) {\n newErrors.remoteAddr = `第${i + 1}行地址格式错误`;\n break;\n }\n }\n }\n\n setErrors(newErrors);\n\n return Object.keys(newErrors).length === 0;\n };\n\n // 新增转发\n const handleAdd = () => {\n setIsEdit(false);\n setForm({\n name: \"\",\n tunnelId: null,\n inPort: null,\n inIp: \"\",\n remoteAddr: \"\",\n interfaceName: \"\",\n strategy: \"fifo\",\n speedId: null,\n });\n setErrors({});\n setModalOpen(true);\n };\n\n // 编辑转发\n const handleEdit = (forward: Forward) => {\n setIsEdit(true);\n setForm({\n id: forward.id,\n userId: forward.userId,\n name: forward.name,\n tunnelId: forward.tunnelId,\n inPort: forward.inPort,\n inIp: forward.inIp || \"\",\n remoteAddr: forward.remoteAddr.split(\",\").join(\"\\n\"),\n interfaceName: forward.interfaceName || \"\",\n strategy: forward.strategy || \"fifo\",\n speedId: normalizeSpeedId(forward.speedId),\n });\n setErrors({});\n setModalOpen(true);\n };\n\n // 显示删除确认\n const handleDelete = (forward: Forward) => {\n setForwardToDelete(forward);\n setDeleteModalOpen(true);\n };\n\n // 确认删除转发\n const confirmDelete = async () => {\n if (!forwardToDelete) return;\n\n setDeleteLoading(true);\n try {\n const res = await deleteForward(forwardToDelete.id);\n\n if (res.code === 0) {\n toast.success(\"删除成功\");\n setDeleteModalOpen(false);\n loadData();\n } else {\n // 删除失败,询问是否强制删除\n const confirmed = window.confirm(\n `常规删除失败:${res.msg || \"删除失败\"}\\n\\n是否需要强制删除?\\n\\n⚠️ 注意:强制删除不会去验证节点端是否已经删除对应的转发服务。`,\n );\n\n if (confirmed) {\n const forceRes = await forceDeleteForward(forwardToDelete.id);\n\n if (forceRes.code === 0) {\n toast.success(\"强制删除成功\");\n setDeleteModalOpen(false);\n loadData();\n } else {\n toast.error(forceRes.msg || \"强制删除失败\");\n }\n }\n }\n } catch {\n toast.error(\"删除失败\");\n } finally {\n setDeleteLoading(false);\n }\n };\n\n // 处理隧道选择变化\n const handleTunnelChange = (tunnelId: string) => {\n const nextTunnelId = parseInt(tunnelId);\n const options = tunnelInIpOptionMap.get(nextTunnelId) || [];\n\n setForm((prev) => ({\n ...prev,\n tunnelId: nextTunnelId,\n inIp: options.includes(prev.inIp) ? prev.inIp : \"\",\n }));\n };\n\n // 提交表单\n const handleSubmit = async () => {\n if (!validateForm()) return;\n\n setSubmitLoading(true);\n try {\n const processedRemoteAddr = form.remoteAddr\n .split(\"\\n\")\n .map((addr) => addr.trim())\n .filter((addr) => addr)\n .join(\",\");\n\n const addressCount = processedRemoteAddr.split(\",\").length;\n\n let res: { code: number; msg: string };\n\n if (isEdit) {\n // 更新时确保包含必要字段\n const updateData = {\n id: form.id,\n userId: form.userId,\n name: form.name,\n tunnelId: form.tunnelId,\n inPort: form.inPort,\n inIp: form.inIp || undefined,\n remoteAddr: processedRemoteAddr,\n strategy: addressCount > 1 ? form.strategy : \"fifo\",\n speedId: normalizeSpeedId(form.speedId),\n };\n\n res = await updateForward(updateData);\n } else {\n const createData = {\n name: form.name,\n tunnelId: form.tunnelId,\n inPort: form.inPort,\n inIp: form.inIp || undefined,\n remoteAddr: processedRemoteAddr,\n strategy: addressCount > 1 ? form.strategy : \"fifo\",\n speedId: normalizeSpeedId(form.speedId),\n };\n\n res = await createForward(createData);\n }\n\n if (res.code === 0) {\n toast.success(isEdit ? \"修改成功\" : \"创建成功\");\n setModalOpen(false);\n loadData();\n } else {\n toast.error(res.msg || \"操作失败\");\n }\n } catch {\n toast.error(\"操作失败\");\n } finally {\n setSubmitLoading(false);\n }\n };\n\n // 处理服务开关\n const handleServiceToggle = async (forward: Forward) => {\n if (forward.status !== 1 && forward.status !== 0) {\n toast.error(\"转发状态异常,无法操作\");\n\n return;\n }\n\n const targetState = !forward.serviceRunning;\n\n try {\n // 乐观更新UI\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, serviceRunning: targetState } : f,\n ),\n );\n\n let res: { code: number; msg: string };\n\n if (targetState) {\n res = await resumeForwardService(forward.id);\n } else {\n res = await pauseForwardService(forward.id);\n }\n\n if (res.code === 0) {\n toast.success(targetState ? \"服务已启动\" : \"服务已暂停\");\n // 更新转发状态\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, status: targetState ? 1 : 0 } : f,\n ),\n );\n } else {\n // 操作失败,恢复UI状态\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, serviceRunning: !targetState } : f,\n ),\n );\n toast.error(res.msg || \"操作失败\");\n }\n } catch {\n // 操作失败,恢复UI状态\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, serviceRunning: !targetState } : f,\n ),\n );\n toast.error(\"网络错误,操作失败\");\n }\n };\n\n // 诊断转发\n const handleDiagnose = async (forward: Forward) => {\n diagnosisAbortRef.current?.abort();\n const abortController = new AbortController();\n\n diagnosisAbortRef.current = abortController;\n\n setCurrentDiagnosisForward(forward);\n setDiagnosisModalOpen(true);\n setDiagnosisLoading(true);\n setDiagnosisProgress({\n total: 0,\n completed: 0,\n success: 0,\n failed: 0,\n timedOut: false,\n });\n setDiagnosisResult({\n forwardName: forward.name,\n timestamp: Date.now(),\n results: [],\n });\n\n try {\n let streamErrorMessage = \"\";\n const streamResult = await diagnoseForwardStream(\n forward.id,\n {\n onStart: (payload) => {\n const startForwardName =\n typeof payload.forwardName === \"string\" &&\n payload.forwardName.trim() !== \"\"\n ? payload.forwardName\n : forward.name;\n const startTotal = Number(payload.total);\n const startItems = Array.isArray(payload.items)\n ? (payload.items as ForwardDiagnosisResult[\"results\"])\n : [];\n\n setDiagnosisResult((prev) => ({\n forwardName: startForwardName,\n timestamp: Date.now(),\n results: startItems.length > 0 ? startItems : prev?.results || [],\n }));\n if (Number.isFinite(startTotal) && startTotal >= 0) {\n setDiagnosisProgress((prev) => ({\n ...prev,\n total: startTotal,\n }));\n }\n },\n onItem: ({ result, progress }) => {\n setDiagnosisResult((prev) => {\n const base: ForwardDiagnosisResult = prev || {\n forwardName: forward.name,\n timestamp: Date.now(),\n results: [],\n };\n const nextResults = [...base.results];\n const existingIndex = nextResults.findIndex(\n (item) =>\n item.description === result.description &&\n item.nodeId === result.nodeId &&\n item.targetIp === result.targetIp &&\n item.targetPort === result.targetPort,\n );\n\n if (existingIndex >= 0) {\n nextResults[existingIndex] = {\n ...result,\n diagnosing: false,\n };\n } else {\n nextResults.push({\n ...result,\n diagnosing: false,\n });\n }\n\n return {\n ...base,\n timestamp: Date.now(),\n results: nextResults,\n };\n });\n setDiagnosisProgress({\n total: progress.total,\n completed: progress.completed,\n success: progress.success,\n failed: progress.failed,\n timedOut: Boolean(progress.timedOut),\n });\n },\n onDone: (progress) => {\n setDiagnosisProgress({\n total: progress.total,\n completed: progress.completed,\n success: progress.success,\n failed: progress.failed,\n timedOut: Boolean(progress.timedOut),\n });\n },\n onError: (message) => {\n streamErrorMessage = message;\n },\n },\n abortController.signal,\n );\n\n if (streamResult.fallback) {\n const response = await diagnoseForward(forward.id);\n\n if (response.code === 0) {\n const resultData = response.data as ForwardDiagnosisResult;\n const successCount = resultData.results.filter(\n (r) => r.success,\n ).length;\n const failedCount = resultData.results.length - successCount;\n\n setDiagnosisResult(resultData);\n setDiagnosisProgress({\n total: resultData.results.length,\n completed: resultData.results.length,\n success: successCount,\n failed: failedCount,\n timedOut: false,\n });\n } else {\n toast.error(response.msg || \"诊断失败\");\n setDiagnosisResult(\n buildForwardDiagnosisFallbackResult({\n forwardName: forward.name,\n remoteAddr: forward.remoteAddr,\n description: \"诊断失败\",\n message: response.msg || \"诊断过程中发生错误\",\n }),\n );\n setDiagnosisProgress({\n total: 1,\n completed: 1,\n success: 0,\n failed: 1,\n timedOut: false,\n });\n }\n\n return;\n }\n\n if (streamErrorMessage) {\n toast.error(streamErrorMessage);\n }\n if (streamResult.timedOut) {\n toast.error(\"诊断超时(单条30秒 / 整体2分钟),已返回当前结果\");\n }\n } catch {\n if (abortController.signal.aborted) {\n return;\n }\n toast.error(\"网络错误,请重试\");\n setDiagnosisResult(\n buildForwardDiagnosisFallbackResult({\n forwardName: forward.name,\n remoteAddr: forward.remoteAddr,\n description: \"网络错误\",\n message: \"无法连接到服务器\",\n }),\n );\n setDiagnosisProgress({\n total: 1,\n completed: 1,\n success: 0,\n failed: 1,\n timedOut: false,\n });\n } finally {\n if (diagnosisAbortRef.current === abortController) {\n diagnosisAbortRef.current = null;\n }\n setDiagnosisLoading(false);\n }\n };\n\n // 格式化流量\n const formatFlow = (value: number): string => {\n if (value === 0) return \"0 B\";\n if (value < 1024) return value + \" B\";\n if (value < 1024 * 1024) return (value / 1024).toFixed(2) + \" KB\";\n if (value < 1024 * 1024 * 1024)\n return (value / (1024 * 1024)).toFixed(2) + \" MB\";\n\n return (value / (1024 * 1024 * 1024)).toFixed(2) + \" GB\";\n };\n\n // 显示地址列表弹窗\n const showAddressModal = (\n addressString: string,\n port: number | null,\n title: string,\n ) => {\n const action = resolveForwardAddressAction(addressString, port, title);\n\n if (action.type === \"none\") {\n return;\n }\n\n if (action.type === \"copy\") {\n copyToClipboard(action.text, action.label);\n\n return;\n }\n\n setAddressList(action.items);\n setAddressModalTitle(action.title);\n setAddressModalOpen(true);\n };\n\n // 复制到剪贴板\n const copyToClipboard = async (text: string, label: string = \"内容\") => {\n try {\n await navigator.clipboard.writeText(text);\n toast.success(`已复制${label}`);\n } catch {\n toast.error(\"复制失败\");\n }\n };\n\n // 复制地址\n const copyAddress = async (addressItem: ForwardAddressItem) => {\n try {\n setAddressList((prev) =>\n prev.map((item) =>\n item.id === addressItem.id ? { ...item, copying: true } : item,\n ),\n );\n await copyToClipboard(addressItem.address, \"地址\");\n } catch {\n toast.error(\"复制失败\");\n } finally {\n setAddressList((prev) =>\n prev.map((item) =>\n item.id === addressItem.id ? { ...item, copying: false } : item,\n ),\n );\n }\n };\n\n // 复制所有地址\n const copyAllAddresses = async () => {\n if (addressList.length === 0) return;\n const allAddresses = addressList.map((item) => item.address).join(\"\\n\");\n\n await copyToClipboard(allAddresses, \"所有地址\");\n };\n\n // 导出转发数据\n const handleExport = () => {\n setSelectedTunnelForExport(null);\n setExportData(\"\");\n setExportModalOpen(true);\n };\n\n // 执行导出\n const executeExport = () => {\n if (!selectedTunnelForExport) {\n toast.error(\"请选择要导出的隧道\");\n\n return;\n }\n\n setExportLoading(true);\n\n try {\n // 获取要导出的转发列表\n const forwardsToExport = sortedForwards.filter(\n (forward) => forward.tunnelId === selectedTunnelForExport,\n );\n\n if (forwardsToExport.length === 0) {\n toast.error(\"所选隧道没有转发数据\");\n setExportLoading(false);\n\n return;\n }\n\n // 格式化导出数据:remoteAddr|name|inPort\n const exportLines = forwardsToExport.map((forward) => {\n return `${forward.remoteAddr}|${forward.name}|${forward.inPort}`;\n });\n\n const exportText = exportLines.join(\"\\n\");\n\n setExportData(exportText);\n } catch {\n toast.error(\"导出失败\");\n } finally {\n setExportLoading(false);\n }\n };\n\n // 复制导出数据\n const copyExportData = async () => {\n await copyToClipboard(exportData, \"转发数据\");\n };\n\n // 导入转发数据\n const handleImport = () => {\n setImportData(\"\");\n setImportResults([]);\n setSelectedTunnelForImport(null);\n setImportModalOpen(true);\n };\n\n // 执行导入\n const executeImport = async () => {\n if (!importData.trim()) {\n toast.error(\"请输入要导入的数据\");\n\n return;\n }\n\n if (!selectedTunnelForImport) {\n toast.error(\"请选择要导入的隧道\");\n\n return;\n }\n\n setImportLoading(true);\n setImportResults([]);\n\n try {\n if (importFormat === \"ny\") {\n const parsedItems = parseNyFormatData(importData);\n\n if (parsedItems.length === 0) {\n toast.error(\"未解析到有效的ny格式数据\");\n\n setImportLoading(false);\n\n return;\n }\n\n for (const item of parsedItems) {\n if (item.error) {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: item.error || \"解析失败\",\n },\n ...prev,\n ]);\n\n continue;\n }\n\n if (!item.parsed) {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: \"解析失败\",\n },\n ...prev,\n ]);\n\n continue;\n }\n\n const parsedNyItem = item.parsed;\n const nyForwardInput = convertNyItemToForwardInput(parsedNyItem);\n\n try {\n const response = await createForward({\n name: nyForwardInput.name,\n tunnelId: selectedTunnelForImport,\n inPort: nyForwardInput.inPort,\n remoteAddr: nyForwardInput.remoteAddr,\n strategy: nyForwardInput.strategy,\n });\n\n if (response.code === 0) {\n setImportResults((prev) => [\n {\n line: item.line,\n success: true,\n message: `创建成功 (${parsedNyItem.dest.length}个目标)`,\n forwardName: nyForwardInput.name,\n },\n ...prev,\n ]);\n } else {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: response.msg || \"创建失败\",\n },\n ...prev,\n ]);\n }\n } catch {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: \"网络错误,创建失败\",\n },\n ...prev,\n ]);\n }\n }\n } else {\n const lines = importData\n .trim()\n .split(\"\\n\")\n .filter((line) => line.trim());\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n const parts = line.split(\"|\");\n\n if (parts.length < 2) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"格式错误:需要至少包含目标地址和转发名称\",\n },\n ...prev,\n ]);\n continue;\n }\n\n const [remoteAddr, name, inPort] = parts;\n\n if (!remoteAddr.trim() || !name.trim()) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"目标地址和转发名称不能为空\",\n },\n ...prev,\n ]);\n continue;\n }\n\n const addresses = remoteAddr.trim().split(\",\");\n const addressPattern = /^[^:]+:\\d+$/;\n const isValidFormat = addresses.every((addr) =>\n addressPattern.test(addr.trim()),\n );\n\n if (!isValidFormat) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message:\n \"目标地址格式错误,应为 地址:端口 格式,多个地址用逗号分隔\",\n },\n ...prev,\n ]);\n continue;\n }\n\n try {\n let portNumber: number | null = null;\n\n if (inPort && inPort.trim()) {\n const port = parseInt(inPort.trim());\n\n if (isNaN(port) || port < 1 || port > 65535) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"入口端口格式错误,应为1-65535之间的数字\",\n },\n ...prev,\n ]);\n continue;\n }\n portNumber = port;\n }\n\n const response = await createForward({\n name: name.trim(),\n tunnelId: selectedTunnelForImport,\n inPort: portNumber,\n remoteAddr: remoteAddr.trim(),\n strategy: \"fifo\",\n });\n\n if (response.code === 0) {\n setImportResults((prev) => [\n {\n line,\n success: true,\n message: \"创建成功\",\n forwardName: name.trim(),\n },\n ...prev,\n ]);\n } else {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: response.msg || \"创建失败\",\n },\n ...prev,\n ]);\n }\n } catch {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"网络错误,创建失败\",\n },\n ...prev,\n ]);\n }\n }\n }\n\n toast.success(\"导入执行完成\");\n\n await loadData(false);\n } catch {\n toast.error(\"导入过程中发生错误\");\n } finally {\n setImportLoading(false);\n }\n };\n\n // 获取状态显示\n const getStatusDisplay = (status: number) => {\n switch (status) {\n case 1:\n return { color: \"success\", text: \"正常\" };\n case 0:\n return { color: \"warning\", text: \"暂停\" };\n case -1:\n return { color: \"danger\", text: \"异常\" };\n default:\n return { color: \"default\", text: \"未知\" };\n }\n };\n\n // 获取策略显示\n const getStrategyDisplay = (strategy: string) => {\n switch (strategy) {\n case \"fifo\":\n return { color: \"primary\", text: \"主备\" };\n case \"round\":\n return { color: \"success\", text: \"轮询\" };\n case \"rand\":\n return { color: \"warning\", text: \"随机\" };\n default:\n return { color: \"default\", text: \"未知\" };\n }\n };\n\n // 获取地址数量\n const getAddressCount = (addressString: string): number => {\n if (!addressString) return 0;\n const addresses = addressString\n .split(\"\\n\")\n .map((addr) => addr.trim())\n .filter((addr) => addr);\n\n return addresses.length;\n };\n\n // 处理拖拽结束\n const handleDragEnd = async (event: DragEndEvent) => {\n const { active, over } = event;\n\n if (!active || !over || active.id === over.id) return;\n\n const activeGroup = parseTunnelGroupSortableId(active.id);\n const overGroup = parseTunnelGroupSortableId(over.id);\n\n if (activeGroup && overGroup) {\n if (compactMode || !groupPreferenceHydrated) {\n return;\n }\n\n if (activeGroup.userId !== overGroup.userId) {\n return;\n }\n\n const userIdKey = activeGroup.userId.toString();\n const currentOrder = groupOrderMap[userIdKey] || [];\n const oldIndex = currentOrder.indexOf(activeGroup.tunnelKey);\n const newIndex = currentOrder.indexOf(overGroup.tunnelKey);\n\n if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) {\n return;\n }\n\n const moved = arrayMove(currentOrder, oldIndex, newIndex);\n const nextOrderMap: ForwardGroupOrderMap = {\n ...groupOrderMap,\n [userIdKey]: moved,\n };\n\n setGroupOrderMap(nextOrderMap);\n persistGroupOrderToLocal(nextOrderMap);\n void persistGroupOrderToGlobal(nextOrderMap);\n\n return;\n }\n\n // 确保 forwardOrder 存在且有效\n if (!forwardOrder || forwardOrder.length === 0) return;\n\n const activeId = Number(active.id);\n const overId = Number(over.id);\n\n // 检查 ID 是否有效\n if (isNaN(activeId) || isNaN(overId)) return;\n\n const activeForward = forwards.find((forward) => forward.id === activeId);\n const overForward = forwards.find((forward) => forward.id === overId);\n const activeUserId = activeForward?.userId ?? 0;\n const overUserId = overForward?.userId ?? 0;\n const activeTunnelGroupKey = buildForwardTunnelGroupKey(\n activeForward?.tunnelName,\n );\n const overTunnelGroupKey = buildForwardTunnelGroupKey(\n overForward?.tunnelName,\n );\n\n // 非精简模式仅允许在同一用户+隧道分组内拖拽,避免混排\n if (!compactMode) {\n if (\n activeUserId !== overUserId ||\n activeTunnelGroupKey !== overTunnelGroupKey\n ) {\n return;\n }\n }\n\n const oldIndex = forwardOrder.indexOf(activeId);\n const newIndex = forwardOrder.indexOf(overId);\n\n if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {\n const newOrder = arrayMove(forwardOrder, oldIndex, newIndex);\n\n setForwardOrder(newOrder);\n\n saveOrder(FORWARD_ORDER_KEY, newOrder);\n\n // 持久化到数据库\n try {\n const forwardsToUpdate = newOrder.map((id, index) => ({\n id,\n inx: index,\n }));\n\n const response = await updateForwardOrder({\n forwards: forwardsToUpdate,\n });\n\n if (response.code === 0) {\n // 更新本地数据中的 inx 字段\n setForwards((prev) =>\n prev.map((forward) => {\n const updatedForward = forwardsToUpdate.find(\n (f) => f.id === forward.id,\n );\n\n if (updatedForward) {\n return { ...forward, inx: updatedForward.inx };\n }\n\n return forward;\n }),\n );\n } else {\n toast.error(\"保存排序失败:\" + (response.msg || \"未知错误\"));\n }\n } catch {\n toast.error(\"保存排序失败,请重试\");\n }\n }\n };\n\n const toggleSelectMode = () => {\n setSelectMode(!selectMode);\n if (selectMode) {\n setSelectedIds(new Set());\n }\n };\n\n const toggleSelect = (id: number) => {\n const newSet = new Set(selectedIds);\n\n if (newSet.has(id)) {\n newSet.delete(id);\n } else {\n newSet.add(id);\n }\n setSelectedIds(newSet);\n };\n\n const selectAll = () => {\n const allIds = sortedForwards.map((f) => f.id);\n\n setSelectedIds(new Set(allIds));\n };\n\n const deselectAll = () => {\n setSelectedIds(new Set());\n };\n\n const handleBatchDelete = async () => {\n if (selectedIds.size === 0) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchDelete(Array.from(selectedIds));\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n if (outcome.closeDeleteModal) {\n setBatchDeleteModalOpen(false);\n }\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n const handleBatchToggleService = async (enable: boolean) => {\n if (selectedIds.size === 0) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchToggleService(\n Array.from(selectedIds),\n enable,\n );\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n const handleBatchRedeploy = async () => {\n if (selectedIds.size === 0) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchRedeploy(\n Array.from(selectedIds),\n );\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n const handleBatchChangeTunnel = async () => {\n if (selectedIds.size === 0 || !batchTargetTunnelId) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchChangeTunnel(\n Array.from(selectedIds),\n batchTargetTunnelId,\n );\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n if (outcome.closeChangeTunnelModal) {\n setBatchChangeTunnelModalOpen(false);\n }\n if (outcome.resetTargetTunnel) {\n setBatchTargetTunnelId(null);\n }\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n // 传感器配置 - 使用默认配置避免错误\n const sensors = useSensors(\n useSensor(MouseSensor, {\n activationConstraint: {\n distance: 8,\n },\n }),\n useSensor(TouchSensor, {\n activationConstraint: {\n delay: 250,\n tolerance: 8,\n },\n }),\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n }),\n );\n\n // 根据排序顺序获取转发列表\n const orderedForwards = useMemo((): Forward[] => {\n // 确保 forwards 数组存在且有效\n if (!forwards || forwards.length === 0) {\n return [];\n }\n\n let filteredForwards = forwards;\n\n if (filterUserId !== \"all\") {\n const targetUserId = parseInt(filterUserId);\n\n filteredForwards = filteredForwards.filter(\n (f) => f.userId === targetUserId || (targetUserId === 0 && !f.userId),\n );\n }\n if (filterTunnelId !== \"all\") {\n const targetTunnelId = parseInt(filterTunnelId);\n\n filteredForwards = filteredForwards.filter(\n (f) => f.tunnelId === targetTunnelId,\n );\n }\n\n if (searchKeyword.trim()) {\n const lowerKeyword = searchKeyword.toLowerCase();\n\n filteredForwards = filteredForwards.filter(\n (f) =>\n (f.name && f.name.toLowerCase().includes(lowerKeyword)) ||\n (f.remoteAddr && f.remoteAddr.toLowerCase().includes(lowerKeyword)) ||\n (f.userName && f.userName.toLowerCase().includes(lowerKeyword)),\n );\n }\n\n // 确保过滤后的转发列表有效\n if (!filteredForwards || filteredForwards.length === 0) {\n return [];\n }\n\n // 优先使用数据库中的 inx 字段进行排序\n const sortedByDb = [...filteredForwards].sort((a, b) => {\n const aInx = a.inx ?? 0;\n const bInx = b.inx ?? 0;\n\n if (aInx !== bInx) {\n return aInx - bInx;\n }\n\n return (a.id ?? 0) - (b.id ?? 0);\n });\n\n // 如果数据库中没有排序信息,则使用本地存储的顺序\n if (\n forwardOrder &&\n forwardOrder.length > 0 &&\n sortedByDb.every((f) => f.inx === undefined || f.inx === 0)\n ) {\n const forwardMap = new Map(filteredForwards.map((f) => [f.id, f]));\n const localSortedForwards: Forward[] = [];\n\n forwardOrder.forEach((id) => {\n const forward = forwardMap.get(id);\n\n if (forward) {\n localSortedForwards.push(forward);\n }\n });\n\n // 添加不在排序列表中的转发(新添加的)\n filteredForwards.forEach((forward) => {\n if (!forwardOrder.includes(forward.id)) {\n localSortedForwards.push(forward);\n }\n });\n\n return localSortedForwards;\n }\n\n return sortedByDb;\n }, [forwards, forwardOrder, filterUserId, filterTunnelId, searchKeyword]);\n\n const availableGroupData = useMemo(\n () => buildAvailableGroupData(forwards),\n [forwards],\n );\n\n const sanitizedGroupOrderMap = useMemo(\n () =>\n sanitizeGroupOrderMap(\n groupOrderMap,\n availableGroupData.availableTunnelKeysByUser,\n ),\n [groupOrderMap, availableGroupData],\n );\n\n const sanitizedCollapsedTunnelGroups = useMemo(\n () =>\n sanitizeGroupCollapsedMap(\n collapsedTunnelGroups,\n availableGroupData.availableCollapseKeys,\n ),\n [collapsedTunnelGroups, availableGroupData],\n );\n\n useEffect(() => {\n if (!groupPreferenceHydrated || tokenUserId === null) {\n return;\n }\n\n if (!isSameGroupOrderMap(groupOrderMap, sanitizedGroupOrderMap)) {\n setGroupOrderMap(sanitizedGroupOrderMap);\n persistGroupOrderToLocal(sanitizedGroupOrderMap);\n void persistGroupOrderToGlobal(sanitizedGroupOrderMap);\n }\n\n if (\n !isSameGroupCollapsedMap(\n collapsedTunnelGroups,\n sanitizedCollapsedTunnelGroups,\n )\n ) {\n setCollapsedTunnelGroups(sanitizedCollapsedTunnelGroups);\n persistGroupCollapsedToLocal(sanitizedCollapsedTunnelGroups);\n void persistGroupCollapsedToGlobal(sanitizedCollapsedTunnelGroups);\n }\n }, [\n groupPreferenceHydrated,\n tokenUserId,\n groupOrderMap,\n sanitizedGroupOrderMap,\n collapsedTunnelGroups,\n sanitizedCollapsedTunnelGroups,\n ]);\n\n const groupedForwards = useMemo((): ForwardUserGroup[] => {\n if (orderedForwards.length === 0) {\n return [];\n }\n\n type MutableForwardUserGroup = {\n userId: number;\n userName: string;\n tunnelMap: Map;\n };\n\n const userGroupMap = new Map();\n\n orderedForwards.forEach((forward) => {\n const userId = forward.userId ?? 0;\n const userName = normalizeForwardUserName(forward.userName);\n const tunnelName = normalizeForwardTunnelName(forward.tunnelName);\n const tunnelKey = buildForwardTunnelGroupKey(forward.tunnelName);\n\n let existingGroup = userGroupMap.get(userId);\n\n if (!existingGroup) {\n existingGroup = {\n userId,\n userName,\n tunnelMap: new Map(),\n };\n userGroupMap.set(userId, existingGroup);\n } else if (\n existingGroup.userName === UNKNOWN_FORWARD_USER_NAME &&\n userName !== UNKNOWN_FORWARD_USER_NAME\n ) {\n existingGroup.userName = userName;\n }\n\n const existingTunnelGroup = existingGroup.tunnelMap.get(tunnelKey);\n\n if (!existingTunnelGroup) {\n existingGroup.tunnelMap.set(tunnelKey, {\n tunnelKey,\n tunnelName,\n items: [forward],\n });\n\n return;\n }\n\n existingTunnelGroup.items.push(forward);\n\n if (\n existingTunnelGroup.tunnelName === UNCATEGORIZED_FORWARD_TUNNEL_NAME &&\n tunnelName !== UNCATEGORIZED_FORWARD_TUNNEL_NAME\n ) {\n existingTunnelGroup.tunnelName = tunnelName;\n }\n });\n\n const groups = Array.from(userGroupMap.values()).map((group) => {\n const tunnels = Array.from(group.tunnelMap.values());\n const tunnelOrder = sanitizedGroupOrderMap[group.userId.toString()] || [];\n const tunnelOrderIndex = new Map();\n\n tunnelOrder.forEach((key, index) => {\n tunnelOrderIndex.set(key, index);\n });\n\n tunnels.sort((a, b) => {\n const aIndex = tunnelOrderIndex.get(a.tunnelKey);\n const bIndex = tunnelOrderIndex.get(b.tunnelKey);\n\n if (aIndex !== undefined || bIndex !== undefined) {\n if (aIndex === undefined) {\n return 1;\n }\n\n if (bIndex === undefined) {\n return -1;\n }\n\n return aIndex - bIndex;\n }\n\n const nameCompare = compareForwardTunnelNameAsc(\n a.tunnelName,\n b.tunnelName,\n );\n\n if (nameCompare !== 0) {\n return nameCompare;\n }\n\n return compareForwardTunnelNameAsc(a.tunnelKey, b.tunnelKey);\n });\n\n return {\n userId: group.userId,\n userName: group.userName,\n tunnels,\n };\n });\n\n groups.sort((a, b) => {\n if (isAdmin && tokenUserId !== null) {\n const aIsSelf = a.userId === tokenUserId;\n const bIsSelf = b.userId === tokenUserId;\n\n if (aIsSelf !== bIsSelf) {\n return aIsSelf ? -1 : 1;\n }\n }\n\n const nameCompare = compareForwardUserNameAsc(a.userName, b.userName);\n\n if (nameCompare !== 0) {\n return nameCompare;\n }\n\n return a.userId - b.userId;\n });\n\n return groups;\n }, [orderedForwards, isAdmin, tokenUserId, sanitizedGroupOrderMap]);\n\n const sortedForwards = useMemo(() => {\n if (compactMode) {\n return orderedForwards;\n }\n\n return groupedForwards.flatMap((group) =>\n group.tunnels.flatMap((tunnel) => tunnel.items),\n );\n }, [compactMode, orderedForwards, groupedForwards]);\n\n const sortableForwardIds = useMemo(\n () => sortedForwards.map((f) => f.id).filter((id) => id > 0),\n [sortedForwards],\n );\n\n const toggleTunnelGroupCollapsed = (userId: number, tunnelKey: string) => {\n const collapseKey = buildTunnelGroupCollapseKey(userId, tunnelKey);\n const nextCollapsedMap: ForwardGroupCollapsedMap = {\n ...sanitizedCollapsedTunnelGroups,\n };\n\n if (nextCollapsedMap[collapseKey] === true) {\n delete nextCollapsedMap[collapseKey];\n } else {\n nextCollapsedMap[collapseKey] = true;\n }\n\n setCollapsedTunnelGroups(nextCollapsedMap);\n persistGroupCollapsedToLocal(nextCollapsedMap);\n void persistGroupCollapsedToGlobal(nextCollapsedMap);\n };\n\n const SortableTunnelGroupContainer = ({\n groupUserId,\n tunnel,\n collapsed,\n onToggleCollapsed,\n wrapperClassName,\n headerClassName,\n titleClassName,\n countClassName,\n bodyClassName,\n children,\n }: {\n groupUserId: number;\n tunnel: ForwardTunnelGroup;\n collapsed: boolean;\n onToggleCollapsed: () => void;\n wrapperClassName: string;\n headerClassName: string;\n titleClassName: string;\n countClassName: string;\n bodyClassName: string;\n children: React.ReactNode;\n }) => {\n const sortableId = buildTunnelGroupSortableId(\n groupUserId,\n tunnel.tunnelKey,\n );\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: sortableId });\n\n const style: React.CSSProperties = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.55 : 1,\n willChange: isDragging ? \"transform\" : undefined,\n zIndex: isDragging ? 1 : undefined,\n };\n\n return (\n
\n
\n
\n \n \n \n \n \n {tunnel.tunnelName}\n
\n
\n {tunnel.items.length} 条转发\n \n \n \n \n
\n
\n
\n {!collapsed &&
{children}
}\n
\n );\n };\n\n // 可拖拽的转发卡片组件\n const SortableForwardCard = ({ forward }: { forward: Forward }) => {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: forward.id });\n\n const style: React.CSSProperties = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.5 : 1,\n willChange: isDragging ? \"transform\" : undefined,\n };\n\n return (\n
\n {renderForwardCard(forward, listeners)}\n
\n );\n };\n\n // 生成用作筛选项的用户和隧道列表\n const uniqueUsers = useMemo(() => {\n const userMap = new Map();\n\n forwards.forEach((f) => {\n const uId = f.userId ?? 0;\n const userName = normalizeForwardUserName(f.userName);\n const existingUser = userMap.get(uId);\n\n if (!existingUser) {\n userMap.set(uId, { id: uId, name: userName });\n\n return;\n }\n\n if (\n existingUser.name === UNKNOWN_FORWARD_USER_NAME &&\n userName !== UNKNOWN_FORWARD_USER_NAME\n ) {\n existingUser.name = userName;\n }\n });\n\n const users = Array.from(userMap.values());\n\n users.sort((a, b) => {\n if (isAdmin && tokenUserId !== null) {\n const aIsSelf = a.id === tokenUserId;\n const bIsSelf = b.id === tokenUserId;\n\n if (aIsSelf !== bIsSelf) {\n return aIsSelf ? -1 : 1;\n }\n }\n\n const nameCompare = compareForwardUserNameAsc(a.name, b.name);\n\n if (nameCompare !== 0) {\n return nameCompare;\n }\n\n return a.id - b.id;\n });\n\n return users;\n }, [forwards, isAdmin, tokenUserId]);\n\n // 可拖拽的表格行组件\n const SortableTableRow = ({\n forward,\n selectMode,\n selectedIds,\n toggleSelect,\n getStrategyDisplay,\n formatInAddress,\n formatRemoteAddress,\n handleServiceToggle,\n handleEdit,\n handleDelete,\n handleDiagnose,\n showAddressModal,\n hasMultipleAddresses,\n formatFlow,\n }: any) => {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: forward.id });\n\n const style = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.5 : 1,\n backgroundColor: isDragging ? \"var(--nextui-default-100)\" : undefined,\n };\n\n const strategyDisplay = getStrategyDisplay(forward.strategy);\n\n return (\n \n {selectMode && (\n \n toggleSelect(forward.id)}\n />\n \n )}\n \n \n \n \n \n
\n \n \n {forward.name}\n \n \n \n showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n }\n >\n {formatInAddress(forward.inIp, forward.inPort)}\n \n \n \n \n showAddressModal(forward.remoteAddr, null, \"目标地址\")\n }\n >\n {formatRemoteAddress(forward.remoteAddr)}\n \n \n \n \n {strategyDisplay.text}\n \n \n \n \n {formatFlow(getForwardDisplayFlow(forward))}\n \n \n \n
\n handleServiceToggle(forward)}\n />\n
\n
\n \n
\n handleEdit(forward)}\n >\n \n \n \n \n handleDiagnose(forward)}\n >\n \n \n \n \n handleDelete(forward)}\n >\n \n \n \n \n
\n
\n \n );\n };\n\n const SortableCompactTableRow = ({\n forward,\n selectMode,\n selectedIds,\n toggleSelect,\n getStrategyDisplay,\n formatInAddress,\n formatRemoteAddress,\n handleServiceToggle,\n handleEdit,\n handleDelete,\n handleDiagnose,\n showAddressModal,\n hasMultipleAddresses,\n formatFlow,\n }: any) => {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: forward.id });\n\n const style = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.5 : 1,\n backgroundColor: isDragging ? \"var(--nextui-default-100)\" : undefined,\n };\n\n const strategyDisplay = getStrategyDisplay(forward.strategy);\n\n return (\n \n {selectMode && (\n \n toggleSelect(forward.id)}\n />\n \n )}\n \n \n \n \n \n \n \n \n \n {forward.userName || \"未知用户\"}\n \n \n \n {forward.name}\n \n \n \n \n {forward.tunnelName}\n \n \n \n \n \n showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n }\n >\n {formatInAddress(forward.inIp, forward.inPort)}\n \n \n \n \n showAddressModal(forward.remoteAddr, null, \"目标地址\")\n }\n >\n {formatRemoteAddress(forward.remoteAddr)}\n \n \n \n \n {strategyDisplay.text}\n \n \n \n \n {formatFlow(getForwardDisplayFlow(forward))}\n \n \n \n
\n handleServiceToggle(forward)}\n />\n
\n
\n \n
\n handleEdit(forward)}\n >\n \n \n \n \n handleDiagnose(forward)}\n >\n \n \n \n \n handleDelete(forward)}\n >\n \n \n \n \n
\n
\n
\n );\n };\n\n // 渲染转发卡片\n const renderForwardCard = (forward: Forward, listeners?: any) => {\n const statusDisplay = getStatusDisplay(forward.status);\n const strategyDisplay = getStrategyDisplay(forward.strategy);\n\n return (\n \n \n
\n {selectMode && (\n toggleSelect(forward.id)}\n />\n )}\n
\n

\n {forward.name}\n

\n

\n {normalizeForwardTunnelName(forward.tunnelName)}\n

\n
\n
\n {viewMode === \"direct\" && (\n \n \n \n \n
\n )}\n handleServiceToggle(forward)}\n />\n \n {statusDisplay.text}\n \n
\n \n
\n\n \n
\n {/* 地址信息 */}\n
\n \n showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n }\n >\n
\n
\n \n 入口:\n \n \n {formatInAddress(forward.inIp, forward.inPort)}\n \n
\n {hasMultipleAddresses(forward.inIp) && (\n \n \n \n )}\n
\n \n\n \n showAddressModal(forward.remoteAddr, null, \"目标地址\")\n }\n >\n
\n
\n \n 目标:\n \n \n {formatRemoteAddress(forward.remoteAddr)}\n \n
\n {hasMultipleAddresses(forward.remoteAddr) && (\n \n \n \n )}\n
\n \n
\n\n {/* 统计信息 */}\n
\n \n {strategyDisplay.text}\n \n {(forward.inFlow || 0) + (forward.outFlow || 0) > 0 ? (\n <>\n
\n \n ↑{formatFlow(forward.inFlow || 0)}\n \n
\n \n ↓{formatFlow(forward.outFlow || 0)}\n \n \n ) : (forward.federationShareFlow || 0) > 0 ? (\n \n 共享 {formatFlow(forward.federationShareFlow || 0)}\n \n ) : (\n \n 总流量 {formatFlow(0)}\n \n )}\n
\n
\n\n
\n \n \n \n }\n variant=\"flat\"\n onPress={() => handleEdit(forward)}\n >\n 编辑\n \n \n \n \n }\n variant=\"flat\"\n onPress={() => handleDiagnose(forward)}\n >\n 诊断\n \n \n \n \n \n }\n variant=\"flat\"\n onPress={() => handleDelete(forward)}\n >\n 删除\n \n
\n
\n \n );\n };\n\n if (loading || !groupPreferenceHydrated) {\n return ;\n }\n\n return (\n \n {/* 页面头部 */}\n
\n
\n setIsSearchVisible(false)}\n onOpen={() => setIsSearchVisible(true)}\n />\n
\n
\n
*]:shrink-0\">\n {selectMode ? (\n <>\n \n 已选择 {selectedIds.size} 项\n \n \n 全选\n \n \n 清空\n \n setBatchDeleteModalOpen(true)}\n >\n 删除\n \n handleBatchToggleService(false)}\n >\n 停用\n \n handleBatchToggleService(true)}\n >\n 启用\n \n \n 下发\n \n setBatchChangeTunnelModalOpen(true)}\n >\n 隧道\n \n \n 退出\n \n \n ) : (\n <>\n {/* 筛选按钮 */}\n setIsFilterModalOpen(true)}\n >\n \n \n \n {(filterUserId !== \"all\" || filterTunnelId !== \"all\") && (\n \n )}\n \n {/* 显示模式切换按钮 */}\n \n {viewMode === \"grouped\" ? (\n \n \n \n ) : (\n \n \n \n )}\n \n\n {/* 导入按钮 */}\n \n 导入\n \n\n {/* 导出按钮 */}\n \n 导出\n \n\n \n 批量\n \n\n \n 新增\n \n \n )}\n
\n
\n
\n\n {/* 根据显示模式渲染不同内容 */}\n {compactMode ? (\n viewMode === \"grouped\" ? (\n sortedForwards.length > 0 ? (\n
\n \n \n \n {selectMode && (\n 选择\n )}\n \n 用户\n 名称\n 隧道\n 入口\n 目标\n 策略\n 总流量\n 状态\n 操作\n \n \n {(forward) => (\n \n \n \n )}\n \n \n \n
\n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )\n ) : sortedForwards.length > 0 ? (\n {}}\n >\n \n
\n {sortedForwards.map((forward) =>\n forward && forward.id ? (\n \n ) : null,\n )}\n
\n \n \n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )\n ) : viewMode === \"grouped\" ? (\n sortedForwards.length > 0 ? (\n
\n {groupedForwards.map((group) => {\n const isSelfGroup =\n isAdmin && tokenUserId !== null && group.userId === tokenUserId;\n const groupForwardCount = group.tunnels.reduce(\n (total, tunnel) => total + tunnel.items.length,\n 0,\n );\n\n return (\n \n
\n
\n \n {group.userName}\n \n {isSelfGroup && (\n \n 管理员本人\n \n )}\n
\n \n {groupForwardCount} 条转发\n \n
\n\n
\n \n \n buildTunnelGroupSortableId(\n group.userId,\n tunnel.tunnelKey,\n ),\n )}\n strategy={verticalListSortingStrategy}\n >\n {group.tunnels.map((tunnel) => {\n const tunnelSortableForwardIds = tunnel.items\n .map((item) => item.id)\n .filter((id) => id > 0);\n const collapsed =\n sanitizedCollapsedTunnelGroups[\n buildTunnelGroupCollapseKey(\n group.userId,\n tunnel.tunnelKey,\n )\n ] === true;\n\n return (\n \n toggleTunnelGroupCollapsed(\n group.userId,\n tunnel.tunnelKey,\n )\n }\n >\n \n \n \n {selectMode && (\n \n 选择\n \n )}\n \n \n 名称\n \n \n 入口\n \n \n 目标\n \n \n 策略\n \n \n 总流量\n \n \n 状态\n \n \n 操作\n \n \n \n {(forward) => (\n \n \n \n )}\n \n \n \n \n );\n })}\n \n \n
\n
\n );\n })}\n \n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )\n ) : sortedForwards.length > 0 ? (\n
\n {groupedForwards.map((group) => {\n const isSelfGroup =\n isAdmin && tokenUserId !== null && group.userId === tokenUserId;\n const groupForwardCount = group.tunnels.reduce(\n (total, tunnel) => total + tunnel.items.length,\n 0,\n );\n\n return (\n \n
\n
\n \n {group.userName}\n \n {isSelfGroup && (\n \n 管理员本人\n \n )}\n
\n \n {groupForwardCount} 条转发\n \n
\n\n
\n \n \n buildTunnelGroupSortableId(\n group.userId,\n tunnel.tunnelKey,\n ),\n )}\n strategy={verticalListSortingStrategy}\n >\n {group.tunnels.map((tunnel) => {\n const tunnelSortableForwardIds = tunnel.items\n .map((item) => item.id)\n .filter((id) => id > 0);\n const collapsed =\n sanitizedCollapsedTunnelGroups[\n buildTunnelGroupCollapseKey(\n group.userId,\n tunnel.tunnelKey,\n )\n ] === true;\n\n return (\n \n toggleTunnelGroupCollapsed(\n group.userId,\n tunnel.tunnelKey,\n )\n }\n >\n {}}\n >\n \n
\n {tunnel.items.map((forward) =>\n forward && forward.id ? (\n \n ) : null,\n )}\n
\n \n \n \n );\n })}\n \n \n
\n
\n );\n })}\n \n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )}\n\n {/* 新增/编辑模态框 */}\n \n \n {(onClose) => (\n <>\n \n

\n {isEdit ? \"编辑转发\" : \"新增转发\"}\n

\n

\n {isEdit ? \"修改现有转发配置的信息\" : \"创建新的转发配置\"}\n

\n
\n \n
\n \n setForm((prev) => ({ ...prev, name: e.target.value }))\n }\n />\n\n {\n const selectedKey = Array.from(keys)[0] as\n | string\n | undefined;\n\n setForm((prev) => ({\n ...prev,\n speedId: selectedKey ? Number(selectedKey) : null,\n }));\n }}\n >\n {availableSpeedLimits.map((speedLimit) => (\n \n {speedLimit.name}\n \n ))}\n \n\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n if (selectedKey) {\n handleTunnelChange(selectedKey);\n }\n }}\n >\n {tunnels.map((tunnel) => (\n {tunnel.name}\n ))}\n \n\n {\n const value = e.target.value;\n\n setForm((prev) => ({\n ...prev,\n inPort: value ? parseInt(value) : null,\n }));\n }}\n />\n\n 0\n ? \"选择入口监听IP\"\n : \"当前隧道入口节点暂无可选IP\"\n : \"请先选择隧道\"\n }\n selectedKeys={[form.inIp || \"__default__\"]}\n variant=\"bordered\"\n onSelectionChange={(keys) => {\n const selectedKey = Array.from(keys)[0] as string;\n\n setForm((prev) => ({\n ...prev,\n inIp: selectedKey === \"__default__\" ? \"\" : selectedKey,\n }));\n }}\n >\n 默认入口IP\n {currentTunnelIpOptions.map((ip) => (\n {ip}\n ))}\n \n\n \n setForm((prev) => ({\n ...prev,\n remoteAddr: e.target.value,\n }))\n }\n />\n\n {getAddressCount(form.remoteAddr) > 1 && (\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n setForm((prev) => ({ ...prev, strategy: selectedKey }));\n }}\n >\n 主备模式 - 自上而下\n 轮询模式 - 依次轮换\n 随机模式 - 随机选择\n 哈希模式 - IP哈希\n \n )}\n
\n
\n \n \n \n {isEdit ? \"保存修改\" : \"创建转发\"}\n \n \n \n )}\n
\n \n\n {/* 删除确认模态框 */}\n \n \n {(onClose) => (\n <>\n \n

确认删除

\n
\n \n

\n 确定要删除转发{\" \"}\n \n "{forwardToDelete?.name}"\n {\" \"}\n 吗?\n

\n

\n 此操作无法撤销,删除后该转发将永久消失。\n

\n
\n \n \n \n 确认删除\n \n \n \n )}\n
\n \n\n {/* 地址列表弹窗 */}\n setAddressModalOpen(false)}\n >\n \n {addressModalTitle}\n \n
\n \n
\n\n
\n {addressList.map((item) => (\n \n \n {item.address}\n \n copyAddress(item)}\n >\n 复制\n \n
\n ))}\n \n
\n
\n \n\n {/* 导出数据模态框 */}\n {\n setExportModalOpen(false);\n setSelectedTunnelForExport(null);\n setExportData(\"\");\n }}\n >\n \n \n

导出转发数据

\n

\n 格式:目标地址|转发名称|入口端口\n

\n
\n \n
\n {/* 隧道选择 */}\n
\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n setSelectedTunnelForExport(\n selectedKey ? parseInt(selectedKey) : null,\n );\n }}\n >\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n
\n\n {/* 导出按钮和数据 */}\n {exportData && (\n
\n \n \n \n }\n onPress={executeExport}\n >\n 重新生成\n \n \n \n \n \n }\n onPress={copyExportData}\n >\n 复制\n \n
\n )}\n\n {/* 初始导出按钮 */}\n {!exportData && (\n
\n \n \n \n }\n onPress={executeExport}\n >\n 生成导出数据\n \n
\n )}\n\n {/* 导出数据显示 */}\n {exportData && (\n
\n \n
\n )}\n
\n
\n \n \n \n
\n \n\n {/* 导入数据模态框 */}\n setImportModalOpen(false)}\n >\n \n \n

导入转发数据

\n {importFormat === \"flvx\" ? (\n <>\n

\n 格式:目标地址|转发名称|入口端口,每行一个,入口端口留空将自动分配可用端口\n

\n

\n 目标地址支持单个地址(如:example.com:8080)或多个地址用逗号分隔(如:3.3.3.3:3,4.4.4.4:4)\n

\n \n ) : (\n <>\n

\n ny格式:JSON对象,支持多个目标地址(负载均衡)\n

\n

\n 格式:{"dest":["地址:端口"],"listen_port":端口,"name":"名称"}\n

\n \n )}\n
\n \n
\n {/* 格式选择 */}\n {\n const selectedKey = Array.from(keys)[0] as ImportFormat;\n\n if (selectedKey) {\n setImportFormat(selectedKey);\n setSelectedTunnelForImport(null);\n setImportData(\"\");\n setImportResults([]);\n }\n }}\n >\n \n flvx格式(管道分隔)\n \n \n ny格式(JSON)\n \n \n\n {/* 隧道选择 - 两种格式都需要 */}\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n setSelectedTunnelForImport(\n selectedKey ? parseInt(selectedKey) : null,\n );\n }}\n >\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n\n {/* 输入区域 */}\n setImportData(e.target.value)}\n />\n\n {/* 导入结果 */}\n {importResults.length > 0 && (\n
\n
\n

导入结果

\n \n 成功:{importResults.filter((r) => r.success).length} /\n 总计:{importResults.length}\n \n
\n\n \n {importResults.map((result, index) => (\n \n
\n {result.success ? (\n \n \n \n ) : (\n \n \n \n )}\n
\n
\n \n {result.success ? \"成功\" : \"失败\"}\n \n \n |\n \n \n {result.line}\n \n
\n \n {result.message}\n
\n
\n
\n
\n ))}\n \n \n )}\n \n
\n \n \n \n 开始导入\n \n \n
\n \n\n {/* 诊断结果模态框 */}\n {\n setDiagnosisModalOpen(open);\n if (!open) {\n diagnosisAbortRef.current?.abort();\n diagnosisAbortRef.current = null;\n setDiagnosisLoading(false);\n }\n }}\n >\n \n {(onClose) => (\n <>\n \n

转发诊断结果

\n {currentDiagnosisForward && (\n
\n \n {currentDiagnosisForward.name}\n \n \n 转发服务\n \n
\n )}\n
\n \n {diagnosisResult ? (\n
\n {diagnosisLoading && (\n
\n
\n \n \n 正在诊断 {diagnosisProgress.completed}/\n {diagnosisProgress.total > 0\n ? diagnosisProgress.total\n : \"?\"}\n \n
\n \n 流式更新中\n \n
\n )}\n\n {diagnosisProgress.timedOut && (\n \n )}\n\n {/* 统计摘要 */}\n
\n
\n
\n {diagnosisProgress.total > 0\n ? diagnosisProgress.total\n : diagnosisResult.results.length}\n
\n
\n 总测试数\n
\n
\n
\n
\n {diagnosisProgress.completed > 0 ||\n diagnosisProgress.total > 0\n ? diagnosisProgress.success\n : diagnosisResult.results.filter((r) => r.success)\n .length}\n
\n
\n 成功\n
\n
\n
\n
\n {diagnosisProgress.completed > 0 ||\n diagnosisProgress.total > 0\n ? diagnosisProgress.failed\n : diagnosisResult.results.filter((r) => !r.success)\n .length}\n
\n
\n 失败\n
\n
\n
\n\n {/* 桌面端表格展示 */}\n
\n {(() => {\n // 使用后端返回的 chainType 和 inx 字段进行分组\n const groupedResults = {\n entry: diagnosisResult.results.filter(\n (r) => r.fromChainType === 1,\n ),\n chains: {} as Record<\n number,\n typeof diagnosisResult.results\n >,\n exit: diagnosisResult.results.filter(\n (r) => r.fromChainType === 3,\n ),\n };\n\n // 按 inx 分组链路测试\n diagnosisResult.results.forEach((r) => {\n if (r.fromChainType === 2 && r.fromInx != null) {\n if (!groupedResults.chains[r.fromInx]) {\n groupedResults.chains[r.fromInx] = [];\n }\n groupedResults.chains[r.fromInx].push(r);\n }\n });\n\n const renderTableSection = (\n title: string,\n results: typeof diagnosisResult.results,\n ) => {\n if (results.length === 0) return null;\n\n return (\n \n
\n

\n {title}\n

\n
\n \n \n \n \n \n \n \n \n \n \n \n {results.map((result, index) => {\n const isDiagnosing = Boolean(\n result.diagnosing,\n );\n const isSuccess = result.success === true;\n const quality =\n getForwardDiagnosisQualityDisplay(\n result.averageTime,\n result.packetLoss,\n );\n\n return (\n \n \n \n \n \n \n \n );\n })}\n \n
\n 路径\n \n 状态\n \n 延迟(ms)\n \n 丢包率\n \n 质量\n
\n
\n {isDiagnosing ? (\n \n ) : (\n \n {isSuccess ? \"✓\" : \"✗\"}\n \n )}\n
\n
\n {result.description}\n
\n
\n {result.targetIp}:\n {result.targetPort}\n
\n
\n
\n
\n \n {isDiagnosing\n ? \"诊断中\"\n : isSuccess\n ? \"成功\"\n : \"失败\"}\n \n \n {isSuccess ? (\n \n {result.averageTime?.toFixed(0)}\n \n ) : (\n \n -\n \n )}\n \n {isSuccess ? (\n 0\n ? \"text-warning\"\n : \"text-success\"\n }`}\n >\n {result.packetLoss?.toFixed(1)}%\n \n ) : (\n \n -\n \n )}\n \n {isSuccess && quality ? (\n \n {quality.text}\n \n ) : (\n \n -\n \n )}\n
\n
\n );\n };\n\n return (\n <>\n {/* 入口测试 */}\n {renderTableSection(\n \"🚪 入口测试\",\n groupedResults.entry,\n )}\n\n {/* 链路测试(按跳数排序) */}\n {Object.keys(groupedResults.chains)\n .map(Number)\n .sort((a, b) => a - b)\n .map((hop) =>\n renderTableSection(\n `🔗 转发链 - 第${hop}跳`,\n groupedResults.chains[hop],\n ),\n )}\n\n {/* 出口测试 */}\n {renderTableSection(\n \"🚀 出口测试\",\n groupedResults.exit,\n )}\n \n );\n })()}\n
\n\n {/* 移动端卡片展示 */}\n
\n {(() => {\n // 使用后端返回的 chainType 和 inx 字段进行分组\n const groupedResults = {\n entry: diagnosisResult.results.filter(\n (r) => r.fromChainType === 1,\n ),\n chains: {} as Record<\n number,\n typeof diagnosisResult.results\n >,\n exit: diagnosisResult.results.filter(\n (r) => r.fromChainType === 3,\n ),\n };\n\n // 按 inx 分组链路测试\n diagnosisResult.results.forEach((r) => {\n if (r.fromChainType === 2 && r.fromInx != null) {\n if (!groupedResults.chains[r.fromInx]) {\n groupedResults.chains[r.fromInx] = [];\n }\n groupedResults.chains[r.fromInx].push(r);\n }\n });\n\n const renderCardSection = (\n title: string,\n results: typeof diagnosisResult.results,\n ) => {\n if (results.length === 0) return null;\n\n return (\n
\n
\n

\n {title}\n

\n
\n {results.map((result, index) => {\n const isDiagnosing = Boolean(result.diagnosing);\n const isSuccess = result.success === true;\n const quality =\n getForwardDiagnosisQualityDisplay(\n result.averageTime,\n result.packetLoss,\n );\n\n return (\n \n
\n {isDiagnosing ? (\n \n ) : (\n \n {isSuccess ? \"✓\" : \"✗\"}\n \n )}\n
\n
\n {result.description}\n
\n
\n {result.targetIp}:{result.targetPort}\n
\n
\n \n {isDiagnosing\n ? \"诊断中\"\n : isSuccess\n ? \"成功\"\n : \"失败\"}\n \n
\n\n {isSuccess ? (\n
\n
\n
\n {result.averageTime?.toFixed(0)}\n
\n
\n 延迟(ms)\n
\n
\n
\n 0\n ? \"text-warning\"\n : \"text-success\"\n }`}\n >\n {result.packetLoss?.toFixed(1)}%\n
\n
\n 丢包率\n
\n
\n
\n {quality && (\n <>\n \n {quality.text}\n \n
\n 质量\n
\n \n )}\n
\n
\n ) : (\n
\n \n {isDiagnosing\n ? result.message || \"诊断中...\"\n : result.message || \"连接失败\"}\n
\n
\n )}\n \n );\n })}\n \n );\n };\n\n return (\n <>\n {/* 入口测试 */}\n {renderCardSection(\n \"🚪 入口测试\",\n groupedResults.entry,\n )}\n\n {/* 链路测试(按跳数排序) */}\n {Object.keys(groupedResults.chains)\n .map(Number)\n .sort((a, b) => a - b)\n .map((hop) =>\n renderCardSection(\n `🔗 转发链 - 第${hop}跳`,\n groupedResults.chains[hop],\n ),\n )}\n\n {/* 出口测试 */}\n {renderCardSection(\n \"🚀 出口测试\",\n groupedResults.exit,\n )}\n \n );\n })()}\n \n\n {/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */}\n {diagnosisResult.results.some(\n (r) => r.success === false && !r.diagnosing,\n ) && (\n \n )}\n \n ) : (\n
\n
\n \n \n \n
\n

\n 暂无诊断数据\n

\n
\n )}\n
\n \n \n {currentDiagnosisForward && (\n handleDiagnose(currentDiagnosisForward)}\n >\n 重新诊断\n \n )}\n \n \n )}\n
\n \n\n {/* 批量删除确认模态框 */}\n \n \n {(onClose) => (\n <>\n 确认删除\n \n

\n 确定要删除选中的 {selectedIds.size} 项转发吗?此操作不可撤销。\n

\n
\n \n \n \n 确认删除\n \n \n \n )}\n
\n \n\n {/* 批量换隧道模态框 */}\n \n \n {(onClose) => (\n <>\n 隧道\n \n

\n 将选中的 {selectedIds.size} 项转发迁移到新隧道:\n

\n {\n const selected = Array.from(keys)[0];\n\n setBatchTargetTunnelId(selected ? Number(selected) : null);\n }}\n >\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n
\n \n \n \n 确认换隧道\n \n \n \n )}\n
\n \n\n {/* 筛选模态框 */}\n \n \n {(onClose) => (\n <>\n \n 筛选条件\n \n \n
\n
\n

按用户筛选

\n {\n const key = Array.from(keys)[0] as string;\n\n setFilterUserId(key || \"all\");\n }}\n >\n 全部用户\n {uniqueUsers.map((user) => (\n \n {user.name}\n \n ))}\n \n
\n
\n

按隧道筛选

\n {\n const key = Array.from(keys)[0] as string;\n\n setFilterTunnelId(key || \"all\");\n }}\n >\n 全部隧道\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n
\n
\n
\n \n {\n resetFilterUserId();\n resetFilterTunnelId();\n }}\n >\n 重置\n \n \n \n \n )}\n
\n \n
\n );\n}\n", "after": "import type { SpeedLimitApiItem } from \"@/api/types\";\n\nimport { useState, useEffect, useMemo, useRef } from \"react\";\nimport toast from \"react-hot-toast\";\nimport {\n DndContext,\n closestCenter,\n KeyboardSensor,\n MouseSensor,\n TouchSensor,\n useSensor,\n useSensors,\n DragEndEvent,\n} from \"@dnd-kit/core\";\nimport {\n arrayMove,\n SortableContext,\n sortableKeyboardCoordinates,\n rectSortingStrategy,\n verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { useSortable } from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\n\nimport { SearchBar } from \"@/components/search-bar\";\nimport { AnimatedPage } from \"@/components/animated-page\";\nimport { Card, CardBody, CardHeader } from \"@/shadcn-bridge/heroui/card\";\nimport { Button } from \"@/shadcn-bridge/heroui/button\";\nimport { Input } from \"@/shadcn-bridge/heroui/input\";\nimport { Textarea } from \"@/shadcn-bridge/heroui/input\";\nimport { Select, SelectItem } from \"@/shadcn-bridge/heroui/select\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableColumn,\n TableHeader,\n TableRow,\n} from \"@/shadcn-bridge/heroui/table\";\nimport {\n Modal,\n ModalContent,\n ModalHeader,\n ModalBody,\n ModalFooter,\n} from \"@/shadcn-bridge/heroui/modal\";\nimport { Chip } from \"@/shadcn-bridge/heroui/chip\";\nimport { Spinner } from \"@/shadcn-bridge/heroui/spinner\";\nimport { Switch } from \"@/shadcn-bridge/heroui/switch\";\nimport { Alert } from \"@/shadcn-bridge/heroui/alert\";\nimport { Checkbox } from \"@/shadcn-bridge/heroui/checkbox\";\nimport {\n createForward,\n getForwardList,\n getSpeedLimitList,\n getPeerShareList,\n getPeerRemoteUsageList,\n updateForward,\n deleteForward,\n forceDeleteForward,\n userTunnel,\n getTunnelList,\n getNodeList,\n pauseForwardService,\n resumeForwardService,\n diagnoseForward,\n updateForwardOrder,\n getConfigByName,\n updateConfig,\n} from \"@/api\";\nimport {\n type ForwardAddressItem,\n formatInAddress,\n formatRemoteAddress,\n hasMultipleAddresses,\n resolveForwardAddressAction,\n} from \"@/pages/forward/address\";\nimport {\n buildForwardDiagnosisFallbackResult,\n getForwardDiagnosisQualityDisplay,\n type ForwardDiagnosisResult,\n} from \"@/pages/forward/diagnosis\";\nimport { diagnoseForwardStream } from \"@/api/diagnosis-stream\";\nimport {\n executeForwardBatchChangeTunnel,\n executeForwardBatchDelete,\n executeForwardBatchRedeploy,\n executeForwardBatchToggleService,\n} from \"@/pages/forward/batch-actions\";\nimport {\n convertNyItemToForwardInput,\n parseNyFormatData,\n} from \"@/pages/forward/import-format\";\nimport { buildForwardOrder, FORWARD_ORDER_KEY } from \"@/pages/forward/order\";\nimport { PageLoadingState } from \"@/components/page-state\";\nimport { useMobileBreakpoint } from \"@/hooks/useMobileBreakpoint\";\nimport { useLocalStorageState } from \"@/hooks/use-local-storage-state\";\nimport { saveOrder } from \"@/utils/order-storage\";\nimport { JwtUtil } from \"@/utils/jwt\";\n\ninterface Forward {\n id: number;\n name: string;\n tunnelId: number;\n tunnelName: string;\n inIp: string;\n inPort: number;\n remoteAddr: string;\n interfaceName?: string;\n strategy: string;\n status: number;\n inFlow: number;\n outFlow: number;\n serviceRunning: boolean;\n federationShareFlow?: number;\n createdTime: string;\n userName?: string;\n userId?: number;\n inx?: number;\n speedId?: number | null;\n}\n\ninterface Tunnel {\n id: number;\n name: string;\n type?: number;\n inIp?: string;\n inNodeId?: Array<{ nodeId: number }>;\n inNodePortSta?: number;\n inNodePortEnd?: number;\n}\n\ninterface Node {\n id: number;\n name?: string;\n serverIp?: string;\n serverIpV4?: string;\n serverIpV6?: string;\n extraIPs?: string;\n}\n\ninterface ForwardForm {\n id?: number;\n userId?: number;\n name: string;\n tunnelId: number | null;\n inPort: number | null;\n inIp: string;\n remoteAddr: string;\n interfaceName?: string;\n strategy: string;\n speedId: number | null;\n}\n\ninterface ForwardUserGroup {\n userId: number;\n userName: string;\n tunnels: ForwardTunnelGroup[];\n}\n\ninterface ForwardTunnelGroup {\n tunnelKey: string;\n tunnelName: string;\n items: Forward[];\n}\n\ntype ForwardGroupOrderMap = Record;\ntype ForwardGroupCollapsedMap = Record;\n\nconst UNKNOWN_FORWARD_USER_NAME = \"未知用户\";\nconst UNCATEGORIZED_FORWARD_TUNNEL_NAME = \"未分类\";\nconst FORWARD_COMPACT_MODE_CONFIG_KEY = \"forward_compact_mode\";\nconst FORWARD_COMPACT_MODE_EVENT = \"forwardCompactModeChanged\";\nconst FORWARD_GROUP_ORDER_CONFIG_KEY = \"forward_group_order_map\";\nconst FORWARD_GROUP_COLLAPSED_CONFIG_KEY = \"forward_group_collapsed_map\";\nconst FORWARD_GROUP_ORDER_LOCAL_STORAGE_PREFIX = \"forward-group-order\";\nconst FORWARD_GROUP_COLLAPSED_LOCAL_STORAGE_PREFIX = \"forward-group-collapsed\";\nconst FORWARD_TUNNEL_GROUP_SORTABLE_PREFIX = \"forward-tunnel-group\";\nconst FORWARD_GROUPED_TABLE_MIN_WIDTH_CLASS = \"min-w-[1320px]\";\nconst FORWARD_GROUPED_TABLE_COLUMN_CLASS = {\n select: \"w-14\",\n drag: \"w-10 pl-4\",\n name: \"w-[200px]\",\n inbound: \"w-[280px]\",\n target: \"w-[280px]\",\n strategy: \"w-[100px]\",\n totalFlow: \"w-[120px]\",\n status: \"w-[100px]\",\n actions: \"w-[144px] text-right\",\n} as const;\n\nconst normalizeForwardUserName = (userName?: string): string => {\n const normalized = (userName || UNKNOWN_FORWARD_USER_NAME).trim();\n\n return normalized || UNKNOWN_FORWARD_USER_NAME;\n};\n\nconst compareForwardUserNameAsc = (a: string, b: string): number => {\n return a.localeCompare(b, \"en\", {\n sensitivity: \"base\",\n numeric: true,\n });\n};\n\nconst normalizeForwardTunnelName = (tunnelName?: string): string => {\n const normalized = (tunnelName || \"\").trim();\n\n return normalized || UNCATEGORIZED_FORWARD_TUNNEL_NAME;\n};\n\nconst buildForwardTunnelGroupKey = (tunnelName?: string): string => {\n const normalized = normalizeForwardTunnelName(tunnelName);\n\n if (normalized === UNCATEGORIZED_FORWARD_TUNNEL_NAME) {\n return \"__uncategorized__\";\n }\n\n return normalized.toLocaleLowerCase();\n};\n\nconst compareForwardTunnelNameAsc = (a: string, b: string): number => {\n return a.localeCompare(b, \"en\", {\n sensitivity: \"base\",\n numeric: true,\n });\n};\n\nconst compareForwardTunnelGroupKeyAsc = (a: string, b: string): number => {\n const aIsUncategorized = a === \"__uncategorized__\";\n const bIsUncategorized = b === \"__uncategorized__\";\n\n if (aIsUncategorized !== bIsUncategorized) {\n return aIsUncategorized ? 1 : -1;\n }\n\n return compareForwardTunnelNameAsc(a, b);\n};\n\nconst buildForwardGroupOrderLocalKey = (tokenUserId: number): string => {\n return `${FORWARD_GROUP_ORDER_LOCAL_STORAGE_PREFIX}:u:${tokenUserId}`;\n};\n\nconst buildForwardGroupCollapsedLocalKey = (tokenUserId: number): string => {\n return `${FORWARD_GROUP_COLLAPSED_LOCAL_STORAGE_PREFIX}:u:${tokenUserId}`;\n};\n\nconst parsePreferenceMap = (\n raw: string | null,\n): Record | null => {\n if (!raw) {\n return null;\n }\n\n try {\n const parsed = JSON.parse(raw);\n\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return null;\n }\n\n return parsed as Record;\n } catch {\n return null;\n }\n};\n\nconst parseGroupOrderMap = (raw: string | null): ForwardGroupOrderMap => {\n const parsed = parsePreferenceMap(raw);\n\n if (!parsed) {\n return {};\n }\n\n const result: ForwardGroupOrderMap = {};\n\n Object.entries(parsed).forEach(([userId, value]) => {\n if (!Array.isArray(value)) {\n return;\n }\n\n const keys = value\n .map((item) => (typeof item === \"string\" ? item.trim() : \"\"))\n .filter((item) => item !== \"\");\n\n if (keys.length > 0) {\n result[userId] = Array.from(new Set(keys));\n }\n });\n\n return result;\n};\n\nconst parseGroupCollapsedMap = (\n raw: string | null,\n): ForwardGroupCollapsedMap => {\n const parsed = parsePreferenceMap(raw);\n\n if (!parsed) {\n return {};\n }\n\n const result: ForwardGroupCollapsedMap = {};\n\n Object.entries(parsed).forEach(([key, value]) => {\n if (typeof value === \"boolean\") {\n result[key] = value;\n }\n });\n\n return result;\n};\n\nconst sanitizeGroupOrderMap = (\n source: ForwardGroupOrderMap,\n availableTunnelKeysByUser: Map>,\n): ForwardGroupOrderMap => {\n const sanitized: ForwardGroupOrderMap = {};\n\n availableTunnelKeysByUser.forEach((availableKeys, userId) => {\n if (availableKeys.size === 0) {\n return;\n }\n\n const orderFromSource = source[userId.toString()] || [];\n const used = new Set();\n const merged: string[] = [];\n\n orderFromSource.forEach((key) => {\n if (!availableKeys.has(key) || used.has(key)) {\n return;\n }\n\n used.add(key);\n merged.push(key);\n });\n\n Array.from(availableKeys)\n .sort(compareForwardTunnelGroupKeyAsc)\n .forEach((key) => {\n if (!used.has(key)) {\n used.add(key);\n merged.push(key);\n }\n });\n\n if (merged.length > 0) {\n sanitized[userId.toString()] = merged;\n }\n });\n\n return sanitized;\n};\n\nconst sanitizeGroupCollapsedMap = (\n source: ForwardGroupCollapsedMap,\n availableCollapseKeys: Set,\n): ForwardGroupCollapsedMap => {\n const sanitized: ForwardGroupCollapsedMap = {};\n\n availableCollapseKeys.forEach((key) => {\n if (source[key] === true) {\n sanitized[key] = true;\n }\n });\n\n return sanitized;\n};\n\nconst buildTunnelGroupCollapseKey = (\n userId: number,\n tunnelKey: string,\n): string => {\n return `${userId}:${tunnelKey}`;\n};\n\nconst buildTunnelGroupSortableId = (\n userId: number,\n tunnelKey: string,\n): string => {\n return `${FORWARD_TUNNEL_GROUP_SORTABLE_PREFIX}:${userId}:${tunnelKey}`;\n};\n\nconst parseTunnelGroupSortableId = (\n value: unknown,\n): { userId: number; tunnelKey: string } | null => {\n if (typeof value !== \"string\") {\n return null;\n }\n\n if (!value.startsWith(`${FORWARD_TUNNEL_GROUP_SORTABLE_PREFIX}:`)) {\n return null;\n }\n\n const parts = value.split(\":\");\n\n if (parts.length < 3) {\n return null;\n }\n\n const userId = Number(parts[1]);\n const tunnelKey = parts.slice(2).join(\":\").trim();\n\n if (!Number.isFinite(userId) || tunnelKey === \"\") {\n return null;\n }\n\n return { userId, tunnelKey };\n};\n\nconst buildAvailableGroupData = (\n forwards: Forward[],\n): {\n availableTunnelKeysByUser: Map>;\n availableCollapseKeys: Set;\n} => {\n const availableTunnelKeysByUser = new Map>();\n const availableCollapseKeys = new Set();\n\n forwards.forEach((forward) => {\n const userId = forward.userId ?? 0;\n const tunnelKey = buildForwardTunnelGroupKey(forward.tunnelName);\n\n let set = availableTunnelKeysByUser.get(userId);\n\n if (!set) {\n set = new Set();\n availableTunnelKeysByUser.set(userId, set);\n }\n\n set.add(tunnelKey);\n availableCollapseKeys.add(buildTunnelGroupCollapseKey(userId, tunnelKey));\n });\n\n return { availableTunnelKeysByUser, availableCollapseKeys };\n};\n\nconst isSameStringArray = (a: string[], b: string[]): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n\n for (let i = 0; i < a.length; i += 1) {\n if (a[i] !== b[i]) {\n return false;\n }\n }\n\n return true;\n};\n\nconst isSameGroupOrderMap = (\n a: ForwardGroupOrderMap,\n b: ForwardGroupOrderMap,\n): boolean => {\n const aKeys = Object.keys(a).sort(compareForwardTunnelNameAsc);\n const bKeys = Object.keys(b).sort(compareForwardTunnelNameAsc);\n\n if (!isSameStringArray(aKeys, bKeys)) {\n return false;\n }\n\n for (const key of aKeys) {\n if (!isSameStringArray(a[key] || [], b[key] || [])) {\n return false;\n }\n }\n\n return true;\n};\n\nconst isSameGroupCollapsedMap = (\n a: ForwardGroupCollapsedMap,\n b: ForwardGroupCollapsedMap,\n): boolean => {\n const aKeys = Object.keys(a).sort(compareForwardTunnelNameAsc);\n const bKeys = Object.keys(b).sort(compareForwardTunnelNameAsc);\n\n if (!isSameStringArray(aKeys, bKeys)) {\n return false;\n }\n\n for (const key of aKeys) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n};\n\nexport default function ForwardPage() {\n const [loading, setLoading] = useState(true);\n const [forwards, setForwards] = useState([]);\n const [tunnels, setTunnels] = useState([]);\n const [allTunnels, setAllTunnels] = useState([]);\n const [nodes, setNodes] = useState([]);\n const [speedLimits, setSpeedLimits] = useState([]);\n const isMobile = useMobileBreakpoint();\n const [searchKeyword, setSearchKeyword] = useLocalStorageState(\n \"forward-search-keyword\",\n \"\",\n );\n const [isSearchVisible, setIsSearchVisible] = useState(false);\n const [compactMode, setCompactMode] = useState(false);\n\n // 显示模式状态 - 从localStorage读取,默认为平铺显示\n const [viewMode, setViewMode] = useState<\"grouped\" | \"direct\">(() => {\n try {\n const savedMode = localStorage.getItem(\"forward-view-mode\");\n\n return (savedMode as \"grouped\" | \"direct\") || \"direct\";\n } catch {\n return \"direct\";\n }\n });\n\n // 筛选状态\n const [filterUserId, setFilterUserId, resetFilterUserId] =\n useLocalStorageState(\"forward-filter-user-id\", \"all\");\n const [filterTunnelId, setFilterTunnelId, resetFilterTunnelId] =\n useLocalStorageState(\"forward-filter-tunnel-id\", \"all\");\n\n // 拖拽排序相关状态\n const [forwardOrder, setForwardOrder] = useState([]);\n\n // 模态框状态\n const [modalOpen, setModalOpen] = useState(false);\n const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);\n const [deleteModalOpen, setDeleteModalOpen] = useState(false);\n const [addressModalOpen, setAddressModalOpen] = useState(false);\n const [diagnosisModalOpen, setDiagnosisModalOpen] = useState(false);\n const [isEdit, setIsEdit] = useState(false);\n const [submitLoading, setSubmitLoading] = useState(false);\n const [deleteLoading, setDeleteLoading] = useState(false);\n const [diagnosisLoading, setDiagnosisLoading] = useState(false);\n const [forwardToDelete, setForwardToDelete] = useState(null);\n const [currentDiagnosisForward, setCurrentDiagnosisForward] =\n useState(null);\n const [diagnosisResult, setDiagnosisResult] =\n useState(null);\n const [diagnosisProgress, setDiagnosisProgress] = useState({\n total: 0,\n completed: 0,\n success: 0,\n failed: 0,\n timedOut: false,\n });\n const diagnosisAbortRef = useRef(null);\n const [addressModalTitle, setAddressModalTitle] = useState(\"\");\n const [addressList, setAddressList] = useState([]);\n\n // 导出相关状态\n const [exportModalOpen, setExportModalOpen] = useState(false);\n const [exportData, setExportData] = useState(\"\");\n const [exportLoading, setExportLoading] = useState(false);\n const [selectedTunnelForExport, setSelectedTunnelForExport] = useState<\n number | null\n >(null);\n\n // 导入相关状态\n type ImportFormat = \"flvx\" | \"ny\";\n const [importModalOpen, setImportModalOpen] = useState(false);\n const [importData, setImportData] = useState(\"\");\n const [importLoading, setImportLoading] = useState(false);\n const [importFormat, setImportFormat] = useState(\"flvx\");\n const [selectedTunnelForImport, setSelectedTunnelForImport] = useState<\n number | null\n >(null);\n const [importResults, setImportResults] = useState<\n Array<{\n line: string;\n success: boolean;\n message: string;\n forwardName?: string;\n }>\n >([]);\n\n // 表单状态\n const [form, setForm] = useState({\n name: \"\",\n tunnelId: null,\n inPort: null,\n inIp: \"\",\n remoteAddr: \"\",\n interfaceName: \"\",\n strategy: \"fifo\",\n speedId: null,\n });\n\n // 表单验证错误\n const [errors, setErrors] = useState<{ [key: string]: string }>({});\n\n // 批量操作相关状态\n const [selectMode, setSelectMode] = useState(false);\n const [selectedIds, setSelectedIds] = useState>(new Set());\n const [batchDeleteModalOpen, setBatchDeleteModalOpen] = useState(false);\n const [batchChangeTunnelModalOpen, setBatchChangeTunnelModalOpen] =\n useState(false);\n const [batchTargetTunnelId, setBatchTargetTunnelId] = useState(\n null,\n );\n const [batchLoading, setBatchLoading] = useState(false);\n const [groupOrderMap, setGroupOrderMap] = useState({});\n const [collapsedTunnelGroups, setCollapsedTunnelGroups] =\n useState({});\n const [groupPreferenceHydrated, setGroupPreferenceHydrated] = useState(false);\n const tokenUserId = JwtUtil.getUserIdFromToken();\n const tokenRoleId = JwtUtil.getRoleIdFromToken();\n const isAdmin = tokenRoleId === 0;\n\n const parseNodeIPs = (node?: Node): string[] => {\n if (!node) {\n return [];\n }\n\n const ips: string[] = [];\n const add = (value?: string) => {\n const trimmed = (value || \"\").trim();\n\n if (trimmed) {\n ips.push(trimmed);\n }\n };\n\n add(node.serverIpV4);\n add(node.serverIpV6);\n add(node.serverIp);\n\n (node.extraIPs || \"\")\n .split(\",\")\n .map((v) => v.trim())\n .filter((v) => v)\n .forEach((v) => ips.push(v));\n\n return Array.from(new Set(ips));\n };\n\n const tunnelInIpOptionMap = useMemo(() => {\n const map = new Map();\n const nodeMap = new Map(nodes.map((n) => [n.id, n]));\n\n for (const tunnel of allTunnels) {\n const collected: string[] = [];\n const entryNodes = tunnel.inNodeId || [];\n\n for (const entry of entryNodes) {\n collected.push(...parseNodeIPs(nodeMap.get(entry.nodeId)));\n }\n\n if (collected.length === 0) {\n (tunnel.inIp || \"\")\n .split(\",\")\n .map((v) => v.trim())\n .filter((v) => v)\n .forEach((v) => collected.push(v));\n }\n\n map.set(tunnel.id, Array.from(new Set(collected)));\n }\n\n return map;\n }, [allTunnels, nodes]);\n\n const currentTunnelIpOptions = useMemo(() => {\n if (!form.tunnelId) {\n return [];\n }\n\n return tunnelInIpOptionMap.get(form.tunnelId) || [];\n }, [form.tunnelId, tunnelInIpOptionMap]);\n\n useEffect(() => {\n return () => {\n diagnosisAbortRef.current?.abort();\n diagnosisAbortRef.current = null;\n };\n }, []);\n\n const persistGroupOrderToLocal = (nextOrderMap: ForwardGroupOrderMap) => {\n if (tokenUserId === null) {\n return;\n }\n\n try {\n localStorage.setItem(\n buildForwardGroupOrderLocalKey(tokenUserId),\n JSON.stringify(nextOrderMap),\n );\n } catch {}\n };\n\n const persistGroupCollapsedToLocal = (\n nextCollapsedMap: ForwardGroupCollapsedMap,\n ) => {\n if (tokenUserId === null) {\n return;\n }\n\n try {\n localStorage.setItem(\n buildForwardGroupCollapsedLocalKey(tokenUserId),\n JSON.stringify(nextCollapsedMap),\n );\n } catch {}\n };\n\n const persistGroupOrderToGlobal = async (\n nextOrderMap: ForwardGroupOrderMap,\n ): Promise => {\n if (!isAdmin || tokenUserId === null) {\n return;\n }\n\n try {\n const currentRes = await getConfigByName(FORWARD_GROUP_ORDER_CONFIG_KEY);\n const globalMap =\n parsePreferenceMap(\n currentRes.code === 0 && typeof currentRes.data?.value === \"string\"\n ? currentRes.data.value\n : null,\n ) || {};\n\n globalMap[tokenUserId.toString()] = nextOrderMap;\n\n const saveRes = await updateConfig(\n FORWARD_GROUP_ORDER_CONFIG_KEY,\n JSON.stringify(globalMap),\n );\n\n if (saveRes.code !== 0) {\n toast.error(saveRes.msg || \"保存分组排序失败\");\n }\n } catch {\n toast.error(\"保存分组排序失败\");\n }\n };\n\n const persistGroupCollapsedToGlobal = async (\n nextCollapsedMap: ForwardGroupCollapsedMap,\n ): Promise => {\n if (!isAdmin || tokenUserId === null) {\n return;\n }\n\n try {\n const currentRes = await getConfigByName(\n FORWARD_GROUP_COLLAPSED_CONFIG_KEY,\n );\n const globalMap =\n parsePreferenceMap(\n currentRes.code === 0 && typeof currentRes.data?.value === \"string\"\n ? currentRes.data.value\n : null,\n ) || {};\n\n globalMap[tokenUserId.toString()] = nextCollapsedMap;\n\n const saveRes = await updateConfig(\n FORWARD_GROUP_COLLAPSED_CONFIG_KEY,\n JSON.stringify(globalMap),\n );\n\n if (saveRes.code !== 0) {\n toast.error(saveRes.msg || \"保存分组折叠状态失败\");\n }\n } catch {\n toast.error(\"保存分组折叠状态失败\");\n }\n };\n\n useEffect(() => {\n let cancelled = false;\n\n const loadGroupPreferences = async () => {\n if (tokenUserId === null) {\n if (!cancelled) {\n setGroupOrderMap({});\n setCollapsedTunnelGroups({});\n setGroupPreferenceHydrated(true);\n }\n\n return;\n }\n\n let localOrderMap: ForwardGroupOrderMap = {};\n let localCollapsedMap: ForwardGroupCollapsedMap = {};\n\n try {\n localOrderMap = parseGroupOrderMap(\n localStorage.getItem(buildForwardGroupOrderLocalKey(tokenUserId)),\n );\n } catch {\n localOrderMap = {};\n }\n\n try {\n localCollapsedMap = parseGroupCollapsedMap(\n localStorage.getItem(buildForwardGroupCollapsedLocalKey(tokenUserId)),\n );\n } catch {\n localCollapsedMap = {};\n }\n\n if (isAdmin) {\n try {\n const [globalOrderRes, globalCollapsedRes] = await Promise.all([\n getConfigByName(FORWARD_GROUP_ORDER_CONFIG_KEY),\n getConfigByName(FORWARD_GROUP_COLLAPSED_CONFIG_KEY),\n ]);\n\n const globalOrderMap = parsePreferenceMap(\n globalOrderRes.code === 0 &&\n typeof globalOrderRes.data?.value === \"string\"\n ? globalOrderRes.data.value\n : null,\n );\n const globalCollapsedMap =\n parsePreferenceMap(\n globalCollapsedRes.code === 0 &&\n typeof globalCollapsedRes.data?.value === \"string\"\n ? globalCollapsedRes.data.value\n : null,\n );\n\n const globalOrderBucket = globalOrderMap?.[tokenUserId.toString()];\n const globalCollapsedBucket =\n globalCollapsedMap?.[tokenUserId.toString()];\n\n if (\n globalOrderBucket &&\n typeof globalOrderBucket === \"object\" &&\n !Array.isArray(globalOrderBucket)\n ) {\n localOrderMap = parseGroupOrderMap(\n JSON.stringify(globalOrderBucket),\n );\n }\n\n if (\n globalCollapsedBucket &&\n typeof globalCollapsedBucket === \"object\" &&\n !Array.isArray(globalCollapsedBucket)\n ) {\n localCollapsedMap = parseGroupCollapsedMap(\n JSON.stringify(globalCollapsedBucket),\n );\n }\n } catch {}\n }\n\n if (cancelled) {\n return;\n }\n\n setGroupOrderMap(localOrderMap);\n setCollapsedTunnelGroups(localCollapsedMap);\n persistGroupOrderToLocal(localOrderMap);\n persistGroupCollapsedToLocal(localCollapsedMap);\n setGroupPreferenceHydrated(true);\n };\n\n setGroupPreferenceHydrated(false);\n loadGroupPreferences();\n\n return () => {\n cancelled = true;\n };\n }, [tokenUserId, isAdmin]);\n\n useEffect(() => {\n const loadForwardCompactMode = async () => {\n try {\n const response = await getConfigByName(FORWARD_COMPACT_MODE_CONFIG_KEY);\n const enabled =\n response.code === 0 &&\n typeof response.data?.value === \"string\" &&\n response.data.value === \"true\";\n\n setCompactMode(enabled);\n } catch {\n setCompactMode(false);\n }\n };\n\n const handleCompactModeChanged = (event: Event) => {\n const customEvent = event as CustomEvent<{ enabled?: boolean }>;\n\n if (typeof customEvent.detail?.enabled === \"boolean\") {\n setCompactMode(customEvent.detail.enabled);\n }\n };\n\n loadForwardCompactMode();\n window.addEventListener(\n FORWARD_COMPACT_MODE_EVENT,\n handleCompactModeChanged,\n );\n\n return () => {\n window.removeEventListener(\n FORWARD_COMPACT_MODE_EVENT,\n handleCompactModeChanged,\n );\n };\n }, []);\n\n const parseShareIdFromTunnelName = (tunnelName: string): number | null => {\n const normalized = (tunnelName || \"\").trim();\n\n if (!normalized.startsWith(\"Share-\")) {\n return null;\n }\n\n const raw = normalized.slice(\"Share-\".length);\n const idx = raw.indexOf(\"-Port-\");\n\n if (idx <= 0) {\n return null;\n }\n\n const shareId = Number(raw.slice(0, idx).trim());\n\n return Number.isFinite(shareId) && shareId > 0 ? shareId : null;\n };\n\n const mergeFederationShareFlow = async (\n forwardsData: Forward[],\n ): Promise => {\n if (forwardsData.length === 0) {\n return forwardsData;\n }\n\n try {\n const [usageRes, localShareRes] = await Promise.all([\n getPeerRemoteUsageList(),\n getPeerShareList(),\n ]);\n\n const flowByShare = new Map();\n const shareIdsByTunnel = new Map>();\n\n if (usageRes.code === 0 && Array.isArray(usageRes.data)) {\n usageRes.data.forEach((item: Record) => {\n const shareId = Number(item.shareId || 0);\n const currentFlow = Number(item.currentFlow || 0);\n\n if (\n Number.isFinite(shareId) &&\n shareId > 0 &&\n Number.isFinite(currentFlow) &&\n currentFlow > 0\n ) {\n const prev = flowByShare.get(shareId) || 0;\n\n flowByShare.set(shareId, Math.max(prev, currentFlow));\n }\n\n if (Number.isFinite(shareId) && shareId > 0) {\n const bindings = Array.isArray(item.bindings)\n ? (item.bindings as Array>)\n : [];\n\n bindings.forEach((binding) => {\n const tunnelId = Number(binding.tunnelId || 0);\n const chainType = Number(binding.chainType || 0);\n\n if (!Number.isFinite(tunnelId) || tunnelId <= 0) {\n return;\n }\n\n if (Number.isFinite(chainType) && chainType !== 1) {\n return;\n }\n\n let shareSet = shareIdsByTunnel.get(tunnelId);\n\n if (!shareSet) {\n shareSet = new Set();\n shareIdsByTunnel.set(tunnelId, shareSet);\n }\n\n shareSet.add(shareId);\n });\n }\n });\n }\n\n if (localShareRes.code === 0 && Array.isArray(localShareRes.data)) {\n localShareRes.data.forEach((item: Record) => {\n const shareId = Number(item.id || 0);\n const currentFlow = Number(item.currentFlow || 0);\n\n if (\n Number.isFinite(shareId) &&\n shareId > 0 &&\n Number.isFinite(currentFlow) &&\n currentFlow > 0\n ) {\n const prev = flowByShare.get(shareId) || 0;\n\n flowByShare.set(shareId, Math.max(prev, currentFlow));\n }\n });\n }\n\n if (flowByShare.size === 0) {\n return forwardsData;\n }\n\n const resolveShareIdForForward = (forward: Forward): number | null => {\n const candidates = new Set();\n const shareIdFromName = parseShareIdFromTunnelName(\n forward.tunnelName || \"\",\n );\n\n if (shareIdFromName) {\n candidates.add(shareIdFromName);\n }\n\n const tunnelId = Number(forward.tunnelId || 0);\n const shareSetByTunnel = shareIdsByTunnel.get(tunnelId);\n\n if (shareSetByTunnel && shareSetByTunnel.size > 0) {\n shareSetByTunnel.forEach((shareId) => {\n if (Number.isFinite(shareId) && shareId > 0) {\n candidates.add(shareId);\n }\n });\n }\n\n if (candidates.size === 0) {\n return null;\n }\n\n let bestShareId: number | null = null;\n let bestFlow = 0;\n\n candidates.forEach((shareId) => {\n const shareFlow = flowByShare.get(shareId) || 0;\n\n if (shareFlow > bestFlow) {\n bestFlow = shareFlow;\n bestShareId = shareId;\n }\n });\n\n return bestShareId;\n };\n\n const resolvedShareByForwardId = new Map();\n\n forwardsData.forEach((forward) => {\n const shareId = resolveShareIdForForward(forward);\n\n if (shareId) {\n resolvedShareByForwardId.set(forward.id, shareId);\n }\n });\n\n const forwardCountByShare = new Map();\n\n forwardsData.forEach((forward) => {\n const shareId = resolvedShareByForwardId.get(forward.id) || null;\n\n if (!shareId || !flowByShare.has(shareId)) {\n return;\n }\n\n forwardCountByShare.set(\n shareId,\n (forwardCountByShare.get(shareId) || 0) + 1,\n );\n });\n\n return forwardsData.map((forward) => {\n const shareId = resolvedShareByForwardId.get(forward.id) || null;\n\n if (!shareId) {\n return { ...forward, federationShareFlow: undefined };\n }\n\n const shareFlow = flowByShare.get(shareId) || 0;\n\n if (shareFlow <= 0) {\n return { ...forward, federationShareFlow: undefined };\n }\n\n const directFlow = (forward.inFlow || 0) + (forward.outFlow || 0);\n\n if (directFlow > 0) {\n return { ...forward, federationShareFlow: undefined };\n }\n\n const count = forwardCountByShare.get(shareId) || 1;\n const estimated = Math.max(1, Math.floor(shareFlow / count));\n\n return { ...forward, federationShareFlow: estimated };\n });\n } catch {\n return forwardsData;\n }\n };\n\n const getForwardDisplayFlow = (forward: Forward): number => {\n const directFlow = (forward.inFlow || 0) + (forward.outFlow || 0);\n\n if (directFlow > 0) {\n return directFlow;\n }\n\n return forward.federationShareFlow || 0;\n };\n\n useEffect(() => {\n loadData();\n }, []);\n\n // 切换显示模式并保存到localStorage\n const handleViewModeChange = () => {\n const newMode = viewMode === \"grouped\" ? \"direct\" : \"grouped\";\n\n setViewMode(newMode);\n try {\n localStorage.setItem(\"forward-view-mode\", newMode);\n } catch {}\n };\n\n // 加载所有数据\n const loadData = async (lod = true) => {\n setLoading(lod);\n try {\n const [forwardsRes, tunnelsRes, speedLimitsRes] = await Promise.all([\n getForwardList(),\n userTunnel(),\n getSpeedLimitList(),\n ]);\n const [allTunnelsRes, nodesRes] = await Promise.allSettled([\n getTunnelList(),\n getNodeList(),\n ]);\n\n if (forwardsRes.code === 0) {\n const forwardsData =\n forwardsRes.data?.map((forward: any) => ({\n ...forward,\n serviceRunning: forward.status === 1,\n })) || [];\n\n const mergedForwards = await mergeFederationShareFlow(forwardsData);\n\n setForwards(mergedForwards);\n\n // 初始化拖拽排序顺序\n const currentUserId = JwtUtil.getUserIdFromToken();\n const { order, fromDatabase } = buildForwardOrder(\n mergedForwards,\n currentUserId,\n );\n\n setForwardOrder(order);\n\n if (fromDatabase) {\n saveOrder(FORWARD_ORDER_KEY, order);\n }\n } else {\n toast.error(forwardsRes.msg || \"获取转发列表失败\");\n }\n\n if (tunnelsRes.code === 0) {\n setTunnels(tunnelsRes.data || []);\n } else {\n }\n\n if (\n allTunnelsRes.status === \"fulfilled\" &&\n allTunnelsRes.value.code === 0\n ) {\n setAllTunnels((allTunnelsRes.value.data || []) as Tunnel[]);\n }\n\n if (nodesRes.status === \"fulfilled\" && nodesRes.value.code === 0) {\n setNodes((nodesRes.value.data || []) as Node[]);\n }\n\n if (speedLimitsRes.code === 0) {\n setSpeedLimits(speedLimitsRes.data || []);\n }\n } catch {\n toast.error(\"加载数据失败\");\n } finally {\n setLoading(false);\n }\n };\n\n // 表单验证\n const noLimitSpeedLimitIds = useMemo(() => {\n return new Set(\n speedLimits\n .filter((speedLimit) => speedLimit.name.trim() === \"不限速\")\n .map((speedLimit) => speedLimit.id),\n );\n }, [speedLimits]);\n\n const availableSpeedLimits = useMemo(() => {\n return speedLimits.filter(\n (speedLimit) => !noLimitSpeedLimitIds.has(speedLimit.id),\n );\n }, [speedLimits, noLimitSpeedLimitIds]);\n\n const normalizeSpeedId = (speedId?: number | null): number | null => {\n if (speedId === null || speedId === undefined) {\n return null;\n }\n\n return noLimitSpeedLimitIds.has(speedId) ? null : speedId;\n };\n\n const selectedSpeedId = normalizeSpeedId(form.speedId);\n\n const validateForm = (): boolean => {\n const newErrors: { [key: string]: string } = {};\n\n if (!form.name.trim()) {\n newErrors.name = \"请输入转发名称\";\n } else if (form.name.length < 2 || form.name.length > 50) {\n newErrors.name = \"转发名称长度应在2-50个字符之间\";\n }\n\n if (!form.tunnelId) {\n newErrors.tunnelId = \"请选择关联隧道\";\n }\n\n // 验证入口端口(可选,如果填写则验证)\n if (form.inPort !== null && form.inPort !== undefined) {\n const port = Number(form.inPort);\n\n if (isNaN(port) || port < 1 || port > 65535) {\n newErrors.inPort = \"端口必须在 1-65535 之间\";\n }\n }\n\n if (!form.remoteAddr.trim()) {\n newErrors.remoteAddr = \"请输入远程地址\";\n } else {\n // 验证地址格式\n const addresses = form.remoteAddr\n .split(\"\\n\")\n .map((addr) => addr.trim())\n .filter((addr) => addr);\n const ipv4Pattern =\n /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):\\d+$/;\n const ipv6FullPattern =\n /^\\[((([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:))|(([0-9a-fA-F]{1,4}:){6}(:[0-9a-fA-F]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9a-fA-F]{1,4}:){5}(((:[0-9a-fA-F]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9a-fA-F]{1,4}:){4}(((:[0-9a-fA-F]{1,4}){1,3})|((:[0-9a-fA-F]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){3}(((:[0-9a-fA-F]{1,4}){1,4})|((:[0-9a-fA-F]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){2}(((:[0-9a-fA-F]{1,4}){1,5})|((:[0-9a-fA-F]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){1}(((:[0-9a-fA-F]{1,4}){1,6})|((:[0-9a-fA-F]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9a-fA-F]{1,4}){1,7})|((:[0-9a-fA-F]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))\\]:\\d+$/;\n const domainPattern =\n /^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*:\\d+$/;\n\n for (let i = 0; i < addresses.length; i++) {\n const addr = addresses[i];\n\n if (\n !ipv4Pattern.test(addr) &&\n !ipv6FullPattern.test(addr) &&\n !domainPattern.test(addr)\n ) {\n newErrors.remoteAddr = `第${i + 1}行地址格式错误`;\n break;\n }\n }\n }\n\n setErrors(newErrors);\n\n return Object.keys(newErrors).length === 0;\n };\n\n // 新增转发\n const handleAdd = () => {\n setIsEdit(false);\n setForm({\n name: \"\",\n tunnelId: null,\n inPort: null,\n inIp: \"\",\n remoteAddr: \"\",\n interfaceName: \"\",\n strategy: \"fifo\",\n speedId: null,\n });\n setErrors({});\n setModalOpen(true);\n };\n\n // 编辑转发\n const handleEdit = (forward: Forward) => {\n setIsEdit(true);\n setForm({\n id: forward.id,\n userId: forward.userId,\n name: forward.name,\n tunnelId: forward.tunnelId,\n inPort: forward.inPort,\n inIp: forward.inIp || \"\",\n remoteAddr: forward.remoteAddr.split(\",\").join(\"\\n\"),\n interfaceName: forward.interfaceName || \"\",\n strategy: forward.strategy || \"fifo\",\n speedId: normalizeSpeedId(forward.speedId),\n });\n setErrors({});\n setModalOpen(true);\n };\n\n // 显示删除确认\n const handleDelete = (forward: Forward) => {\n setForwardToDelete(forward);\n setDeleteModalOpen(true);\n };\n\n // 确认删除转发\n const confirmDelete = async () => {\n if (!forwardToDelete) return;\n\n setDeleteLoading(true);\n try {\n const res = await deleteForward(forwardToDelete.id);\n\n if (res.code === 0) {\n toast.success(\"删除成功\");\n setDeleteModalOpen(false);\n loadData();\n } else {\n // 删除失败,询问是否强制删除\n const confirmed = window.confirm(\n `常规删除失败:${res.msg || \"删除失败\"}\\n\\n是否需要强制删除?\\n\\n⚠️ 注意:强制删除不会去验证节点端是否已经删除对应的转发服务。`,\n );\n\n if (confirmed) {\n const forceRes = await forceDeleteForward(forwardToDelete.id);\n\n if (forceRes.code === 0) {\n toast.success(\"强制删除成功\");\n setDeleteModalOpen(false);\n loadData();\n } else {\n toast.error(forceRes.msg || \"强制删除失败\");\n }\n }\n }\n } catch {\n toast.error(\"删除失败\");\n } finally {\n setDeleteLoading(false);\n }\n };\n\n // 处理隧道选择变化\n const handleTunnelChange = (tunnelId: string) => {\n const nextTunnelId = parseInt(tunnelId);\n const options = tunnelInIpOptionMap.get(nextTunnelId) || [];\n\n setForm((prev) => ({\n ...prev,\n tunnelId: nextTunnelId,\n inIp: options.includes(prev.inIp) ? prev.inIp : \"\",\n }));\n };\n\n // 提交表单\n const handleSubmit = async () => {\n if (!validateForm()) return;\n\n setSubmitLoading(true);\n try {\n const processedRemoteAddr = form.remoteAddr\n .split(\"\\n\")\n .map((addr) => addr.trim())\n .filter((addr) => addr)\n .join(\",\");\n\n const addressCount = processedRemoteAddr.split(\",\").length;\n\n let res: { code: number; msg: string };\n\n if (isEdit) {\n // 更新时确保包含必要字段\n const updateData = {\n id: form.id,\n userId: form.userId,\n name: form.name,\n tunnelId: form.tunnelId,\n inPort: form.inPort,\n inIp: form.inIp || undefined,\n remoteAddr: processedRemoteAddr,\n strategy: addressCount > 1 ? form.strategy : \"fifo\",\n speedId: normalizeSpeedId(form.speedId),\n };\n\n res = await updateForward(updateData);\n } else {\n const createData = {\n name: form.name,\n tunnelId: form.tunnelId,\n inPort: form.inPort,\n inIp: form.inIp || undefined,\n remoteAddr: processedRemoteAddr,\n strategy: addressCount > 1 ? form.strategy : \"fifo\",\n speedId: normalizeSpeedId(form.speedId),\n };\n\n res = await createForward(createData);\n }\n\n if (res.code === 0) {\n toast.success(isEdit ? \"修改成功\" : \"创建成功\");\n setModalOpen(false);\n loadData();\n } else {\n toast.error(res.msg || \"操作失败\");\n }\n } catch {\n toast.error(\"操作失败\");\n } finally {\n setSubmitLoading(false);\n }\n };\n\n // 处理服务开关\n const handleServiceToggle = async (forward: Forward) => {\n if (forward.status !== 1 && forward.status !== 0) {\n toast.error(\"转发状态异常,无法操作\");\n\n return;\n }\n\n const targetState = !forward.serviceRunning;\n\n try {\n // 乐观更新UI\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, serviceRunning: targetState } : f,\n ),\n );\n\n let res: { code: number; msg: string };\n\n if (targetState) {\n res = await resumeForwardService(forward.id);\n } else {\n res = await pauseForwardService(forward.id);\n }\n\n if (res.code === 0) {\n toast.success(targetState ? \"服务已启动\" : \"服务已暂停\");\n // 更新转发状态\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, status: targetState ? 1 : 0 } : f,\n ),\n );\n } else {\n // 操作失败,恢复UI状态\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, serviceRunning: !targetState } : f,\n ),\n );\n toast.error(res.msg || \"操作失败\");\n }\n } catch {\n // 操作失败,恢复UI状态\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, serviceRunning: !targetState } : f,\n ),\n );\n toast.error(\"网络错误,操作失败\");\n }\n };\n\n // 诊断转发\n const handleDiagnose = async (forward: Forward) => {\n diagnosisAbortRef.current?.abort();\n const abortController = new AbortController();\n\n diagnosisAbortRef.current = abortController;\n\n setCurrentDiagnosisForward(forward);\n setDiagnosisModalOpen(true);\n setDiagnosisLoading(true);\n setDiagnosisProgress({\n total: 0,\n completed: 0,\n success: 0,\n failed: 0,\n timedOut: false,\n });\n setDiagnosisResult({\n forwardName: forward.name,\n timestamp: Date.now(),\n results: [],\n });\n\n try {\n let streamErrorMessage = \"\";\n const streamResult = await diagnoseForwardStream(\n forward.id,\n {\n onStart: (payload) => {\n const startForwardName =\n typeof payload.forwardName === \"string\" &&\n payload.forwardName.trim() !== \"\"\n ? payload.forwardName\n : forward.name;\n const startTotal = Number(payload.total);\n const startItems = Array.isArray(payload.items)\n ? (payload.items as ForwardDiagnosisResult[\"results\"])\n : [];\n\n setDiagnosisResult((prev) => ({\n forwardName: startForwardName,\n timestamp: Date.now(),\n results: startItems.length > 0 ? startItems : prev?.results || [],\n }));\n if (Number.isFinite(startTotal) && startTotal >= 0) {\n setDiagnosisProgress((prev) => ({\n ...prev,\n total: startTotal,\n }));\n }\n },\n onItem: ({ result, progress }) => {\n setDiagnosisResult((prev) => {\n const base: ForwardDiagnosisResult = prev || {\n forwardName: forward.name,\n timestamp: Date.now(),\n results: [],\n };\n const nextResults = [...base.results];\n const existingIndex = nextResults.findIndex(\n (item) =>\n item.description === result.description &&\n item.nodeId === result.nodeId &&\n item.targetIp === result.targetIp &&\n item.targetPort === result.targetPort,\n );\n\n if (existingIndex >= 0) {\n nextResults[existingIndex] = {\n ...result,\n diagnosing: false,\n };\n } else {\n nextResults.push({\n ...result,\n diagnosing: false,\n });\n }\n\n return {\n ...base,\n timestamp: Date.now(),\n results: nextResults,\n };\n });\n setDiagnosisProgress({\n total: progress.total,\n completed: progress.completed,\n success: progress.success,\n failed: progress.failed,\n timedOut: Boolean(progress.timedOut),\n });\n },\n onDone: (progress) => {\n setDiagnosisProgress({\n total: progress.total,\n completed: progress.completed,\n success: progress.success,\n failed: progress.failed,\n timedOut: Boolean(progress.timedOut),\n });\n },\n onError: (message) => {\n streamErrorMessage = message;\n },\n },\n abortController.signal,\n );\n\n if (streamResult.fallback) {\n const response = await diagnoseForward(forward.id);\n\n if (response.code === 0) {\n const resultData = response.data as ForwardDiagnosisResult;\n const successCount = resultData.results.filter(\n (r) => r.success,\n ).length;\n const failedCount = resultData.results.length - successCount;\n\n setDiagnosisResult(resultData);\n setDiagnosisProgress({\n total: resultData.results.length,\n completed: resultData.results.length,\n success: successCount,\n failed: failedCount,\n timedOut: false,\n });\n } else {\n toast.error(response.msg || \"诊断失败\");\n setDiagnosisResult(\n buildForwardDiagnosisFallbackResult({\n forwardName: forward.name,\n remoteAddr: forward.remoteAddr,\n description: \"诊断失败\",\n message: response.msg || \"诊断过程中发生错误\",\n }),\n );\n setDiagnosisProgress({\n total: 1,\n completed: 1,\n success: 0,\n failed: 1,\n timedOut: false,\n });\n }\n\n return;\n }\n\n if (streamErrorMessage) {\n toast.error(streamErrorMessage);\n }\n if (streamResult.timedOut) {\n toast.error(\"诊断超时(单条30秒 / 整体2分钟),已返回当前结果\");\n }\n } catch {\n if (abortController.signal.aborted) {\n return;\n }\n toast.error(\"网络错误,请重试\");\n setDiagnosisResult(\n buildForwardDiagnosisFallbackResult({\n forwardName: forward.name,\n remoteAddr: forward.remoteAddr,\n description: \"网络错误\",\n message: \"无法连接到服务器\",\n }),\n );\n setDiagnosisProgress({\n total: 1,\n completed: 1,\n success: 0,\n failed: 1,\n timedOut: false,\n });\n } finally {\n if (diagnosisAbortRef.current === abortController) {\n diagnosisAbortRef.current = null;\n }\n setDiagnosisLoading(false);\n }\n };\n\n // 格式化流量\n const formatFlow = (value: number): string => {\n if (value === 0) return \"0 B\";\n if (value < 1024) return value + \" B\";\n if (value < 1024 * 1024) return (value / 1024).toFixed(2) + \" KB\";\n if (value < 1024 * 1024 * 1024)\n return (value / (1024 * 1024)).toFixed(2) + \" MB\";\n\n return (value / (1024 * 1024 * 1024)).toFixed(2) + \" GB\";\n };\n\n // 显示地址列表弹窗\n const showAddressModal = (\n addressString: string,\n port: number | null,\n title: string,\n ) => {\n const action = resolveForwardAddressAction(addressString, port, title);\n\n if (action.type === \"none\") {\n return;\n }\n\n if (action.type === \"copy\") {\n copyToClipboard(action.text, action.label);\n\n return;\n }\n\n setAddressList(action.items);\n setAddressModalTitle(action.title);\n setAddressModalOpen(true);\n };\n\n // 复制到剪贴板\n const copyToClipboard = async (text: string, label: string = \"内容\") => {\n try {\n await navigator.clipboard.writeText(text);\n toast.success(`已复制${label}`);\n } catch {\n toast.error(\"复制失败\");\n }\n };\n\n // 复制地址\n const copyAddress = async (addressItem: ForwardAddressItem) => {\n try {\n setAddressList((prev) =>\n prev.map((item) =>\n item.id === addressItem.id ? { ...item, copying: true } : item,\n ),\n );\n await copyToClipboard(addressItem.address, \"地址\");\n } catch {\n toast.error(\"复制失败\");\n } finally {\n setAddressList((prev) =>\n prev.map((item) =>\n item.id === addressItem.id ? { ...item, copying: false } : item,\n ),\n );\n }\n };\n\n // 复制所有地址\n const copyAllAddresses = async () => {\n if (addressList.length === 0) return;\n const allAddresses = addressList.map((item) => item.address).join(\"\\n\");\n\n await copyToClipboard(allAddresses, \"所有地址\");\n };\n\n // 导出转发数据\n const handleExport = () => {\n setSelectedTunnelForExport(null);\n setExportData(\"\");\n setExportModalOpen(true);\n };\n\n // 执行导出\n const executeExport = () => {\n if (!selectedTunnelForExport) {\n toast.error(\"请选择要导出的隧道\");\n\n return;\n }\n\n setExportLoading(true);\n\n try {\n // 获取要导出的转发列表\n const forwardsToExport = sortedForwards.filter(\n (forward) => forward.tunnelId === selectedTunnelForExport,\n );\n\n if (forwardsToExport.length === 0) {\n toast.error(\"所选隧道没有转发数据\");\n setExportLoading(false);\n\n return;\n }\n\n // 格式化导出数据:remoteAddr|name|inPort\n const exportLines = forwardsToExport.map((forward) => {\n return `${forward.remoteAddr}|${forward.name}|${forward.inPort}`;\n });\n\n const exportText = exportLines.join(\"\\n\");\n\n setExportData(exportText);\n } catch {\n toast.error(\"导出失败\");\n } finally {\n setExportLoading(false);\n }\n };\n\n // 复制导出数据\n const copyExportData = async () => {\n await copyToClipboard(exportData, \"转发数据\");\n };\n\n // 导入转发数据\n const handleImport = () => {\n setImportData(\"\");\n setImportResults([]);\n setSelectedTunnelForImport(null);\n setImportModalOpen(true);\n };\n\n // 执行导入\n const executeImport = async () => {\n if (!importData.trim()) {\n toast.error(\"请输入要导入的数据\");\n\n return;\n }\n\n if (!selectedTunnelForImport) {\n toast.error(\"请选择要导入的隧道\");\n\n return;\n }\n\n setImportLoading(true);\n setImportResults([]);\n\n try {\n if (importFormat === \"ny\") {\n const parsedItems = parseNyFormatData(importData);\n\n if (parsedItems.length === 0) {\n toast.error(\"未解析到有效的ny格式数据\");\n\n setImportLoading(false);\n\n return;\n }\n\n for (const item of parsedItems) {\n if (item.error) {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: item.error || \"解析失败\",\n },\n ...prev,\n ]);\n\n continue;\n }\n\n if (!item.parsed) {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: \"解析失败\",\n },\n ...prev,\n ]);\n\n continue;\n }\n\n const parsedNyItem = item.parsed;\n const nyForwardInput = convertNyItemToForwardInput(parsedNyItem);\n\n try {\n const response = await createForward({\n name: nyForwardInput.name,\n tunnelId: selectedTunnelForImport,\n inPort: nyForwardInput.inPort,\n remoteAddr: nyForwardInput.remoteAddr,\n strategy: nyForwardInput.strategy,\n });\n\n if (response.code === 0) {\n setImportResults((prev) => [\n {\n line: item.line,\n success: true,\n message: `创建成功 (${parsedNyItem.dest.length}个目标)`,\n forwardName: nyForwardInput.name,\n },\n ...prev,\n ]);\n } else {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: response.msg || \"创建失败\",\n },\n ...prev,\n ]);\n }\n } catch {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: \"网络错误,创建失败\",\n },\n ...prev,\n ]);\n }\n }\n } else {\n const lines = importData\n .trim()\n .split(\"\\n\")\n .filter((line) => line.trim());\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n const parts = line.split(\"|\");\n\n if (parts.length < 2) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"格式错误:需要至少包含目标地址和转发名称\",\n },\n ...prev,\n ]);\n continue;\n }\n\n const [remoteAddr, name, inPort] = parts;\n\n if (!remoteAddr.trim() || !name.trim()) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"目标地址和转发名称不能为空\",\n },\n ...prev,\n ]);\n continue;\n }\n\n const addresses = remoteAddr.trim().split(\",\");\n const addressPattern = /^[^:]+:\\d+$/;\n const isValidFormat = addresses.every((addr) =>\n addressPattern.test(addr.trim()),\n );\n\n if (!isValidFormat) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message:\n \"目标地址格式错误,应为 地址:端口 格式,多个地址用逗号分隔\",\n },\n ...prev,\n ]);\n continue;\n }\n\n try {\n let portNumber: number | null = null;\n\n if (inPort && inPort.trim()) {\n const port = parseInt(inPort.trim());\n\n if (isNaN(port) || port < 1 || port > 65535) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"入口端口格式错误,应为1-65535之间的数字\",\n },\n ...prev,\n ]);\n continue;\n }\n portNumber = port;\n }\n\n const response = await createForward({\n name: name.trim(),\n tunnelId: selectedTunnelForImport,\n inPort: portNumber,\n remoteAddr: remoteAddr.trim(),\n strategy: \"fifo\",\n });\n\n if (response.code === 0) {\n setImportResults((prev) => [\n {\n line,\n success: true,\n message: \"创建成功\",\n forwardName: name.trim(),\n },\n ...prev,\n ]);\n } else {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: response.msg || \"创建失败\",\n },\n ...prev,\n ]);\n }\n } catch {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"网络错误,创建失败\",\n },\n ...prev,\n ]);\n }\n }\n }\n\n toast.success(\"导入执行完成\");\n\n await loadData(false);\n } catch {\n toast.error(\"导入过程中发生错误\");\n } finally {\n setImportLoading(false);\n }\n };\n\n // 获取状态显示\n const getStatusDisplay = (status: number) => {\n switch (status) {\n case 1:\n return { color: \"success\", text: \"正常\" };\n case 0:\n return { color: \"warning\", text: \"暂停\" };\n case -1:\n return { color: \"danger\", text: \"异常\" };\n default:\n return { color: \"default\", text: \"未知\" };\n }\n };\n\n // 获取策略显示\n const getStrategyDisplay = (strategy: string) => {\n switch (strategy) {\n case \"fifo\":\n return { color: \"primary\", text: \"主备\" };\n case \"round\":\n return { color: \"success\", text: \"轮询\" };\n case \"rand\":\n return { color: \"warning\", text: \"随机\" };\n default:\n return { color: \"default\", text: \"未知\" };\n }\n };\n\n // 获取地址数量\n const getAddressCount = (addressString: string): number => {\n if (!addressString) return 0;\n const addresses = addressString\n .split(\"\\n\")\n .map((addr) => addr.trim())\n .filter((addr) => addr);\n\n return addresses.length;\n };\n\n // 处理拖拽结束\n const handleDragEnd = async (event: DragEndEvent) => {\n const { active, over } = event;\n\n if (!active || !over || active.id === over.id) return;\n\n const activeGroup = parseTunnelGroupSortableId(active.id);\n const overGroup = parseTunnelGroupSortableId(over.id);\n\n if (activeGroup && overGroup) {\n if (compactMode || !groupPreferenceHydrated) {\n return;\n }\n\n if (activeGroup.userId !== overGroup.userId) {\n return;\n }\n\n const userIdKey = activeGroup.userId.toString();\n const currentOrder = groupOrderMap[userIdKey] || [];\n const oldIndex = currentOrder.indexOf(activeGroup.tunnelKey);\n const newIndex = currentOrder.indexOf(overGroup.tunnelKey);\n\n if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) {\n return;\n }\n\n const moved = arrayMove(currentOrder, oldIndex, newIndex);\n const nextOrderMap: ForwardGroupOrderMap = {\n ...groupOrderMap,\n [userIdKey]: moved,\n };\n\n setGroupOrderMap(nextOrderMap);\n persistGroupOrderToLocal(nextOrderMap);\n void persistGroupOrderToGlobal(nextOrderMap);\n\n return;\n }\n\n // 确保 forwardOrder 存在且有效\n if (!forwardOrder || forwardOrder.length === 0) return;\n\n const activeId = Number(active.id);\n const overId = Number(over.id);\n\n // 检查 ID 是否有效\n if (isNaN(activeId) || isNaN(overId)) return;\n\n const activeForward = forwards.find((forward) => forward.id === activeId);\n const overForward = forwards.find((forward) => forward.id === overId);\n const activeUserId = activeForward?.userId ?? 0;\n const overUserId = overForward?.userId ?? 0;\n const activeTunnelGroupKey = buildForwardTunnelGroupKey(\n activeForward?.tunnelName,\n );\n const overTunnelGroupKey = buildForwardTunnelGroupKey(\n overForward?.tunnelName,\n );\n\n // 非精简模式仅允许在同一用户+隧道分组内拖拽,避免混排\n if (!compactMode) {\n if (\n activeUserId !== overUserId ||\n activeTunnelGroupKey !== overTunnelGroupKey\n ) {\n return;\n }\n }\n\n const oldIndex = forwardOrder.indexOf(activeId);\n const newIndex = forwardOrder.indexOf(overId);\n\n if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {\n const newOrder = arrayMove(forwardOrder, oldIndex, newIndex);\n\n setForwardOrder(newOrder);\n\n saveOrder(FORWARD_ORDER_KEY, newOrder);\n\n // 持久化到数据库\n try {\n const forwardsToUpdate = newOrder.map((id, index) => ({\n id,\n inx: index,\n }));\n\n const response = await updateForwardOrder({\n forwards: forwardsToUpdate,\n });\n\n if (response.code === 0) {\n // 更新本地数据中的 inx 字段\n setForwards((prev) =>\n prev.map((forward) => {\n const updatedForward = forwardsToUpdate.find(\n (f) => f.id === forward.id,\n );\n\n if (updatedForward) {\n return { ...forward, inx: updatedForward.inx };\n }\n\n return forward;\n }),\n );\n } else {\n toast.error(\"保存排序失败:\" + (response.msg || \"未知错误\"));\n }\n } catch {\n toast.error(\"保存排序失败,请重试\");\n }\n }\n };\n\n const toggleSelectMode = () => {\n setSelectMode(!selectMode);\n if (selectMode) {\n setSelectedIds(new Set());\n }\n };\n\n const toggleSelect = (id: number) => {\n const newSet = new Set(selectedIds);\n\n if (newSet.has(id)) {\n newSet.delete(id);\n } else {\n newSet.add(id);\n }\n setSelectedIds(newSet);\n };\n\n const selectAll = () => {\n const allIds = sortedForwards.map((f) => f.id);\n\n setSelectedIds(new Set(allIds));\n };\n\n const deselectAll = () => {\n setSelectedIds(new Set());\n };\n\n const handleBatchDelete = async () => {\n if (selectedIds.size === 0) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchDelete(Array.from(selectedIds));\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n if (outcome.closeDeleteModal) {\n setBatchDeleteModalOpen(false);\n }\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n const handleBatchToggleService = async (enable: boolean) => {\n if (selectedIds.size === 0) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchToggleService(\n Array.from(selectedIds),\n enable,\n );\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n const handleBatchRedeploy = async () => {\n if (selectedIds.size === 0) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchRedeploy(\n Array.from(selectedIds),\n );\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n const handleBatchChangeTunnel = async () => {\n if (selectedIds.size === 0 || !batchTargetTunnelId) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchChangeTunnel(\n Array.from(selectedIds),\n batchTargetTunnelId,\n );\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n if (outcome.closeChangeTunnelModal) {\n setBatchChangeTunnelModalOpen(false);\n }\n if (outcome.resetTargetTunnel) {\n setBatchTargetTunnelId(null);\n }\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n // 传感器配置 - 使用默认配置避免错误\n const sensors = useSensors(\n useSensor(MouseSensor, {\n activationConstraint: {\n distance: 8,\n },\n }),\n useSensor(TouchSensor, {\n activationConstraint: {\n delay: 250,\n tolerance: 8,\n },\n }),\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n }),\n );\n\n // 根据排序顺序获取转发列表\n const orderedForwards = useMemo((): Forward[] => {\n // 确保 forwards 数组存在且有效\n if (!forwards || forwards.length === 0) {\n return [];\n }\n\n let filteredForwards = forwards;\n\n if (filterUserId !== \"all\") {\n const targetUserId = parseInt(filterUserId);\n\n filteredForwards = filteredForwards.filter(\n (f) => f.userId === targetUserId || (targetUserId === 0 && !f.userId),\n );\n }\n if (filterTunnelId !== \"all\") {\n const targetTunnelId = parseInt(filterTunnelId);\n\n filteredForwards = filteredForwards.filter(\n (f) => f.tunnelId === targetTunnelId,\n );\n }\n\n if (searchKeyword.trim()) {\n const lowerKeyword = searchKeyword.toLowerCase();\n\n filteredForwards = filteredForwards.filter(\n (f) =>\n (f.name && f.name.toLowerCase().includes(lowerKeyword)) ||\n (f.remoteAddr && f.remoteAddr.toLowerCase().includes(lowerKeyword)) ||\n (f.userName && f.userName.toLowerCase().includes(lowerKeyword)),\n );\n }\n\n // 确保过滤后的转发列表有效\n if (!filteredForwards || filteredForwards.length === 0) {\n return [];\n }\n\n // 优先使用数据库中的 inx 字段进行排序\n const sortedByDb = [...filteredForwards].sort((a, b) => {\n const aInx = a.inx ?? 0;\n const bInx = b.inx ?? 0;\n\n if (aInx !== bInx) {\n return aInx - bInx;\n }\n\n return (a.id ?? 0) - (b.id ?? 0);\n });\n\n // 如果数据库中没有排序信息,则使用本地存储的顺序\n if (\n forwardOrder &&\n forwardOrder.length > 0 &&\n sortedByDb.every((f) => f.inx === undefined || f.inx === 0)\n ) {\n const forwardMap = new Map(filteredForwards.map((f) => [f.id, f]));\n const localSortedForwards: Forward[] = [];\n\n forwardOrder.forEach((id) => {\n const forward = forwardMap.get(id);\n\n if (forward) {\n localSortedForwards.push(forward);\n }\n });\n\n // 添加不在排序列表中的转发(新添加的)\n filteredForwards.forEach((forward) => {\n if (!forwardOrder.includes(forward.id)) {\n localSortedForwards.push(forward);\n }\n });\n\n return localSortedForwards;\n }\n\n return sortedByDb;\n }, [forwards, forwardOrder, filterUserId, filterTunnelId, searchKeyword]);\n\n const availableGroupData = useMemo(\n () => buildAvailableGroupData(forwards),\n [forwards],\n );\n\n const sanitizedGroupOrderMap = useMemo(\n () =>\n sanitizeGroupOrderMap(\n groupOrderMap,\n availableGroupData.availableTunnelKeysByUser,\n ),\n [groupOrderMap, availableGroupData],\n );\n\n const sanitizedCollapsedTunnelGroups = useMemo(\n () =>\n sanitizeGroupCollapsedMap(\n collapsedTunnelGroups,\n availableGroupData.availableCollapseKeys,\n ),\n [collapsedTunnelGroups, availableGroupData],\n );\n\n useEffect(() => {\n if (!groupPreferenceHydrated || tokenUserId === null) {\n return;\n }\n\n if (!isSameGroupOrderMap(groupOrderMap, sanitizedGroupOrderMap)) {\n setGroupOrderMap(sanitizedGroupOrderMap);\n persistGroupOrderToLocal(sanitizedGroupOrderMap);\n void persistGroupOrderToGlobal(sanitizedGroupOrderMap);\n }\n\n if (\n !isSameGroupCollapsedMap(\n collapsedTunnelGroups,\n sanitizedCollapsedTunnelGroups,\n )\n ) {\n setCollapsedTunnelGroups(sanitizedCollapsedTunnelGroups);\n persistGroupCollapsedToLocal(sanitizedCollapsedTunnelGroups);\n void persistGroupCollapsedToGlobal(sanitizedCollapsedTunnelGroups);\n }\n }, [\n groupPreferenceHydrated,\n tokenUserId,\n groupOrderMap,\n sanitizedGroupOrderMap,\n collapsedTunnelGroups,\n sanitizedCollapsedTunnelGroups,\n ]);\n\n const groupedForwards = useMemo((): ForwardUserGroup[] => {\n if (orderedForwards.length === 0) {\n return [];\n }\n\n type MutableForwardUserGroup = {\n userId: number;\n userName: string;\n tunnelMap: Map;\n };\n\n const userGroupMap = new Map();\n\n orderedForwards.forEach((forward) => {\n const userId = forward.userId ?? 0;\n const userName = normalizeForwardUserName(forward.userName);\n const tunnelName = normalizeForwardTunnelName(forward.tunnelName);\n const tunnelKey = buildForwardTunnelGroupKey(forward.tunnelName);\n\n let existingGroup = userGroupMap.get(userId);\n\n if (!existingGroup) {\n existingGroup = {\n userId,\n userName,\n tunnelMap: new Map(),\n };\n userGroupMap.set(userId, existingGroup);\n } else if (\n existingGroup.userName === UNKNOWN_FORWARD_USER_NAME &&\n userName !== UNKNOWN_FORWARD_USER_NAME\n ) {\n existingGroup.userName = userName;\n }\n\n const existingTunnelGroup = existingGroup.tunnelMap.get(tunnelKey);\n\n if (!existingTunnelGroup) {\n existingGroup.tunnelMap.set(tunnelKey, {\n tunnelKey,\n tunnelName,\n items: [forward],\n });\n\n return;\n }\n\n existingTunnelGroup.items.push(forward);\n\n if (\n existingTunnelGroup.tunnelName === UNCATEGORIZED_FORWARD_TUNNEL_NAME &&\n tunnelName !== UNCATEGORIZED_FORWARD_TUNNEL_NAME\n ) {\n existingTunnelGroup.tunnelName = tunnelName;\n }\n });\n\n const groups = Array.from(userGroupMap.values()).map((group) => {\n const tunnels = Array.from(group.tunnelMap.values());\n const tunnelOrder = sanitizedGroupOrderMap[group.userId.toString()] || [];\n const tunnelOrderIndex = new Map();\n\n tunnelOrder.forEach((key, index) => {\n tunnelOrderIndex.set(key, index);\n });\n\n tunnels.sort((a, b) => {\n const aIndex = tunnelOrderIndex.get(a.tunnelKey);\n const bIndex = tunnelOrderIndex.get(b.tunnelKey);\n\n if (aIndex !== undefined || bIndex !== undefined) {\n if (aIndex === undefined) {\n return 1;\n }\n\n if (bIndex === undefined) {\n return -1;\n }\n\n return aIndex - bIndex;\n }\n\n const nameCompare = compareForwardTunnelNameAsc(\n a.tunnelName,\n b.tunnelName,\n );\n\n if (nameCompare !== 0) {\n return nameCompare;\n }\n\n return compareForwardTunnelNameAsc(a.tunnelKey, b.tunnelKey);\n });\n\n return {\n userId: group.userId,\n userName: group.userName,\n tunnels,\n };\n });\n\n groups.sort((a, b) => {\n if (isAdmin && tokenUserId !== null) {\n const aIsSelf = a.userId === tokenUserId;\n const bIsSelf = b.userId === tokenUserId;\n\n if (aIsSelf !== bIsSelf) {\n return aIsSelf ? -1 : 1;\n }\n }\n\n const nameCompare = compareForwardUserNameAsc(a.userName, b.userName);\n\n if (nameCompare !== 0) {\n return nameCompare;\n }\n\n return a.userId - b.userId;\n });\n\n return groups;\n }, [orderedForwards, isAdmin, tokenUserId, sanitizedGroupOrderMap]);\n\n const sortedForwards = useMemo(() => {\n if (compactMode) {\n return orderedForwards;\n }\n\n return groupedForwards.flatMap((group) =>\n group.tunnels.flatMap((tunnel) => tunnel.items),\n );\n }, [compactMode, orderedForwards, groupedForwards]);\n\n const sortableForwardIds = useMemo(\n () => sortedForwards.map((f) => f.id).filter((id) => id > 0),\n [sortedForwards],\n );\n\n const toggleTunnelGroupCollapsed = (userId: number, tunnelKey: string) => {\n const collapseKey = buildTunnelGroupCollapseKey(userId, tunnelKey);\n const nextCollapsedMap: ForwardGroupCollapsedMap = {\n ...sanitizedCollapsedTunnelGroups,\n };\n\n if (nextCollapsedMap[collapseKey] === true) {\n delete nextCollapsedMap[collapseKey];\n } else {\n nextCollapsedMap[collapseKey] = true;\n }\n\n setCollapsedTunnelGroups(nextCollapsedMap);\n persistGroupCollapsedToLocal(nextCollapsedMap);\n void persistGroupCollapsedToGlobal(nextCollapsedMap);\n };\n\n const SortableTunnelGroupContainer = ({\n groupUserId,\n tunnel,\n collapsed,\n onToggleCollapsed,\n wrapperClassName,\n headerClassName,\n titleClassName,\n countClassName,\n bodyClassName,\n children,\n }: {\n groupUserId: number;\n tunnel: ForwardTunnelGroup;\n collapsed: boolean;\n onToggleCollapsed: () => void;\n wrapperClassName: string;\n headerClassName: string;\n titleClassName: string;\n countClassName: string;\n bodyClassName: string;\n children: React.ReactNode;\n }) => {\n const sortableId = buildTunnelGroupSortableId(\n groupUserId,\n tunnel.tunnelKey,\n );\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: sortableId });\n\n const style: React.CSSProperties = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.55 : 1,\n willChange: isDragging ? \"transform\" : undefined,\n zIndex: isDragging ? 1 : undefined,\n };\n\n return (\n
\n
\n
\n \n \n \n \n \n {tunnel.tunnelName}\n
\n
\n {tunnel.items.length} 条转发\n \n \n \n \n
\n
\n
\n {!collapsed &&
{children}
}\n \n );\n };\n\n // 可拖拽的转发卡片组件\n const SortableForwardCard = ({ forward }: { forward: Forward }) => {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: forward.id });\n\n const style: React.CSSProperties = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.5 : 1,\n willChange: isDragging ? \"transform\" : undefined,\n };\n\n return (\n
\n {renderForwardCard(forward, listeners)}\n
\n );\n };\n\n // 生成用作筛选项的用户和隧道列表\n const uniqueUsers = useMemo(() => {\n const userMap = new Map();\n\n forwards.forEach((f) => {\n const uId = f.userId ?? 0;\n const userName = normalizeForwardUserName(f.userName);\n const existingUser = userMap.get(uId);\n\n if (!existingUser) {\n userMap.set(uId, { id: uId, name: userName });\n\n return;\n }\n\n if (\n existingUser.name === UNKNOWN_FORWARD_USER_NAME &&\n userName !== UNKNOWN_FORWARD_USER_NAME\n ) {\n existingUser.name = userName;\n }\n });\n\n const users = Array.from(userMap.values());\n\n users.sort((a, b) => {\n if (isAdmin && tokenUserId !== null) {\n const aIsSelf = a.id === tokenUserId;\n const bIsSelf = b.id === tokenUserId;\n\n if (aIsSelf !== bIsSelf) {\n return aIsSelf ? -1 : 1;\n }\n }\n\n const nameCompare = compareForwardUserNameAsc(a.name, b.name);\n\n if (nameCompare !== 0) {\n return nameCompare;\n }\n\n return a.id - b.id;\n });\n\n return users;\n }, [forwards, isAdmin, tokenUserId]);\n\n // 可拖拽的表格行组件\n const SortableTableRow = ({\n forward,\n selectMode,\n selectedIds,\n toggleSelect,\n getStrategyDisplay,\n formatInAddress,\n formatRemoteAddress,\n handleServiceToggle,\n handleEdit,\n handleDelete,\n handleDiagnose,\n showAddressModal,\n hasMultipleAddresses,\n formatFlow,\n }: any) => {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: forward.id });\n\n const style = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.5 : 1,\n backgroundColor: isDragging ? \"var(--nextui-default-100)\" : undefined,\n };\n\n const strategyDisplay = getStrategyDisplay(forward.strategy);\n\n return (\n \n {selectMode && (\n \n toggleSelect(forward.id)}\n />\n \n )}\n \n \n \n \n \n \n \n \n {forward.name}\n \n \n \n showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n }\n >\n {formatInAddress(forward.inIp, forward.inPort)}\n \n \n \n \n showAddressModal(forward.remoteAddr, null, \"目标地址\")\n }\n >\n {formatRemoteAddress(forward.remoteAddr)}\n \n \n \n \n {strategyDisplay.text}\n \n \n \n \n {formatFlow(getForwardDisplayFlow(forward))}\n \n \n \n
\n handleServiceToggle(forward)}\n />\n
\n
\n \n
\n handleEdit(forward)}\n >\n \n \n \n \n handleDiagnose(forward)}\n >\n \n \n \n \n handleDelete(forward)}\n >\n \n \n \n \n
\n
\n
\n );\n };\n\n const SortableCompactTableRow = ({\n forward,\n selectMode,\n selectedIds,\n toggleSelect,\n getStrategyDisplay,\n formatInAddress,\n formatRemoteAddress,\n handleServiceToggle,\n handleEdit,\n handleDelete,\n handleDiagnose,\n showAddressModal,\n hasMultipleAddresses,\n formatFlow,\n }: any) => {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: forward.id });\n\n const style = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.5 : 1,\n backgroundColor: isDragging ? \"var(--nextui-default-100)\" : undefined,\n };\n\n const strategyDisplay = getStrategyDisplay(forward.strategy);\n\n return (\n \n {selectMode && (\n \n toggleSelect(forward.id)}\n />\n \n )}\n \n \n \n \n \n \n \n \n \n {forward.userName || \"未知用户\"}\n \n \n \n {forward.name}\n \n \n \n \n {forward.tunnelName}\n \n \n \n \n \n showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n }\n >\n {formatInAddress(forward.inIp, forward.inPort)}\n \n \n \n \n showAddressModal(forward.remoteAddr, null, \"目标地址\")\n }\n >\n {formatRemoteAddress(forward.remoteAddr)}\n \n \n \n \n {strategyDisplay.text}\n \n \n \n \n {formatFlow(getForwardDisplayFlow(forward))}\n \n \n \n
\n handleServiceToggle(forward)}\n />\n
\n
\n \n
\n handleEdit(forward)}\n >\n \n \n \n \n handleDiagnose(forward)}\n >\n \n \n \n \n handleDelete(forward)}\n >\n \n \n \n \n
\n
\n
\n );\n };\n\n // 渲染转发卡片\n const renderForwardCard = (forward: Forward, listeners?: any) => {\n const statusDisplay = getStatusDisplay(forward.status);\n const strategyDisplay = getStrategyDisplay(forward.strategy);\n\n return (\n \n \n
\n {selectMode && (\n toggleSelect(forward.id)}\n />\n )}\n
\n

\n {forward.name}\n

\n

\n {normalizeForwardTunnelName(forward.tunnelName)}\n

\n
\n
\n {viewMode === \"direct\" && (\n \n \n \n \n
\n )}\n handleServiceToggle(forward)}\n />\n \n {statusDisplay.text}\n \n
\n \n
\n\n \n
\n {/* 地址信息 */}\n
\n \n showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n }\n >\n
\n
\n \n 入口:\n \n \n {formatInAddress(forward.inIp, forward.inPort)}\n \n
\n {hasMultipleAddresses(forward.inIp) && (\n \n \n \n )}\n
\n \n\n \n showAddressModal(forward.remoteAddr, null, \"目标地址\")\n }\n >\n
\n
\n \n 目标:\n \n \n {formatRemoteAddress(forward.remoteAddr)}\n \n
\n {hasMultipleAddresses(forward.remoteAddr) && (\n \n \n \n )}\n
\n \n
\n\n {/* 统计信息 */}\n
\n \n {strategyDisplay.text}\n \n {(forward.inFlow || 0) + (forward.outFlow || 0) > 0 ? (\n <>\n
\n \n ↑{formatFlow(forward.inFlow || 0)}\n \n
\n \n ↓{formatFlow(forward.outFlow || 0)}\n \n \n ) : (forward.federationShareFlow || 0) > 0 ? (\n \n 共享 {formatFlow(forward.federationShareFlow || 0)}\n \n ) : (\n \n 总流量 {formatFlow(0)}\n \n )}\n
\n
\n\n
\n \n \n \n }\n variant=\"flat\"\n onPress={() => handleEdit(forward)}\n >\n 编辑\n \n \n \n \n }\n variant=\"flat\"\n onPress={() => handleDiagnose(forward)}\n >\n 诊断\n \n \n \n \n \n }\n variant=\"flat\"\n onPress={() => handleDelete(forward)}\n >\n 删除\n \n
\n
\n \n );\n };\n\n if (loading || !groupPreferenceHydrated) {\n return ;\n }\n\n return (\n \n {/* 页面头部 */}\n
\n
\n setIsSearchVisible(false)}\n onOpen={() => setIsSearchVisible(true)}\n />\n
\n
\n
*]:shrink-0\">\n {selectMode ? (\n <>\n \n 已选择 {selectedIds.size} 项\n \n \n 全选\n \n \n 清空\n \n setBatchDeleteModalOpen(true)}\n >\n 删除\n \n handleBatchToggleService(false)}\n >\n 停用\n \n handleBatchToggleService(true)}\n >\n 启用\n \n \n 下发\n \n setBatchChangeTunnelModalOpen(true)}\n >\n 隧道\n \n \n 退出\n \n \n ) : (\n <>\n {/* 筛选按钮 */}\n setIsFilterModalOpen(true)}\n >\n \n \n \n {(filterUserId !== \"all\" || filterTunnelId !== \"all\") && (\n \n )}\n \n {/* 显示模式切换按钮 */}\n \n {viewMode === \"grouped\" ? (\n \n \n \n ) : (\n \n \n \n )}\n \n\n {/* 导入按钮 */}\n \n 导入\n \n\n {/* 导出按钮 */}\n \n 导出\n \n\n \n 批量\n \n\n \n 新增\n \n \n )}\n
\n
\n
\n\n {/* 根据显示模式渲染不同内容 */}\n {compactMode ? (\n viewMode === \"grouped\" ? (\n sortedForwards.length > 0 ? (\n
\n \n \n \n {selectMode && (\n 选择\n )}\n \n 用户\n 名称\n 隧道\n 入口\n 目标\n 策略\n 总流量\n 状态\n 操作\n \n \n {(forward) => (\n \n \n \n )}\n \n \n \n
\n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )\n ) : sortedForwards.length > 0 ? (\n {}}\n >\n \n
\n {sortedForwards.map((forward) =>\n forward && forward.id ? (\n \n ) : null,\n )}\n
\n \n \n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )\n ) : viewMode === \"grouped\" ? (\n sortedForwards.length > 0 ? (\n
\n {groupedForwards.map((group) => {\n const isSelfGroup =\n isAdmin && tokenUserId !== null && group.userId === tokenUserId;\n const groupForwardCount = group.tunnels.reduce(\n (total, tunnel) => total + tunnel.items.length,\n 0,\n );\n\n return (\n \n
\n
\n \n {group.userName}\n \n {isSelfGroup && (\n \n 管理员本人\n \n )}\n
\n \n {groupForwardCount} 条转发\n \n
\n\n
\n \n \n buildTunnelGroupSortableId(\n group.userId,\n tunnel.tunnelKey,\n ),\n )}\n strategy={verticalListSortingStrategy}\n >\n {group.tunnels.map((tunnel) => {\n const tunnelSortableForwardIds = tunnel.items\n .map((item) => item.id)\n .filter((id) => id > 0);\n const collapsed =\n sanitizedCollapsedTunnelGroups[\n buildTunnelGroupCollapseKey(\n group.userId,\n tunnel.tunnelKey,\n )\n ] === true;\n\n return (\n \n toggleTunnelGroupCollapsed(\n group.userId,\n tunnel.tunnelKey,\n )\n }\n >\n \n \n \n {selectMode && (\n \n 选择\n \n )}\n \n \n 名称\n \n \n 入口\n \n \n 目标\n \n \n 策略\n \n \n 总流量\n \n \n 状态\n \n \n 操作\n \n \n \n {(forward) => (\n \n \n \n )}\n \n \n \n \n );\n })}\n \n \n
\n
\n );\n })}\n \n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )\n ) : sortedForwards.length > 0 ? (\n
\n {groupedForwards.map((group) => {\n const isSelfGroup =\n isAdmin && tokenUserId !== null && group.userId === tokenUserId;\n const groupForwardCount = group.tunnels.reduce(\n (total, tunnel) => total + tunnel.items.length,\n 0,\n );\n\n return (\n \n
\n
\n \n {group.userName}\n \n {isSelfGroup && (\n \n 管理员本人\n \n )}\n
\n \n {groupForwardCount} 条转发\n \n
\n\n
\n \n \n buildTunnelGroupSortableId(\n group.userId,\n tunnel.tunnelKey,\n ),\n )}\n strategy={verticalListSortingStrategy}\n >\n {group.tunnels.map((tunnel) => {\n const tunnelSortableForwardIds = tunnel.items\n .map((item) => item.id)\n .filter((id) => id > 0);\n const collapsed =\n sanitizedCollapsedTunnelGroups[\n buildTunnelGroupCollapseKey(\n group.userId,\n tunnel.tunnelKey,\n )\n ] === true;\n\n return (\n \n toggleTunnelGroupCollapsed(\n group.userId,\n tunnel.tunnelKey,\n )\n }\n >\n {}}\n >\n \n
\n {tunnel.items.map((forward) =>\n forward && forward.id ? (\n \n ) : null,\n )}\n
\n \n \n \n );\n })}\n \n \n
\n
\n );\n })}\n \n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )}\n\n {/* 新增/编辑模态框 */}\n \n \n {(onClose) => (\n <>\n \n

\n {isEdit ? \"编辑转发\" : \"新增转发\"}\n

\n

\n {isEdit ? \"修改现有转发配置的信息\" : \"创建新的转发配置\"}\n

\n
\n \n
\n \n setForm((prev) => ({ ...prev, name: e.target.value }))\n }\n />\n\n {\n const selectedKey = Array.from(keys)[0] as\n | string\n | undefined;\n\n setForm((prev) => ({\n ...prev,\n speedId: selectedKey ? Number(selectedKey) : null,\n }));\n }}\n >\n {availableSpeedLimits.map((speedLimit) => (\n \n {speedLimit.name}\n \n ))}\n \n\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n if (selectedKey) {\n handleTunnelChange(selectedKey);\n }\n }}\n >\n {tunnels.map((tunnel) => (\n {tunnel.name}\n ))}\n \n\n {\n const value = e.target.value;\n\n setForm((prev) => ({\n ...prev,\n inPort: value ? parseInt(value) : null,\n }));\n }}\n />\n\n 0\n ? \"选择入口监听IP\"\n : \"当前隧道入口节点暂无可选IP\"\n : \"请先选择隧道\"\n }\n selectedKeys={[form.inIp || \"__default__\"]}\n variant=\"bordered\"\n onSelectionChange={(keys) => {\n const selectedKey = Array.from(keys)[0] as string;\n\n setForm((prev) => ({\n ...prev,\n inIp: selectedKey === \"__default__\" ? \"\" : selectedKey,\n }));\n }}\n >\n 默认入口IP\n {currentTunnelIpOptions.map((ip) => (\n {ip}\n ))}\n \n\n \n setForm((prev) => ({\n ...prev,\n remoteAddr: e.target.value,\n }))\n }\n />\n\n {getAddressCount(form.remoteAddr) > 1 && (\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n setForm((prev) => ({ ...prev, strategy: selectedKey }));\n }}\n >\n 主备模式 - 自上而下\n 轮询模式 - 依次轮换\n 随机模式 - 随机选择\n 哈希模式 - IP哈希\n \n )}\n
\n
\n \n \n \n {isEdit ? \"保存修改\" : \"创建转发\"}\n \n \n \n )}\n
\n \n\n {/* 删除确认模态框 */}\n \n \n {(onClose) => (\n <>\n \n

确认删除

\n
\n \n

\n 确定要删除转发{\" \"}\n \n "{forwardToDelete?.name}"\n {\" \"}\n 吗?\n

\n

\n 此操作无法撤销,删除后该转发将永久消失。\n

\n
\n \n \n \n 确认删除\n \n \n \n )}\n
\n \n\n {/* 地址列表弹窗 */}\n setAddressModalOpen(false)}\n >\n \n {addressModalTitle}\n \n
\n \n
\n\n
\n {addressList.map((item) => (\n \n \n {item.address}\n \n copyAddress(item)}\n >\n 复制\n \n
\n ))}\n \n
\n
\n \n\n {/* 导出数据模态框 */}\n {\n setExportModalOpen(false);\n setSelectedTunnelForExport(null);\n setExportData(\"\");\n }}\n >\n \n \n

导出转发数据

\n

\n 格式:目标地址|转发名称|入口端口\n

\n
\n \n
\n {/* 隧道选择 */}\n
\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n setSelectedTunnelForExport(\n selectedKey ? parseInt(selectedKey) : null,\n );\n }}\n >\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n
\n\n {/* 导出按钮和数据 */}\n {exportData && (\n
\n \n \n \n }\n onPress={executeExport}\n >\n 重新生成\n \n \n \n \n \n }\n onPress={copyExportData}\n >\n 复制\n \n
\n )}\n\n {/* 初始导出按钮 */}\n {!exportData && (\n
\n \n \n \n }\n onPress={executeExport}\n >\n 生成导出数据\n \n
\n )}\n\n {/* 导出数据显示 */}\n {exportData && (\n
\n \n
\n )}\n
\n
\n \n \n \n
\n \n\n {/* 导入数据模态框 */}\n setImportModalOpen(false)}\n >\n \n \n

导入转发数据

\n {importFormat === \"flvx\" ? (\n <>\n

\n 格式:目标地址|转发名称|入口端口,每行一个,入口端口留空将自动分配可用端口\n

\n

\n 目标地址支持单个地址(如:example.com:8080)或多个地址用逗号分隔(如:3.3.3.3:3,4.4.4.4:4)\n

\n \n ) : (\n <>\n

\n ny格式:JSON对象,支持多个目标地址(负载均衡),按所选隧道导入\n

\n

\n 格式:{"dest":["地址:端口"],"listen_port":端口,"name":"名称"}(listen_port可省略,自动分配端口)\n

\n \n )}\n
\n \n
\n {/* 格式选择 */}\n {\n const selectedKey = Array.from(keys)[0] as ImportFormat;\n\n if (selectedKey) {\n setImportFormat(selectedKey);\n setSelectedTunnelForImport(null);\n setImportData(\"\");\n setImportResults([]);\n }\n }}\n >\n \n flvx格式(管道分隔)\n \n \n ny格式(JSON)\n \n \n\n {/* 隧道选择 - 两种格式都需要 */}\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n setSelectedTunnelForImport(\n selectedKey ? parseInt(selectedKey) : null,\n );\n }}\n >\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n\n {/* 输入区域 */}\n setImportData(e.target.value)}\n />\n\n {/* 导入结果 */}\n {importResults.length > 0 && (\n
\n
\n

导入结果

\n \n 成功:{importResults.filter((r) => r.success).length} /\n 总计:{importResults.length}\n \n
\n\n \n {importResults.map((result, index) => (\n \n
\n {result.success ? (\n \n \n \n ) : (\n \n \n \n )}\n
\n
\n \n {result.success ? \"成功\" : \"失败\"}\n \n \n |\n \n \n {result.line}\n \n
\n \n {result.message}\n
\n
\n
\n
\n ))}\n \n \n )}\n \n
\n \n \n \n 开始导入\n \n \n
\n \n\n {/* 诊断结果模态框 */}\n {\n setDiagnosisModalOpen(open);\n if (!open) {\n diagnosisAbortRef.current?.abort();\n diagnosisAbortRef.current = null;\n setDiagnosisLoading(false);\n }\n }}\n >\n \n {(onClose) => (\n <>\n \n

转发诊断结果

\n {currentDiagnosisForward && (\n
\n \n {currentDiagnosisForward.name}\n \n \n 转发服务\n \n
\n )}\n
\n \n {diagnosisResult ? (\n
\n {diagnosisLoading && (\n
\n
\n \n \n 正在诊断 {diagnosisProgress.completed}/\n {diagnosisProgress.total > 0\n ? diagnosisProgress.total\n : \"?\"}\n \n
\n \n 流式更新中\n \n
\n )}\n\n {diagnosisProgress.timedOut && (\n \n )}\n\n {/* 统计摘要 */}\n
\n
\n
\n {diagnosisProgress.total > 0\n ? diagnosisProgress.total\n : diagnosisResult.results.length}\n
\n
\n 总测试数\n
\n
\n
\n
\n {diagnosisProgress.completed > 0 ||\n diagnosisProgress.total > 0\n ? diagnosisProgress.success\n : diagnosisResult.results.filter((r) => r.success)\n .length}\n
\n
\n 成功\n
\n
\n
\n
\n {diagnosisProgress.completed > 0 ||\n diagnosisProgress.total > 0\n ? diagnosisProgress.failed\n : diagnosisResult.results.filter((r) => !r.success)\n .length}\n
\n
\n 失败\n
\n
\n
\n\n {/* 桌面端表格展示 */}\n
\n {(() => {\n // 使用后端返回的 chainType 和 inx 字段进行分组\n const groupedResults = {\n entry: diagnosisResult.results.filter(\n (r) => r.fromChainType === 1,\n ),\n chains: {} as Record<\n number,\n typeof diagnosisResult.results\n >,\n exit: diagnosisResult.results.filter(\n (r) => r.fromChainType === 3,\n ),\n };\n\n // 按 inx 分组链路测试\n diagnosisResult.results.forEach((r) => {\n if (r.fromChainType === 2 && r.fromInx != null) {\n if (!groupedResults.chains[r.fromInx]) {\n groupedResults.chains[r.fromInx] = [];\n }\n groupedResults.chains[r.fromInx].push(r);\n }\n });\n\n const renderTableSection = (\n title: string,\n results: typeof diagnosisResult.results,\n ) => {\n if (results.length === 0) return null;\n\n return (\n \n
\n

\n {title}\n

\n
\n \n \n \n \n \n \n \n \n \n \n \n {results.map((result, index) => {\n const isDiagnosing = Boolean(\n result.diagnosing,\n );\n const isSuccess = result.success === true;\n const quality =\n getForwardDiagnosisQualityDisplay(\n result.averageTime,\n result.packetLoss,\n );\n\n return (\n \n \n \n \n \n \n \n );\n })}\n \n
\n 路径\n \n 状态\n \n 延迟(ms)\n \n 丢包率\n \n 质量\n
\n
\n {isDiagnosing ? (\n \n ) : (\n \n {isSuccess ? \"✓\" : \"✗\"}\n \n )}\n
\n
\n {result.description}\n
\n
\n {result.targetIp}:\n {result.targetPort}\n
\n
\n
\n
\n \n {isDiagnosing\n ? \"诊断中\"\n : isSuccess\n ? \"成功\"\n : \"失败\"}\n \n \n {isSuccess ? (\n \n {result.averageTime?.toFixed(0)}\n \n ) : (\n \n -\n \n )}\n \n {isSuccess ? (\n 0\n ? \"text-warning\"\n : \"text-success\"\n }`}\n >\n {result.packetLoss?.toFixed(1)}%\n \n ) : (\n \n -\n \n )}\n \n {isSuccess && quality ? (\n \n {quality.text}\n \n ) : (\n \n -\n \n )}\n
\n
\n );\n };\n\n return (\n <>\n {/* 入口测试 */}\n {renderTableSection(\n \"🚪 入口测试\",\n groupedResults.entry,\n )}\n\n {/* 链路测试(按跳数排序) */}\n {Object.keys(groupedResults.chains)\n .map(Number)\n .sort((a, b) => a - b)\n .map((hop) =>\n renderTableSection(\n `🔗 转发链 - 第${hop}跳`,\n groupedResults.chains[hop],\n ),\n )}\n\n {/* 出口测试 */}\n {renderTableSection(\n \"🚀 出口测试\",\n groupedResults.exit,\n )}\n \n );\n })()}\n
\n\n {/* 移动端卡片展示 */}\n
\n {(() => {\n // 使用后端返回的 chainType 和 inx 字段进行分组\n const groupedResults = {\n entry: diagnosisResult.results.filter(\n (r) => r.fromChainType === 1,\n ),\n chains: {} as Record<\n number,\n typeof diagnosisResult.results\n >,\n exit: diagnosisResult.results.filter(\n (r) => r.fromChainType === 3,\n ),\n };\n\n // 按 inx 分组链路测试\n diagnosisResult.results.forEach((r) => {\n if (r.fromChainType === 2 && r.fromInx != null) {\n if (!groupedResults.chains[r.fromInx]) {\n groupedResults.chains[r.fromInx] = [];\n }\n groupedResults.chains[r.fromInx].push(r);\n }\n });\n\n const renderCardSection = (\n title: string,\n results: typeof diagnosisResult.results,\n ) => {\n if (results.length === 0) return null;\n\n return (\n
\n
\n

\n {title}\n

\n
\n {results.map((result, index) => {\n const isDiagnosing = Boolean(result.diagnosing);\n const isSuccess = result.success === true;\n const quality =\n getForwardDiagnosisQualityDisplay(\n result.averageTime,\n result.packetLoss,\n );\n\n return (\n \n
\n {isDiagnosing ? (\n \n ) : (\n \n {isSuccess ? \"✓\" : \"✗\"}\n \n )}\n
\n
\n {result.description}\n
\n
\n {result.targetIp}:{result.targetPort}\n
\n
\n \n {isDiagnosing\n ? \"诊断中\"\n : isSuccess\n ? \"成功\"\n : \"失败\"}\n \n
\n\n {isSuccess ? (\n
\n
\n
\n {result.averageTime?.toFixed(0)}\n
\n
\n 延迟(ms)\n
\n
\n
\n 0\n ? \"text-warning\"\n : \"text-success\"\n }`}\n >\n {result.packetLoss?.toFixed(1)}%\n
\n
\n 丢包率\n
\n
\n
\n {quality && (\n <>\n \n {quality.text}\n \n
\n 质量\n
\n \n )}\n
\n
\n ) : (\n
\n \n {isDiagnosing\n ? result.message || \"诊断中...\"\n : result.message || \"连接失败\"}\n
\n
\n )}\n \n );\n })}\n \n );\n };\n\n return (\n <>\n {/* 入口测试 */}\n {renderCardSection(\n \"🚪 入口测试\",\n groupedResults.entry,\n )}\n\n {/* 链路测试(按跳数排序) */}\n {Object.keys(groupedResults.chains)\n .map(Number)\n .sort((a, b) => a - b)\n .map((hop) =>\n renderCardSection(\n `🔗 转发链 - 第${hop}跳`,\n groupedResults.chains[hop],\n ),\n )}\n\n {/* 出口测试 */}\n {renderCardSection(\n \"🚀 出口测试\",\n groupedResults.exit,\n )}\n \n );\n })()}\n \n\n {/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */}\n {diagnosisResult.results.some(\n (r) => r.success === false && !r.diagnosing,\n ) && (\n \n )}\n \n ) : (\n
\n
\n \n \n \n
\n

\n 暂无诊断数据\n

\n
\n )}\n
\n \n \n {currentDiagnosisForward && (\n handleDiagnose(currentDiagnosisForward)}\n >\n 重新诊断\n \n )}\n \n \n )}\n
\n \n\n {/* 批量删除确认模态框 */}\n \n \n {(onClose) => (\n <>\n 确认删除\n \n

\n 确定要删除选中的 {selectedIds.size} 项转发吗?此操作不可撤销。\n

\n
\n \n \n \n 确认删除\n \n \n \n )}\n
\n \n\n {/* 批量换隧道模态框 */}\n \n \n {(onClose) => (\n <>\n 隧道\n \n

\n 将选中的 {selectedIds.size} 项转发迁移到新隧道:\n

\n {\n const selected = Array.from(keys)[0];\n\n setBatchTargetTunnelId(selected ? Number(selected) : null);\n }}\n >\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n
\n \n \n \n 确认换隧道\n \n \n \n )}\n
\n \n\n {/* 筛选模态框 */}\n \n \n {(onClose) => (\n <>\n \n 筛选条件\n \n \n
\n
\n

按用户筛选

\n {\n const key = Array.from(keys)[0] as string;\n\n setFilterUserId(key || \"all\");\n }}\n >\n 全部用户\n {uniqueUsers.map((user) => (\n \n {user.name}\n \n ))}\n \n
\n
\n

按隧道筛选

\n {\n const key = Array.from(keys)[0] as string;\n\n setFilterTunnelId(key || \"all\");\n }}\n >\n 全部隧道\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n
\n
\n
\n \n {\n resetFilterUserId();\n resetFilterTunnelId();\n }}\n >\n 重置\n \n \n \n \n )}\n
\n \n
\n );\n}\n", "additions": 3, "deletions": 3, "status": "modified" }, { "file": "vite-frontend/src/pages/forward/import-format.test.ts", "before": "import test from \"node:test\";\nimport assert from \"node:assert/strict\";\n\nimport {\n convertNyItemToForwardInput,\n parseNyFormatData,\n} from \"./import-format.ts\";\n\ntest(\"parseNyFormatData parses concatenated ny JSON objects\", () => {\n const input =\n '{\"dest\":[\"151.241.129.52:23609\"],\"listen_port\":20224,\"name\":\"灵玥-JP-Lpt【三网通用】\"}{\"dest\":[\"64.81.33.2:24577\"],\"listen_port\":41034,\"name\":\"Yolo-US-Lpt【三网通用】\"}';\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.equal(result[0].error, undefined);\n assert.equal(result[1].error, undefined);\n assert.deepEqual(result[0].parsed?.dest, [\"151.241.129.52:23609\"]);\n assert.equal(result[0].parsed?.listen_port, 20224);\n assert.equal(result[1].parsed?.name, \"Yolo-US-Lpt【三网通用】\");\n});\n\ntest(\"parseNyFormatData parses newline-separated ny JSON objects\", () => {\n const input = [\n '{\"dest\":[\"1.1.1.1:1000\",\"2.2.2.2:2000\"],\"listen_port\":3000,\"name\":\"A\"}',\n '{\"dest\":[\"3.3.3.3:4000\"],\"listen_port\":5000,\"name\":\"B\"}',\n ].join(\"\\n\");\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.equal(result[0].error, undefined);\n assert.equal(result[1].error, undefined);\n assert.deepEqual(result[0].parsed?.dest, [\"1.1.1.1:1000\", \"2.2.2.2:2000\"]);\n assert.equal(result[0].parsed?.listen_port, 3000);\n});\n\ntest(\"parseNyFormatData returns validation errors for invalid fields\", () => {\n const input =\n '{\"dest\":[],\"listen_port\":0,\"name\":\"\"}{\"dest\":[\"bad-address\"],\"listen_port\":80,\"name\":\"ok\"}';\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.match(result[0].error || \"\", /dest数组为空|listen_port|name/);\n assert.match(result[1].error || \"\", /目标地址格式错误/);\n});\n\ntest(\"convertNyItemToForwardInput maps ny fields correctly\", () => {\n const mapped = convertNyItemToForwardInput({\n dest: [\"1.1.1.1:1111\", \"2.2.2.2:2222\"],\n listen_port: 3333,\n name: \" Forward Name \",\n });\n\n assert.deepEqual(mapped, {\n name: \"Forward Name\",\n inPort: 3333,\n remoteAddr: \"1.1.1.1:1111,2.2.2.2:2222\",\n strategy: \"fifo\",\n });\n});\n", "after": "import test from \"node:test\";\nimport assert from \"node:assert/strict\";\n\nimport {\n convertNyItemToForwardInput,\n parseNyFormatData,\n} from \"./import-format.ts\";\n\ntest(\"parseNyFormatData parses concatenated ny JSON objects\", () => {\n const input =\n '{\"dest\":[\"151.241.129.52:23609\"],\"listen_port\":20224,\"name\":\"灵玥-JP-Lpt【三网通用】\"}{\"dest\":[\"64.81.33.2:24577\"],\"listen_port\":41034,\"name\":\"Yolo-US-Lpt【三网通用】\"}';\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.equal(result[0].error, undefined);\n assert.equal(result[1].error, undefined);\n assert.deepEqual(result[0].parsed?.dest, [\"151.241.129.52:23609\"]);\n assert.equal(result[0].parsed?.listen_port, 20224);\n assert.equal(result[1].parsed?.name, \"Yolo-US-Lpt【三网通用】\");\n});\n\ntest(\"parseNyFormatData parses newline-separated ny JSON objects\", () => {\n const input = [\n '{\"dest\":[\"1.1.1.1:1000\",\"2.2.2.2:2000\"],\"listen_port\":3000,\"name\":\"A\"}',\n '{\"dest\":[\"3.3.3.3:4000\"],\"listen_port\":5000,\"name\":\"B\"}',\n ].join(\"\\n\");\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.equal(result[0].error, undefined);\n assert.equal(result[1].error, undefined);\n assert.deepEqual(result[0].parsed?.dest, [\"1.1.1.1:1000\", \"2.2.2.2:2000\"]);\n assert.equal(result[0].parsed?.listen_port, 3000);\n});\n\ntest(\"parseNyFormatData returns validation errors for invalid fields\", () => {\n const input =\n '{\"dest\":[],\"listen_port\":0,\"name\":\"\"}{\"dest\":[\"bad-address\"],\"listen_port\":80,\"name\":\"ok\"}';\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.match(result[0].error || \"\", /dest数组为空|listen_port|name/);\n assert.match(result[1].error || \"\", /目标地址格式错误/);\n});\n\ntest(\"parseNyFormatData allows missing listen_port for auto assignment\", () => {\n const input = '{\"dest\":[\"1.1.1.1:1000\"],\"name\":\"No Port\"}';\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 1);\n assert.equal(result[0].error, undefined);\n assert.equal(result[0].parsed?.listen_port, null);\n});\n\ntest(\"parseNyFormatData supports ny alias fields\", () => {\n const input =\n '{\"dst\":[\"2.2.2.2:2000\"],\"listenPort\":\"3000\",\"forward_name\":\"Alias A\"}\\n{\"target\":\"3.3.3.3:4000,4.4.4.4:5000\",\"port\":6000,\"forwardName\":\"Alias B\"}';\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.equal(result[0].error, undefined);\n assert.equal(result[1].error, undefined);\n assert.deepEqual(result[0].parsed?.dest, [\"2.2.2.2:2000\"]);\n assert.equal(result[0].parsed?.listen_port, 3000);\n assert.equal(result[0].parsed?.name, \"Alias A\");\n assert.deepEqual(result[1].parsed?.dest, [\"3.3.3.3:4000\", \"4.4.4.4:5000\"]);\n assert.equal(result[1].parsed?.listen_port, 6000);\n assert.equal(result[1].parsed?.name, \"Alias B\");\n});\n\ntest(\"convertNyItemToForwardInput maps ny fields correctly\", () => {\n const mapped = convertNyItemToForwardInput({\n dest: [\"1.1.1.1:1111\", \"2.2.2.2:2222\"],\n listen_port: 3333,\n name: \" Forward Name \",\n });\n\n assert.deepEqual(mapped, {\n name: \"Forward Name\",\n inPort: 3333,\n remoteAddr: \"1.1.1.1:1111,2.2.2.2:2222\",\n strategy: \"fifo\",\n });\n});\n\ntest(\"convertNyItemToForwardInput keeps null inPort for auto assignment\", () => {\n const mapped = convertNyItemToForwardInput({\n dest: [\"1.1.1.1:1111\"],\n listen_port: null,\n name: \"No Port\",\n });\n\n assert.deepEqual(mapped, {\n name: \"No Port\",\n inPort: null,\n remoteAddr: \"1.1.1.1:1111\",\n strategy: \"fifo\",\n });\n});\n", "additions": 42, "deletions": 0, "status": "modified" }, { "file": "vite-frontend/src/pages/forward/import-format.ts", "before": "export interface NyImportItem {\n dest: string[];\n listen_port: number;\n name: string;\n}\n\nexport interface ParsedNyImportLine {\n line: string;\n parsed?: NyImportItem;\n error?: string;\n}\n\nconst ADDRESS_PATTERN = /^[^:]+:\\d+$/;\n\nconst isValidListenPort = (value: unknown): value is number => {\n return (\n typeof value === \"number\" &&\n Number.isFinite(value) &&\n value >= 1 &&\n value <= 65535\n );\n};\n\nconst validateNyItem = (line: string, value: unknown): ParsedNyImportLine => {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return { line, error: \"JSON结构错误\" };\n }\n\n const item = value as Record;\n const dest = item.dest;\n const listenPort = item.listen_port;\n const name = item.name;\n\n if (!Array.isArray(dest) || dest.length === 0) {\n return { line, error: \"dest数组为空或格式错误\" };\n }\n\n if (typeof name !== \"string\" || name.trim() === \"\") {\n return { line, error: \"name不能为空\" };\n }\n\n if (!isValidListenPort(listenPort)) {\n return { line, error: \"listen_port必须为1-65535之间的数字\" };\n }\n\n const normalizedDest = dest.map((itemValue) =>\n typeof itemValue === \"string\" ? itemValue.trim() : \"\",\n );\n\n if (normalizedDest.some((itemValue) => itemValue === \"\")) {\n return { line, error: \"dest中包含空地址\" };\n }\n\n const invalid = normalizedDest.find(\n (itemValue) => !ADDRESS_PATTERN.test(itemValue),\n );\n\n if (invalid) {\n return { line, error: `目标地址格式错误: ${invalid}` };\n }\n\n return {\n line,\n parsed: {\n dest: normalizedDest,\n listen_port: listenPort,\n name: name.trim(),\n },\n };\n};\n\nconst splitConcatenatedJsonObjects = (input: string): string[] => {\n const result: string[] = [];\n let depth = 0;\n let start = -1;\n let inString = false;\n let escaping = false;\n\n for (let i = 0; i < input.length; i += 1) {\n const char = input[i];\n\n if (escaping) {\n escaping = false;\n continue;\n }\n\n if (char === \"\\\\\") {\n escaping = true;\n continue;\n }\n\n if (char === '\"') {\n inString = !inString;\n continue;\n }\n\n if (inString) {\n continue;\n }\n\n if (char === \"{\") {\n if (depth === 0) {\n start = i;\n }\n depth += 1;\n continue;\n }\n\n if (char === \"}\") {\n depth -= 1;\n if (depth === 0 && start >= 0) {\n result.push(input.slice(start, i + 1));\n start = -1;\n }\n }\n }\n\n return result;\n};\n\nexport const parseNyFormatData = (input: string): ParsedNyImportLine[] => {\n const trimmed = input.trim();\n\n if (!trimmed) {\n return [];\n }\n\n const parsedResults: ParsedNyImportLine[] = [];\n const objectChunks = splitConcatenatedJsonObjects(trimmed);\n\n if (objectChunks.length > 0) {\n objectChunks.forEach((chunk) => {\n try {\n const parsed = JSON.parse(chunk);\n\n parsedResults.push(validateNyItem(chunk, parsed));\n } catch {\n parsedResults.push({ line: chunk, error: \"JSON解析失败\" });\n }\n });\n\n return parsedResults;\n }\n\n trimmed\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line !== \"\")\n .forEach((line) => {\n try {\n const parsed = JSON.parse(line);\n\n parsedResults.push(validateNyItem(line, parsed));\n } catch {\n parsedResults.push({ line, error: \"JSON解析失败\" });\n }\n });\n\n return parsedResults;\n};\n\nexport const convertNyItemToForwardInput = (item: NyImportItem) => {\n return {\n name: item.name.trim(),\n inPort: item.listen_port,\n remoteAddr: item.dest.join(\",\"),\n strategy: \"fifo\" as const,\n };\n};\n", "after": "export interface NyImportItem {\n dest: string[];\n listen_port: number | null;\n name: string;\n}\n\nexport interface ParsedNyImportLine {\n line: string;\n parsed?: NyImportItem;\n error?: string;\n}\n\nconst ADDRESS_PATTERN = /^[^:]+:\\d+$/;\n\nconst getAliasField = (\n item: Record,\n aliases: string[],\n): unknown => {\n for (const alias of aliases) {\n if (Object.prototype.hasOwnProperty.call(item, alias)) {\n return item[alias];\n }\n }\n\n return undefined;\n};\n\nconst normalizeDestList = (value: unknown): string[] | null => {\n if (Array.isArray(value)) {\n const normalized = value.map((itemValue) =>\n typeof itemValue === \"string\" ? itemValue.trim() : \"\",\n );\n\n if (normalized.some((itemValue) => itemValue === \"\")) {\n return null;\n }\n\n return normalized;\n }\n\n if (typeof value === \"string\") {\n const normalized = value\n .split(\",\")\n .map((itemValue) => itemValue.trim())\n .filter((itemValue) => itemValue !== \"\");\n\n return normalized.length > 0 ? normalized : null;\n }\n\n return null;\n};\n\nconst normalizeListenPort = (value: unknown): number | null | undefined => {\n if (value === undefined || value === null || value === \"\") {\n return null;\n }\n\n if (typeof value === \"number\") {\n return Number.isInteger(value) ? value : undefined;\n }\n\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n\n if (!trimmed) {\n return null;\n }\n\n if (!/^\\d+$/.test(trimmed)) {\n return undefined;\n }\n\n return Number.parseInt(trimmed, 10);\n }\n\n return undefined;\n};\n\nconst isValidListenPort = (value: unknown): value is number => {\n return (\n typeof value === \"number\" &&\n Number.isFinite(value) &&\n value >= 1 &&\n value <= 65535\n );\n};\n\nconst validateNyItem = (line: string, value: unknown): ParsedNyImportLine => {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return { line, error: \"JSON结构错误\" };\n }\n\n const item = value as Record;\n const dest = getAliasField(item, [\"dest\", \"dst\", \"target\", \"targets\"]);\n const listenPortRaw = getAliasField(item, [\n \"listen_port\",\n \"listenPort\",\n \"port\",\n \"in_port\",\n \"inPort\",\n ]);\n const name = getAliasField(item, [\"name\", \"forward_name\", \"forwardName\"]);\n const normalizedDest = normalizeDestList(dest);\n const normalizedListenPort = normalizeListenPort(listenPortRaw);\n\n if (!normalizedDest || normalizedDest.length === 0) {\n return { line, error: \"dest数组为空或格式错误\" };\n }\n\n if (typeof name !== \"string\" || name.trim() === \"\") {\n return { line, error: \"name不能为空\" };\n }\n\n if (normalizedListenPort === undefined) {\n return { line, error: \"listen_port格式错误,应为1-65535之间的数字\" };\n }\n\n if (normalizedListenPort !== null && !isValidListenPort(normalizedListenPort)) {\n return { line, error: \"listen_port必须为1-65535之间的数字\" };\n }\n\n const invalid = normalizedDest.find(\n (itemValue) => !ADDRESS_PATTERN.test(itemValue),\n );\n\n if (invalid) {\n return { line, error: `目标地址格式错误: ${invalid}` };\n }\n\n return {\n line,\n parsed: {\n dest: normalizedDest,\n listen_port: normalizedListenPort,\n name: name.trim(),\n },\n };\n};\n\nconst splitConcatenatedJsonObjects = (input: string): string[] => {\n const result: string[] = [];\n let depth = 0;\n let start = -1;\n let inString = false;\n let escaping = false;\n\n for (let i = 0; i < input.length; i += 1) {\n const char = input[i];\n\n if (escaping) {\n escaping = false;\n continue;\n }\n\n if (char === \"\\\\\") {\n escaping = true;\n continue;\n }\n\n if (char === '\"') {\n inString = !inString;\n continue;\n }\n\n if (inString) {\n continue;\n }\n\n if (char === \"{\") {\n if (depth === 0) {\n start = i;\n }\n depth += 1;\n continue;\n }\n\n if (char === \"}\") {\n depth -= 1;\n if (depth === 0 && start >= 0) {\n result.push(input.slice(start, i + 1));\n start = -1;\n }\n }\n }\n\n return result;\n};\n\nexport const parseNyFormatData = (input: string): ParsedNyImportLine[] => {\n const trimmed = input.trim();\n\n if (!trimmed) {\n return [];\n }\n\n const parsedResults: ParsedNyImportLine[] = [];\n const objectChunks = splitConcatenatedJsonObjects(trimmed);\n\n if (objectChunks.length > 0) {\n objectChunks.forEach((chunk) => {\n try {\n const parsed = JSON.parse(chunk);\n\n parsedResults.push(validateNyItem(chunk, parsed));\n } catch {\n parsedResults.push({ line: chunk, error: \"JSON解析失败\" });\n }\n });\n\n return parsedResults;\n }\n\n trimmed\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line !== \"\")\n .forEach((line) => {\n try {\n const parsed = JSON.parse(line);\n\n parsedResults.push(validateNyItem(line, parsed));\n } catch {\n parsedResults.push({ line, error: \"JSON解析失败\" });\n }\n });\n\n return parsedResults;\n};\n\nexport const convertNyItemToForwardInput = (item: NyImportItem) => {\n return {\n name: item.name.trim(),\n inPort: item.listen_port,\n remoteAddr: item.dest.join(\",\"),\n strategy: \"fifo\" as const,\n };\n};\n", "additions": 83, "deletions": 15, "status": "modified" } ] }, "agent": "build", "model": { "providerID": "api", "modelID": "gpt-5.3-codex" }, "variant": "xhigh", "id": "msg_cb2e428a7001Ejj8LNAqWjo6fE", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "text", "text": "实施", "id": "prt_cb2e428a7002A7EIA8Jy6vdYpo", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e428a7001Ejj8LNAqWjo6fE" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527822994, "completed": 1772527859686 }, "parentID": "msg_cb2e428a7001Ejj8LNAqWjo6fE", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "build", "agent": "build", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 36725, "input": 34825, "output": 1900, "reasoning": 660, "cache": { "read": 0, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e42892002t9E3agYX92b7hi", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "22c03105b8dfd549e276ef662ce4d45a34a0d48b", "id": "prt_cb2e47734001Gp8D0qXVmUjMHL", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e42892002t9E3agYX92b7hi" }, { "type": "reasoning", "text": "**Planning import-format enhancements**", "time": { "start": 1772527843124, "end": 1772527859534 }, "id": "prt_cb2e47734002GwDz73NTh3lVQt", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e42892002t9E3agYX92b7hi" }, { "type": "tool", "callID": "call_imBWDSrcQwLxecvUBTg30U2U", "tool": "apply_patch", "state": { "status": "completed", "input": { "patchText": "*** Begin Patch\n*** Update File: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts\n@@\n export interface NyImportItem {\n dest: string[];\n- listen_port: number;\n+ listen_port: number | null;\n name: string;\n }\n@@\n const ADDRESS_PATTERN = /^[^:]+:\\d+$/;\n+\n+const getAliasField = (\n+ item: Record,\n+ aliases: string[],\n+): unknown => {\n+ for (const alias of aliases) {\n+ if (Object.prototype.hasOwnProperty.call(item, alias)) {\n+ return item[alias];\n+ }\n+ }\n+\n+ return undefined;\n+};\n+\n+const normalizeDestList = (value: unknown): string[] | null => {\n+ if (Array.isArray(value)) {\n+ const normalized = value.map((itemValue) =>\n+ typeof itemValue === \"string\" ? itemValue.trim() : \"\",\n+ );\n+\n+ if (normalized.some((itemValue) => itemValue === \"\")) {\n+ return null;\n+ }\n+\n+ return normalized;\n+ }\n+\n+ if (typeof value === \"string\") {\n+ const normalized = value\n+ .split(\",\")\n+ .map((itemValue) => itemValue.trim())\n+ .filter((itemValue) => itemValue !== \"\");\n+\n+ return normalized.length > 0 ? normalized : null;\n+ }\n+\n+ return null;\n+};\n+\n+const normalizeListenPort = (value: unknown): number | null | undefined => {\n+ if (value === undefined || value === null || value === \"\") {\n+ return null;\n+ }\n+\n+ if (typeof value === \"number\") {\n+ return Number.isInteger(value) ? value : undefined;\n+ }\n+\n+ if (typeof value === \"string\") {\n+ const trimmed = value.trim();\n+\n+ if (!trimmed) {\n+ return null;\n+ }\n+\n+ if (!/^\\d+$/.test(trimmed)) {\n+ return undefined;\n+ }\n+\n+ return Number.parseInt(trimmed, 10);\n+ }\n+\n+ return undefined;\n+};\n@@\n const item = value as Record;\n- const dest = item.dest;\n- const listenPort = item.listen_port;\n- const name = item.name;\n+ const dest = getAliasField(item, [\"dest\", \"dst\", \"target\", \"targets\"]);\n+ const listenPortRaw = getAliasField(item, [\n+ \"listen_port\",\n+ \"listenPort\",\n+ \"port\",\n+ \"in_port\",\n+ \"inPort\",\n+ ]);\n+ const name = getAliasField(item, [\"name\", \"forward_name\", \"forwardName\"]);\n+ const normalizedDest = normalizeDestList(dest);\n+ const normalizedListenPort = normalizeListenPort(listenPortRaw);\n \n- if (!Array.isArray(dest) || dest.length === 0) {\n+ if (!normalizedDest || normalizedDest.length === 0) {\n return { line, error: \"dest数组为空或格式错误\" };\n }\n \n if (typeof name !== \"string\" || name.trim() === \"\") {\n return { line, error: \"name不能为空\" };\n }\n \n- if (!isValidListenPort(listenPort)) {\n+ if (normalizedListenPort === undefined) {\n+ return { line, error: \"listen_port格式错误,应为1-65535之间的数字\" };\n+ }\n+\n+ if (normalizedListenPort !== null && !isValidListenPort(normalizedListenPort)) {\n return { line, error: \"listen_port必须为1-65535之间的数字\" };\n }\n-\n- const normalizedDest = dest.map((itemValue) =>\n- typeof itemValue === \"string\" ? itemValue.trim() : \"\",\n- );\n-\n- if (normalizedDest.some((itemValue) => itemValue === \"\")) {\n- return { line, error: \"dest中包含空地址\" };\n- }\n \n const invalid = normalizedDest.find(\n (itemValue) => !ADDRESS_PATTERN.test(itemValue),\n@@\n dest: normalizedDest,\n- listen_port: listenPort,\n+ listen_port: normalizedListenPort,\n name: name.trim(),\n },\n };\n@@\n export const convertNyItemToForwardInput = (item: NyImportItem) => {\n return {\n name: item.name.trim(),\n- inPort: item.listen_port,\n+ inPort: item.listen_port,\n remoteAddr: item.dest.join(\",\"),\n strategy: \"fifo\" as const,\n };\n };\n*** End Patch" }, "output": "Success. Updated the following files:\nM vite-frontend/src/pages/forward/import-format.ts", "title": "Success. Updated the following files:\nM vite-frontend/src/pages/forward/import-format.ts", "metadata": { "diff": "Index: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts\n===================================================================\n--- /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts\n+++ /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts\n@@ -1,7 +1,7 @@\n export interface NyImportItem {\n dest: string[];\n- listen_port: number;\n+ listen_port: number | null;\n name: string;\n }\n \n export interface ParsedNyImportLine {\n@@ -11,8 +11,72 @@\n }\n \n const ADDRESS_PATTERN = /^[^:]+:\\d+$/;\n \n+const getAliasField = (\n+ item: Record,\n+ aliases: string[],\n+): unknown => {\n+ for (const alias of aliases) {\n+ if (Object.prototype.hasOwnProperty.call(item, alias)) {\n+ return item[alias];\n+ }\n+ }\n+\n+ return undefined;\n+};\n+\n+const normalizeDestList = (value: unknown): string[] | null => {\n+ if (Array.isArray(value)) {\n+ const normalized = value.map((itemValue) =>\n+ typeof itemValue === \"string\" ? itemValue.trim() : \"\",\n+ );\n+\n+ if (normalized.some((itemValue) => itemValue === \"\")) {\n+ return null;\n+ }\n+\n+ return normalized;\n+ }\n+\n+ if (typeof value === \"string\") {\n+ const normalized = value\n+ .split(\",\")\n+ .map((itemValue) => itemValue.trim())\n+ .filter((itemValue) => itemValue !== \"\");\n+\n+ return normalized.length > 0 ? normalized : null;\n+ }\n+\n+ return null;\n+};\n+\n+const normalizeListenPort = (value: unknown): number | null | undefined => {\n+ if (value === undefined || value === null || value === \"\") {\n+ return null;\n+ }\n+\n+ if (typeof value === \"number\") {\n+ return Number.isInteger(value) ? value : undefined;\n+ }\n+\n+ if (typeof value === \"string\") {\n+ const trimmed = value.trim();\n+\n+ if (!trimmed) {\n+ return null;\n+ }\n+\n+ if (!/^\\d+$/.test(trimmed)) {\n+ return undefined;\n+ }\n+\n+ return Number.parseInt(trimmed, 10);\n+ }\n+\n+ return undefined;\n+};\n+\n const isValidListenPort = (value: unknown): value is number => {\n return (\n typeof value === \"number\" &&\n Number.isFinite(value) &&\n@@ -26,30 +90,34 @@\n return { line, error: \"JSON结构错误\" };\n }\n \n const item = value as Record;\n- const dest = item.dest;\n- const listenPort = item.listen_port;\n- const name = item.name;\n+ const dest = getAliasField(item, [\"dest\", \"dst\", \"target\", \"targets\"]);\n+ const listenPortRaw = getAliasField(item, [\n+ \"listen_port\",\n+ \"listenPort\",\n+ \"port\",\n+ \"in_port\",\n+ \"inPort\",\n+ ]);\n+ const name = getAliasField(item, [\"name\", \"forward_name\", \"forwardName\"]);\n+ const normalizedDest = normalizeDestList(dest);\n+ const normalizedListenPort = normalizeListenPort(listenPortRaw);\n \n- if (!Array.isArray(dest) || dest.length === 0) {\n+ if (!normalizedDest || normalizedDest.length === 0) {\n return { line, error: \"dest数组为空或格式错误\" };\n }\n \n if (typeof name !== \"string\" || name.trim() === \"\") {\n return { line, error: \"name不能为空\" };\n }\n \n- if (!isValidListenPort(listenPort)) {\n- return { line, error: \"listen_port必须为1-65535之间的数字\" };\n+ if (normalizedListenPort === undefined) {\n+ return { line, error: \"listen_port格式错误,应为1-65535之间的数字\" };\n }\n \n- const normalizedDest = dest.map((itemValue) =>\n- typeof itemValue === \"string\" ? itemValue.trim() : \"\",\n- );\n-\n- if (normalizedDest.some((itemValue) => itemValue === \"\")) {\n- return { line, error: \"dest中包含空地址\" };\n+ if (normalizedListenPort !== null && !isValidListenPort(normalizedListenPort)) {\n+ return { line, error: \"listen_port必须为1-65535之间的数字\" };\n }\n \n const invalid = normalizedDest.find(\n (itemValue) => !ADDRESS_PATTERN.test(itemValue),\n@@ -62,9 +130,9 @@\n return {\n line,\n parsed: {\n dest: normalizedDest,\n- listen_port: listenPort,\n+ listen_port: normalizedListenPort,\n name: name.trim(),\n },\n };\n };\n\n", "files": [ { "filePath": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts", "relativePath": "vite-frontend/src/pages/forward/import-format.ts", "type": "update", "diff": "Index: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts\n===================================================================\n--- /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts\n+++ /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts\n@@ -1,7 +1,7 @@\n export interface NyImportItem {\n dest: string[];\n- listen_port: number;\n+ listen_port: number | null;\n name: string;\n }\n \n export interface ParsedNyImportLine {\n@@ -11,8 +11,72 @@\n }\n \n const ADDRESS_PATTERN = /^[^:]+:\\d+$/;\n \n+const getAliasField = (\n+ item: Record,\n+ aliases: string[],\n+): unknown => {\n+ for (const alias of aliases) {\n+ if (Object.prototype.hasOwnProperty.call(item, alias)) {\n+ return item[alias];\n+ }\n+ }\n+\n+ return undefined;\n+};\n+\n+const normalizeDestList = (value: unknown): string[] | null => {\n+ if (Array.isArray(value)) {\n+ const normalized = value.map((itemValue) =>\n+ typeof itemValue === \"string\" ? itemValue.trim() : \"\",\n+ );\n+\n+ if (normalized.some((itemValue) => itemValue === \"\")) {\n+ return null;\n+ }\n+\n+ return normalized;\n+ }\n+\n+ if (typeof value === \"string\") {\n+ const normalized = value\n+ .split(\",\")\n+ .map((itemValue) => itemValue.trim())\n+ .filter((itemValue) => itemValue !== \"\");\n+\n+ return normalized.length > 0 ? normalized : null;\n+ }\n+\n+ return null;\n+};\n+\n+const normalizeListenPort = (value: unknown): number | null | undefined => {\n+ if (value === undefined || value === null || value === \"\") {\n+ return null;\n+ }\n+\n+ if (typeof value === \"number\") {\n+ return Number.isInteger(value) ? value : undefined;\n+ }\n+\n+ if (typeof value === \"string\") {\n+ const trimmed = value.trim();\n+\n+ if (!trimmed) {\n+ return null;\n+ }\n+\n+ if (!/^\\d+$/.test(trimmed)) {\n+ return undefined;\n+ }\n+\n+ return Number.parseInt(trimmed, 10);\n+ }\n+\n+ return undefined;\n+};\n+\n const isValidListenPort = (value: unknown): value is number => {\n return (\n typeof value === \"number\" &&\n Number.isFinite(value) &&\n@@ -26,30 +90,34 @@\n return { line, error: \"JSON结构错误\" };\n }\n \n const item = value as Record;\n- const dest = item.dest;\n- const listenPort = item.listen_port;\n- const name = item.name;\n+ const dest = getAliasField(item, [\"dest\", \"dst\", \"target\", \"targets\"]);\n+ const listenPortRaw = getAliasField(item, [\n+ \"listen_port\",\n+ \"listenPort\",\n+ \"port\",\n+ \"in_port\",\n+ \"inPort\",\n+ ]);\n+ const name = getAliasField(item, [\"name\", \"forward_name\", \"forwardName\"]);\n+ const normalizedDest = normalizeDestList(dest);\n+ const normalizedListenPort = normalizeListenPort(listenPortRaw);\n \n- if (!Array.isArray(dest) || dest.length === 0) {\n+ if (!normalizedDest || normalizedDest.length === 0) {\n return { line, error: \"dest数组为空或格式错误\" };\n }\n \n if (typeof name !== \"string\" || name.trim() === \"\") {\n return { line, error: \"name不能为空\" };\n }\n \n- if (!isValidListenPort(listenPort)) {\n- return { line, error: \"listen_port必须为1-65535之间的数字\" };\n+ if (normalizedListenPort === undefined) {\n+ return { line, error: \"listen_port格式错误,应为1-65535之间的数字\" };\n }\n \n- const normalizedDest = dest.map((itemValue) =>\n- typeof itemValue === \"string\" ? itemValue.trim() : \"\",\n- );\n-\n- if (normalizedDest.some((itemValue) => itemValue === \"\")) {\n- return { line, error: \"dest中包含空地址\" };\n+ if (normalizedListenPort !== null && !isValidListenPort(normalizedListenPort)) {\n+ return { line, error: \"listen_port必须为1-65535之间的数字\" };\n }\n \n const invalid = normalizedDest.find(\n (itemValue) => !ADDRESS_PATTERN.test(itemValue),\n@@ -62,9 +130,9 @@\n return {\n line,\n parsed: {\n dest: normalizedDest,\n- listen_port: listenPort,\n+ listen_port: normalizedListenPort,\n name: name.trim(),\n },\n };\n };\n", "before": "export interface NyImportItem {\n dest: string[];\n listen_port: number;\n name: string;\n}\n\nexport interface ParsedNyImportLine {\n line: string;\n parsed?: NyImportItem;\n error?: string;\n}\n\nconst ADDRESS_PATTERN = /^[^:]+:\\d+$/;\n\nconst isValidListenPort = (value: unknown): value is number => {\n return (\n typeof value === \"number\" &&\n Number.isFinite(value) &&\n value >= 1 &&\n value <= 65535\n );\n};\n\nconst validateNyItem = (line: string, value: unknown): ParsedNyImportLine => {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return { line, error: \"JSON结构错误\" };\n }\n\n const item = value as Record;\n const dest = item.dest;\n const listenPort = item.listen_port;\n const name = item.name;\n\n if (!Array.isArray(dest) || dest.length === 0) {\n return { line, error: \"dest数组为空或格式错误\" };\n }\n\n if (typeof name !== \"string\" || name.trim() === \"\") {\n return { line, error: \"name不能为空\" };\n }\n\n if (!isValidListenPort(listenPort)) {\n return { line, error: \"listen_port必须为1-65535之间的数字\" };\n }\n\n const normalizedDest = dest.map((itemValue) =>\n typeof itemValue === \"string\" ? itemValue.trim() : \"\",\n );\n\n if (normalizedDest.some((itemValue) => itemValue === \"\")) {\n return { line, error: \"dest中包含空地址\" };\n }\n\n const invalid = normalizedDest.find(\n (itemValue) => !ADDRESS_PATTERN.test(itemValue),\n );\n\n if (invalid) {\n return { line, error: `目标地址格式错误: ${invalid}` };\n }\n\n return {\n line,\n parsed: {\n dest: normalizedDest,\n listen_port: listenPort,\n name: name.trim(),\n },\n };\n};\n\nconst splitConcatenatedJsonObjects = (input: string): string[] => {\n const result: string[] = [];\n let depth = 0;\n let start = -1;\n let inString = false;\n let escaping = false;\n\n for (let i = 0; i < input.length; i += 1) {\n const char = input[i];\n\n if (escaping) {\n escaping = false;\n continue;\n }\n\n if (char === \"\\\\\") {\n escaping = true;\n continue;\n }\n\n if (char === '\"') {\n inString = !inString;\n continue;\n }\n\n if (inString) {\n continue;\n }\n\n if (char === \"{\") {\n if (depth === 0) {\n start = i;\n }\n depth += 1;\n continue;\n }\n\n if (char === \"}\") {\n depth -= 1;\n if (depth === 0 && start >= 0) {\n result.push(input.slice(start, i + 1));\n start = -1;\n }\n }\n }\n\n return result;\n};\n\nexport const parseNyFormatData = (input: string): ParsedNyImportLine[] => {\n const trimmed = input.trim();\n\n if (!trimmed) {\n return [];\n }\n\n const parsedResults: ParsedNyImportLine[] = [];\n const objectChunks = splitConcatenatedJsonObjects(trimmed);\n\n if (objectChunks.length > 0) {\n objectChunks.forEach((chunk) => {\n try {\n const parsed = JSON.parse(chunk);\n\n parsedResults.push(validateNyItem(chunk, parsed));\n } catch {\n parsedResults.push({ line: chunk, error: \"JSON解析失败\" });\n }\n });\n\n return parsedResults;\n }\n\n trimmed\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line !== \"\")\n .forEach((line) => {\n try {\n const parsed = JSON.parse(line);\n\n parsedResults.push(validateNyItem(line, parsed));\n } catch {\n parsedResults.push({ line, error: \"JSON解析失败\" });\n }\n });\n\n return parsedResults;\n};\n\nexport const convertNyItemToForwardInput = (item: NyImportItem) => {\n return {\n name: item.name.trim(),\n inPort: item.listen_port,\n remoteAddr: item.dest.join(\",\"),\n strategy: \"fifo\" as const,\n };\n};\n", "after": "export interface NyImportItem {\n dest: string[];\n listen_port: number | null;\n name: string;\n}\n\nexport interface ParsedNyImportLine {\n line: string;\n parsed?: NyImportItem;\n error?: string;\n}\n\nconst ADDRESS_PATTERN = /^[^:]+:\\d+$/;\n\nconst getAliasField = (\n item: Record,\n aliases: string[],\n): unknown => {\n for (const alias of aliases) {\n if (Object.prototype.hasOwnProperty.call(item, alias)) {\n return item[alias];\n }\n }\n\n return undefined;\n};\n\nconst normalizeDestList = (value: unknown): string[] | null => {\n if (Array.isArray(value)) {\n const normalized = value.map((itemValue) =>\n typeof itemValue === \"string\" ? itemValue.trim() : \"\",\n );\n\n if (normalized.some((itemValue) => itemValue === \"\")) {\n return null;\n }\n\n return normalized;\n }\n\n if (typeof value === \"string\") {\n const normalized = value\n .split(\",\")\n .map((itemValue) => itemValue.trim())\n .filter((itemValue) => itemValue !== \"\");\n\n return normalized.length > 0 ? normalized : null;\n }\n\n return null;\n};\n\nconst normalizeListenPort = (value: unknown): number | null | undefined => {\n if (value === undefined || value === null || value === \"\") {\n return null;\n }\n\n if (typeof value === \"number\") {\n return Number.isInteger(value) ? value : undefined;\n }\n\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n\n if (!trimmed) {\n return null;\n }\n\n if (!/^\\d+$/.test(trimmed)) {\n return undefined;\n }\n\n return Number.parseInt(trimmed, 10);\n }\n\n return undefined;\n};\n\nconst isValidListenPort = (value: unknown): value is number => {\n return (\n typeof value === \"number\" &&\n Number.isFinite(value) &&\n value >= 1 &&\n value <= 65535\n );\n};\n\nconst validateNyItem = (line: string, value: unknown): ParsedNyImportLine => {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return { line, error: \"JSON结构错误\" };\n }\n\n const item = value as Record;\n const dest = getAliasField(item, [\"dest\", \"dst\", \"target\", \"targets\"]);\n const listenPortRaw = getAliasField(item, [\n \"listen_port\",\n \"listenPort\",\n \"port\",\n \"in_port\",\n \"inPort\",\n ]);\n const name = getAliasField(item, [\"name\", \"forward_name\", \"forwardName\"]);\n const normalizedDest = normalizeDestList(dest);\n const normalizedListenPort = normalizeListenPort(listenPortRaw);\n\n if (!normalizedDest || normalizedDest.length === 0) {\n return { line, error: \"dest数组为空或格式错误\" };\n }\n\n if (typeof name !== \"string\" || name.trim() === \"\") {\n return { line, error: \"name不能为空\" };\n }\n\n if (normalizedListenPort === undefined) {\n return { line, error: \"listen_port格式错误,应为1-65535之间的数字\" };\n }\n\n if (normalizedListenPort !== null && !isValidListenPort(normalizedListenPort)) {\n return { line, error: \"listen_port必须为1-65535之间的数字\" };\n }\n\n const invalid = normalizedDest.find(\n (itemValue) => !ADDRESS_PATTERN.test(itemValue),\n );\n\n if (invalid) {\n return { line, error: `目标地址格式错误: ${invalid}` };\n }\n\n return {\n line,\n parsed: {\n dest: normalizedDest,\n listen_port: normalizedListenPort,\n name: name.trim(),\n },\n };\n};\n\nconst splitConcatenatedJsonObjects = (input: string): string[] => {\n const result: string[] = [];\n let depth = 0;\n let start = -1;\n let inString = false;\n let escaping = false;\n\n for (let i = 0; i < input.length; i += 1) {\n const char = input[i];\n\n if (escaping) {\n escaping = false;\n continue;\n }\n\n if (char === \"\\\\\") {\n escaping = true;\n continue;\n }\n\n if (char === '\"') {\n inString = !inString;\n continue;\n }\n\n if (inString) {\n continue;\n }\n\n if (char === \"{\") {\n if (depth === 0) {\n start = i;\n }\n depth += 1;\n continue;\n }\n\n if (char === \"}\") {\n depth -= 1;\n if (depth === 0 && start >= 0) {\n result.push(input.slice(start, i + 1));\n start = -1;\n }\n }\n }\n\n return result;\n};\n\nexport const parseNyFormatData = (input: string): ParsedNyImportLine[] => {\n const trimmed = input.trim();\n\n if (!trimmed) {\n return [];\n }\n\n const parsedResults: ParsedNyImportLine[] = [];\n const objectChunks = splitConcatenatedJsonObjects(trimmed);\n\n if (objectChunks.length > 0) {\n objectChunks.forEach((chunk) => {\n try {\n const parsed = JSON.parse(chunk);\n\n parsedResults.push(validateNyItem(chunk, parsed));\n } catch {\n parsedResults.push({ line: chunk, error: \"JSON解析失败\" });\n }\n });\n\n return parsedResults;\n }\n\n trimmed\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line !== \"\")\n .forEach((line) => {\n try {\n const parsed = JSON.parse(line);\n\n parsedResults.push(validateNyItem(line, parsed));\n } catch {\n parsedResults.push({ line, error: \"JSON解析失败\" });\n }\n });\n\n return parsedResults;\n};\n\nexport const convertNyItemToForwardInput = (item: NyImportItem) => {\n return {\n name: item.name.trim(),\n inPort: item.listen_port,\n remoteAddr: item.dest.join(\",\"),\n strategy: \"fifo\" as const,\n };\n};\n", "additions": 82, "deletions": 14 } ], "diagnostics": { "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/control_plane.go": [ { "range": { "start": { "line": 39, "character": 25 }, "end": { "line": 39, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 58, "character": 58 }, "end": { "line": 58, "character": 69 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 60, "character": 93 }, "end": { "line": 60, "character": 104 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 62, "character": 22 }, "end": { "line": 62, "character": 33 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 66, "character": 28 }, "end": { "line": 66, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 90, "character": 21 }, "end": { "line": 90, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 101, "character": 3 }, "end": { "line": 101, "character": 12 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop" }, "source": "mapsloop", "message": "Replace m[k]=v loop with maps.Copy" }, { "range": { "start": { "line": 340, "character": 26 }, "end": { "line": 340, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 375, "character": 63 }, "end": { "line": 375, "character": 74 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 383, "character": 73 }, "end": { "line": 383, "character": 84 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 387, "character": 84 }, "end": { "line": 387, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 419, "character": 83 }, "end": { "line": 419, "character": 94 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 423, "character": 94 }, "end": { "line": 423, "character": 105 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 464, "character": 98 }, "end": { "line": 464, "character": 109 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 475, "character": 23 }, "end": { "line": 475, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 520, "character": 26 }, "end": { "line": 520, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 537, "character": 27 }, "end": { "line": 537, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 553, "character": 27 }, "end": { "line": 553, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 573, "character": 28 }, "end": { "line": 573, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 590, "character": 28 }, "end": { "line": 590, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 609, "character": 26 }, "end": { "line": 609, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 624, "character": 26 }, "end": { "line": 624, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 635, "character": 89 }, "end": { "line": 635, "character": 100 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 646, "character": 23 }, "end": { "line": 646, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 690, "character": 25 }, "end": { "line": 690, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 706, "character": 27 }, "end": { "line": 706, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 722, "character": 27 }, "end": { "line": 722, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 742, "character": 28 }, "end": { "line": 742, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 759, "character": 28 }, "end": { "line": 759, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 777, "character": 25 }, "end": { "line": 777, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 790, "character": 25 }, "end": { "line": 790, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 821, "character": 1 }, "end": { "line": 821, "character": 11 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicessort" }, "source": "slicessort", "message": "sort.Slice can be modernized using slices.Sort" }, { "range": { "start": { "line": 891, "character": 84 }, "end": { "line": 891, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 905, "character": 112 }, "end": { "line": 905, "character": 123 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 906, "character": 29 }, "end": { "line": 906, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 920, "character": 135 }, "end": { "line": 920, "character": 146 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 924, "character": 30 }, "end": { "line": 924, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 930, "character": 4 }, "end": { "line": 930, "character": 19 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#minmax" }, "source": "minmax", "message": "if statement can be modernized using max" }, { "range": { "start": { "line": 939, "character": 19 }, "end": { "line": 939, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 964, "character": 5 }, "end": { "line": 964, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#rangeint" }, "source": "rangeint", "message": "for loop can be modernized using range over int" }, { "range": { "start": { "line": 1015, "character": 119 }, "end": { "line": 1015, "character": 130 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1015, "character": 143 }, "end": { "line": 1015, "character": 154 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1016, "character": 20 }, "end": { "line": 1016, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1026, "character": 2 }, "end": { "line": 1026, "character": 9 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop" }, "source": "mapsloop", "message": "Replace m[k]=v loop with maps.Copy" }, { "range": { "start": { "line": 1031, "character": 62 }, "end": { "line": 1031, "character": 73 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1031, "character": 199 }, "end": { "line": 1031, "character": 210 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1044, "character": 60 }, "end": { "line": 1044, "character": 71 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1044, "character": 197 }, "end": { "line": 1044, "character": 208 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1057, "character": 22 }, "end": { "line": 1057, "character": 33 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1094, "character": 64 }, "end": { "line": 1094, "character": 75 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1094, "character": 192 }, "end": { "line": 1094, "character": 203 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1159, "character": 110 }, "end": { "line": 1159, "character": 121 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1166, "character": 72 }, "end": { "line": 1166, "character": 83 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1181, "character": 120 }, "end": { "line": 1181, "character": 131 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1305, "character": 193 }, "end": { "line": 1305, "character": 204 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1307, "character": 31 }, "end": { "line": 1307, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1329, "character": 24 }, "end": { "line": 1329, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1332, "character": 25 }, "end": { "line": 1332, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1335, "character": 26 }, "end": { "line": 1335, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1338, "character": 27 }, "end": { "line": 1338, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1340, "character": 27 }, "end": { "line": 1340, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1348, "character": 34 }, "end": { "line": 1348, "character": 45 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1352, "character": 35 }, "end": { "line": 1352, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1355, "character": 34 }, "end": { "line": 1355, "character": 45 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1358, "character": 36 }, "end": { "line": 1358, "character": 47 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1369, "character": 56 }, "end": { "line": 1369, "character": 67 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1370, "character": 28 }, "end": { "line": 1370, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1372, "character": 35 }, "end": { "line": 1372, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1410, "character": 14 }, "end": { "line": 1410, "character": 25 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1433, "character": 76 }, "end": { "line": 1433, "character": 87 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1438, "character": 25 }, "end": { "line": 1438, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1444, "character": 60 }, "end": { "line": 1444, "character": 71 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1444, "character": 84 }, "end": { "line": 1444, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1445, "character": 19 }, "end": { "line": 1445, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1457, "character": 24 }, "end": { "line": 1457, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/model/model.go": [ { "range": { "start": { "line": 230, "character": 27 }, "end": { "line": 230, "character": 85 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero" }, "source": "omitzero", "message": "Omitempty has no effect on nested struct fields" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/repo/repository_control.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/mutations.go": [ { "range": { "start": { "line": 32, "character": 20 }, "end": { "line": 32, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 86, "character": 20 }, "end": { "line": 86, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 213, "character": 20 }, "end": { "line": 213, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 238, "character": 20 }, "end": { "line": 238, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 286, "character": 20 }, "end": { "line": 286, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 453, "character": 20 }, "end": { "line": 453, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 621, "character": 2 }, "end": { "line": 621, "character": 23 } }, "severity": 3, "code": "default", "codeDescription": { "href": "https://staticcheck.dev/docs/checks/#QF1003" }, "source": "QF1003", "message": "could use tagged switch on row.ChainType" }, { "range": { "start": { "line": 622, "character": 67 }, "end": { "line": 622, "character": 78 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 624, "character": 67 }, "end": { "line": 624, "character": 78 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 625, "character": 68 }, "end": { "line": 625, "character": 79 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 627, "character": 68 }, "end": { "line": 627, "character": 79 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 660, "character": 20 }, "end": { "line": 660, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 843, "character": 46 }, "end": { "line": 843, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 993, "character": 46 }, "end": { "line": 993, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 997, "character": 20 }, "end": { "line": 997, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1022, "character": 18 }, "end": { "line": 1022, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1047, "character": 20 }, "end": { "line": 1047, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1112, "character": 20 }, "end": { "line": 1112, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1202, "character": 20 }, "end": { "line": 1202, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1469, "character": 46 }, "end": { "line": 1469, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1500, "character": 46 }, "end": { "line": 1500, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1531, "character": 46 }, "end": { "line": 1531, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1558, "character": 46 }, "end": { "line": 1558, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1654, "character": 46 }, "end": { "line": 1654, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1658, "character": 20 }, "end": { "line": 1658, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1683, "character": 20 }, "end": { "line": 1683, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1866, "character": 20 }, "end": { "line": 1866, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1885, "character": 20 }, "end": { "line": 1885, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1976, "character": 71 }, "end": { "line": 1976, "character": 82 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2162, "character": 46 }, "end": { "line": 2162, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2210, "character": 20 }, "end": { "line": 2210, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2543, "character": 63 }, "end": { "line": 2543, "character": 74 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2554, "character": 62 }, "end": { "line": 2554, "character": 73 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2575, "character": 153 }, "end": { "line": 2575, "character": 164 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2583, "character": 32 }, "end": { "line": 2583, "character": 43 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2598, "character": 26 }, "end": { "line": 2598, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2602, "character": 38 }, "end": { "line": 2602, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2604, "character": 43 }, "end": { "line": 2604, "character": 54 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2608, "character": 24 }, "end": { "line": 2608, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2615, "character": 19 }, "end": { "line": 2615, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2617, "character": 25 }, "end": { "line": 2617, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2628, "character": 19 }, "end": { "line": 2628, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2630, "character": 23 }, "end": { "line": 2630, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2634, "character": 111 }, "end": { "line": 2634, "character": 122 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2639, "character": 26 }, "end": { "line": 2639, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2643, "character": 38 }, "end": { "line": 2643, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2645, "character": 23 }, "end": { "line": 2645, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2649, "character": 25 }, "end": { "line": 2649, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2654, "character": 33 }, "end": { "line": 2654, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2657, "character": 35 }, "end": { "line": 2657, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2659, "character": 21 }, "end": { "line": 2659, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2786, "character": 84 }, "end": { "line": 2786, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2988, "character": 10 }, "end": { "line": 2988, "character": 23 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq" }, "source": "stringsseq", "message": "Ranging over SplitSeq is more efficient" }, { "range": { "start": { "line": 3073, "character": 50 }, "end": { "line": 3073, "character": 61 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3240, "character": 18 }, "end": { "line": 3240, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3240, "character": 33 }, "end": { "line": 3240, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3244, "character": 20 }, "end": { "line": 3244, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3250, "character": 18 }, "end": { "line": 3250, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3250, "character": 44 }, "end": { "line": 3250, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3255, "character": 26 }, "end": { "line": 3255, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3257, "character": 29 }, "end": { "line": 3257, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3264, "character": 16 }, "end": { "line": 3264, "character": 27 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3283, "character": 13 }, "end": { "line": 3283, "character": 24 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3295, "character": 15 }, "end": { "line": 3295, "character": 26 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3307, "character": 15 }, "end": { "line": 3307, "character": 26 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3319, "character": 23 }, "end": { "line": 3319, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3340, "character": 20 }, "end": { "line": 3340, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3358, "character": 20 }, "end": { "line": 3358, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3375, "character": 1 }, "end": { "line": 3375, "character": 11 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicessort" }, "source": "slicessort", "message": "sort.Slice can be modernized using slices.Sort" }, { "range": { "start": { "line": 3379, "character": 28 }, "end": { "line": 3379, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3386, "character": 27 }, "end": { "line": 3386, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3408, "character": 20 }, "end": { "line": 3408, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/diagnosis_stream.go": [ { "range": { "start": { "line": 15, "character": 6 }, "end": { "line": 15, "character": 17 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 31, "character": 99 }, "end": { "line": 31, "character": 110 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 43, "character": 53 }, "end": { "line": 43, "character": 64 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 73, "character": 136 }, "end": { "line": 73, "character": 147 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 80, "character": 23 }, "end": { "line": 80, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 86, "character": 2 }, "end": { "line": 86, "character": 14 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop" }, "source": "mapsloop", "message": "Replace m[k]=v loop with maps.Copy" }, { "range": { "start": { "line": 93, "character": 44 }, "end": { "line": 93, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 97, "character": 28 }, "end": { "line": 97, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 116, "character": 27 }, "end": { "line": 116, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 146, "character": 28 }, "end": { "line": 146, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 195, "character": 28 }, "end": { "line": 195, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/dual_stack_test.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/repo/repository.go": [ { "range": { "start": { "line": 161, "character": 13 }, "end": { "line": 161, "character": 24 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 356, "character": 77 }, "end": { "line": 356, "character": 88 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 436, "character": 74 }, "end": { "line": 436, "character": 85 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 573, "character": 77 }, "end": { "line": 573, "character": 88 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 583, "character": 77 }, "end": { "line": 583, "character": 88 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 596, "character": 28 }, "end": { "line": 596, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 603, "character": 28 }, "end": { "line": 603, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 611, "character": 29 }, "end": { "line": 611, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 624, "character": 47 }, "end": { "line": 624, "character": 58 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 632, "character": 28 }, "end": { "line": 632, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 634, "character": 35 }, "end": { "line": 634, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 654, "character": 47 }, "end": { "line": 654, "character": 58 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 662, "character": 28 }, "end": { "line": 662, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 664, "character": 35 }, "end": { "line": 664, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 676, "character": 53 }, "end": { "line": 676, "character": 64 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 684, "character": 28 }, "end": { "line": 684, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 686, "character": 21 }, "end": { "line": 686, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 696, "character": 50 }, "end": { "line": 696, "character": 61 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 728, "character": 28 }, "end": { "line": 728, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 734, "character": 21 }, "end": { "line": 734, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 750, "character": 75 }, "end": { "line": 750, "character": 86 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 769, "character": 28 }, "end": { "line": 769, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 771, "character": 35 }, "end": { "line": 771, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 776, "character": 64 }, "end": { "line": 776, "character": 75 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 790, "character": 28 }, "end": { "line": 790, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 792, "character": 35 }, "end": { "line": 792, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 797, "character": 49 }, "end": { "line": 797, "character": 60 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 807, "character": 40 }, "end": { "line": 807, "character": 51 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 811, "character": 31 }, "end": { "line": 811, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 817, "character": 37 }, "end": { "line": 817, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 818, "character": 37 }, "end": { "line": 818, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 819, "character": 39 }, "end": { "line": 819, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 839, "character": 47 }, "end": { "line": 839, "character": 58 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 856, "character": 24 }, "end": { "line": 856, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 873, "character": 54 }, "end": { "line": 873, "character": 65 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 879, "character": 51 }, "end": { "line": 879, "character": 62 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 883, "character": 56 }, "end": { "line": 883, "character": 67 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 897, "character": 33 }, "end": { "line": 897, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 910, "character": 29 }, "end": { "line": 910, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 921, "character": 54 }, "end": { "line": 921, "character": 65 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 929, "character": 29 }, "end": { "line": 929, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 935, "character": 37 }, "end": { "line": 935, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 944, "character": 52 }, "end": { "line": 944, "character": 63 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 952, "character": 29 }, "end": { "line": 952, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 958, "character": 37 }, "end": { "line": 958, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 967, "character": 58 }, "end": { "line": 967, "character": 69 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 990, "character": 29 }, "end": { "line": 990, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 992, "character": 37 }, "end": { "line": 992, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1064, "character": 84 }, "end": { "line": 1064, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1130, "character": 27 }, "end": { "line": 1130, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1146, "character": 83 }, "end": { "line": 1146, "character": 94 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1170, "character": 90 }, "end": { "line": 1170, "character": 101 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1184, "character": 85 }, "end": { "line": 1184, "character": 96 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1404, "character": 85 }, "end": { "line": 1404, "character": 96 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1421, "character": 130 }, "end": { "line": 1421, "character": 141 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1442, "character": 21 }, "end": { "line": 1442, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2382, "character": 23 }, "end": { "line": 2382, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2397, "character": 23 }, "end": { "line": 2397, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2503, "character": 36 }, "end": { "line": 2503, "character": 47 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2591, "character": 27 }, "end": { "line": 2591, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2713, "character": 16 }, "end": { "line": 2713, "character": 29 } }, "severity": 4, "code": "stringscut", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#stringscut" }, "source": "stringscut", "message": "strings.Index can be simplified using strings.Cut" }, { "range": { "start": { "line": 2757, "character": 15 }, "end": { "line": 2757, "character": 28 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq" }, "source": "stringsseq", "message": "Ranging over SplitSeq is more efficient" }, { "range": { "start": { "line": 2803, "character": 38 }, "end": { "line": 2803, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2810, "character": 38 }, "end": { "line": 2810, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2818, "character": 36 }, "end": { "line": 2818, "character": 47 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/repo/repository_mutations.go": [ { "range": { "start": { "line": 78, "character": 21 }, "end": { "line": 78, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 96, "character": 21 }, "end": { "line": 96, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 113, "character": 21 }, "end": { "line": 113, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 156, "character": 21 }, "end": { "line": 156, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 163, "character": 21 }, "end": { "line": 163, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 172, "character": 21 }, "end": { "line": 172, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 198, "character": 116 }, "end": { "line": 198, "character": 127 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 198, "character": 277 }, "end": { "line": 198, "character": 288 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 241, "character": 119 }, "end": { "line": 241, "character": 130 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 247, "character": 21 }, "end": { "line": 247, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 294, "character": 21 }, "end": { "line": 294, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 371, "character": 21 }, "end": { "line": 371, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 380, "character": 21 }, "end": { "line": 380, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 559, "character": 107 }, "end": { "line": 559, "character": 118 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 565, "character": 21 }, "end": { "line": 565, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 601, "character": 70 }, "end": { "line": 601, "character": 81 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 620, "character": 62 }, "end": { "line": 620, "character": 73 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 626, "character": 21 }, "end": { "line": 626, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 648, "character": 122 }, "end": { "line": 648, "character": 133 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 654, "character": 21 }, "end": { "line": 654, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 670, "character": 21 }, "end": { "line": 670, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 679, "character": 21 }, "end": { "line": 679, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 722, "character": 149 }, "end": { "line": 722, "character": 160 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 728, "character": 21 }, "end": { "line": 728, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 786, "character": 23 }, "end": { "line": 786, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 837, "character": 23 }, "end": { "line": 837, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1179, "character": 192 }, "end": { "line": 1179, "character": 203 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1228, "character": 80 }, "end": { "line": 1228, "character": 91 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1237, "character": 137 }, "end": { "line": 1237, "character": 148 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1265, "character": 31 }, "end": { "line": 1265, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1291, "character": 30 }, "end": { "line": 1291, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1334, "character": 27 }, "end": { "line": 1334, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1363, "character": 10 }, "end": { "line": 1363, "character": 23 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq" }, "source": "stringsseq", "message": "Ranging over SplitSeq is more efficient" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/client/federation.go": [ { "range": { "start": { "line": 81, "character": 13 }, "end": { "line": 81, "character": 24 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 88, "character": 20 }, "end": { "line": 88, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 147, "character": 23 }, "end": { "line": 147, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 305, "character": 112 }, "end": { "line": 305, "character": 123 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 332, "character": 18 }, "end": { "line": 332, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/tests/contract/diagnosis_contract_test.go": [ { "range": { "start": { "line": 103, "character": 38 }, "end": { "line": 103, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 107, "character": 39 }, "end": { "line": 107, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 116, "character": 31 }, "end": { "line": 116, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 156, "character": 38 }, "end": { "line": 156, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 160, "character": 39 }, "end": { "line": 160, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 169, "character": 31 }, "end": { "line": 169, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 272, "character": 37 }, "end": { "line": 272, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 276, "character": 38 }, "end": { "line": 276, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 284, "character": 30 }, "end": { "line": 284, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 326, "character": 44 }, "end": { "line": 326, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 330, "character": 21 }, "end": { "line": 330, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 333, "character": 44 }, "end": { "line": 333, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 338, "character": 44 }, "end": { "line": 338, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 343, "character": 44 }, "end": { "line": 343, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 348, "character": 43 }, "end": { "line": 348, "character": 54 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 351, "character": 22 }, "end": { "line": 351, "character": 33 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 431, "character": 37 }, "end": { "line": 431, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 435, "character": 38 }, "end": { "line": 435, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 442, "character": 30 }, "end": { "line": 442, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 531, "character": 38 }, "end": { "line": 531, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 535, "character": 39 }, "end": { "line": 535, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 543, "character": 31 }, "end": { "line": 543, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 584, "character": 24 }, "end": { "line": 584, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 592, "character": 41 }, "end": { "line": 592, "character": 52 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 596, "character": 33 }, "end": { "line": 596, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 601, "character": 32 }, "end": { "line": 601, "character": 43 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/control_plane_test.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/jobs_test.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/reconstruct_tunnel_state_test.go": [] }, "truncated": false }, "time": { "start": 1772527859413, "end": 1772527859464 } }, "id": "prt_cb2e47739001YmFc6mJ6UAYVSB", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e42892002t9E3agYX92b7hi" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "bef151cc89fd456e4195183345800f4cf9181ff7", "cost": 0, "tokens": { "total": 36725, "input": 34825, "output": 1900, "reasoning": 660, "cache": { "read": 0, "write": 0 } }, "id": "prt_cb2e4b750001pVDWb73YE3s7YL", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e42892002t9E3agYX92b7hi" }, { "type": "patch", "hash": "22c03105b8dfd549e276ef662ce4d45a34a0d48b", "files": [ "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.ts" ], "id": "prt_cb2e4b7e0001tOVbpN9gIwMkf1", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e42892002t9E3agYX92b7hi" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527859692, "completed": 1772527872574 }, "parentID": "msg_cb2e428a7001Ejj8LNAqWjo6fE", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "build", "agent": "build", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 36836, "input": 22423, "output": 717, "reasoning": 271, "cache": { "read": 13696, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e4b7ec002JuDND0Cz41ar1x", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "bef151cc89fd456e4195183345800f4cf9181ff7", "id": "prt_cb2e4d7c9001cohuGGFwbxzij5", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e4b7ec002JuDND0Cz41ar1x" }, { "type": "reasoning", "text": "**Updating forward.tsx for null ports**", "time": { "start": 1772527867850, "end": 1772527872489 }, "id": "prt_cb2e4d7ca00107W0juRuRv0Ni4", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e4b7ec002JuDND0Cz41ar1x" }, { "type": "tool", "callID": "call_S2vmMADfkY6BfxSeBf1qLpph", "tool": "apply_patch", "state": { "status": "completed", "input": { "patchText": "*** Begin Patch\n*** Update File: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx\n@@\n

\n- ny格式:JSON对象,支持多个目标地址(负载均衡)\n+ ny格式:JSON对象,支持多个目标地址(负载均衡),按所选隧道导入\n

\n

\n- 格式:{"dest":["地址:端口"],"listen_port":端口,"name":"名称"}\n+ 格式:{"dest":["地址:端口"],"listen_port":端口,"name":"名称"}(listen_port可省略,自动分配端口)\n

\n \n )}\n@@\n importFormat === \"flvx\"\n ? \"请输入要导入的转发数据,格式:目标地址|转发名称|入口端口\"\n- : '请输入ny格式数据,每行一个JSON对象,如:{\"dest\":[\"1.2.3.4:80\"],\"listen_port\":8080,\"name\":\"转发1\"}'\n+ : '请输入ny格式数据,每行一个JSON对象,如:{\"dest\":[\"1.2.3.4:80\"],\"listen_port\":8080,\"name\":\"转发1\"};listen_port可省略自动分配'\n }\n value={importData}\n variant=\"flat\"\n*** End Patch" }, "output": "Success. Updated the following files:\nM vite-frontend/src/pages/forward.tsx", "title": "Success. Updated the following files:\nM vite-frontend/src/pages/forward.tsx", "metadata": { "diff": "Index: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx\n===================================================================\n--- /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx\n+++ /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx\n@@ -4619,12 +4619,12 @@\n \n ) : (\n <>\n

\n- ny格式:JSON对象,支持多个目标地址(负载均衡)\n+ ny格式:JSON对象,支持多个目标地址(负载均衡),按所选隧道导入\n

\n

\n- 格式:{"dest":["地址:端口"],"listen_port":端口,"name":"名称"}\n+ 格式:{"dest":["地址:端口"],"listen_port":端口,"name":"名称"}(listen_port可省略,自动分配端口)\n

\n \n )}\n \n@@ -4695,9 +4695,9 @@\n minRows={8}\n placeholder={\n importFormat === \"flvx\"\n ? \"请输入要导入的转发数据,格式:目标地址|转发名称|入口端口\"\n- : '请输入ny格式数据,每行一个JSON对象,如:{\"dest\":[\"1.2.3.4:80\"],\"listen_port\":8080,\"name\":\"转发1\"}'\n+ : '请输入ny格式数据,每行一个JSON对象,如:{\"dest\":[\"1.2.3.4:80\"],\"listen_port\":8080,\"name\":\"转发1\"};listen_port可省略自动分配'\n }\n value={importData}\n variant=\"flat\"\n onChange={(e) => setImportData(e.target.value)}\n\n", "files": [ { "filePath": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx", "relativePath": "vite-frontend/src/pages/forward.tsx", "type": "update", "diff": "Index: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx\n===================================================================\n--- /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx\n+++ /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx\n@@ -4619,12 +4619,12 @@\n \n ) : (\n <>\n

\n- ny格式:JSON对象,支持多个目标地址(负载均衡)\n+ ny格式:JSON对象,支持多个目标地址(负载均衡),按所选隧道导入\n

\n

\n- 格式:{"dest":["地址:端口"],"listen_port":端口,"name":"名称"}\n+ 格式:{"dest":["地址:端口"],"listen_port":端口,"name":"名称"}(listen_port可省略,自动分配端口)\n

\n \n )}\n \n@@ -4695,9 +4695,9 @@\n minRows={8}\n placeholder={\n importFormat === \"flvx\"\n ? \"请输入要导入的转发数据,格式:目标地址|转发名称|入口端口\"\n- : '请输入ny格式数据,每行一个JSON对象,如:{\"dest\":[\"1.2.3.4:80\"],\"listen_port\":8080,\"name\":\"转发1\"}'\n+ : '请输入ny格式数据,每行一个JSON对象,如:{\"dest\":[\"1.2.3.4:80\"],\"listen_port\":8080,\"name\":\"转发1\"};listen_port可省略自动分配'\n }\n value={importData}\n variant=\"flat\"\n onChange={(e) => setImportData(e.target.value)}\n", "before": "import type { SpeedLimitApiItem } from \"@/api/types\";\n\nimport { useState, useEffect, useMemo, useRef } from \"react\";\nimport toast from \"react-hot-toast\";\nimport {\n DndContext,\n closestCenter,\n KeyboardSensor,\n MouseSensor,\n TouchSensor,\n useSensor,\n useSensors,\n DragEndEvent,\n} from \"@dnd-kit/core\";\nimport {\n arrayMove,\n SortableContext,\n sortableKeyboardCoordinates,\n rectSortingStrategy,\n verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { useSortable } from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\n\nimport { SearchBar } from \"@/components/search-bar\";\nimport { AnimatedPage } from \"@/components/animated-page\";\nimport { Card, CardBody, CardHeader } from \"@/shadcn-bridge/heroui/card\";\nimport { Button } from \"@/shadcn-bridge/heroui/button\";\nimport { Input } from \"@/shadcn-bridge/heroui/input\";\nimport { Textarea } from \"@/shadcn-bridge/heroui/input\";\nimport { Select, SelectItem } from \"@/shadcn-bridge/heroui/select\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableColumn,\n TableHeader,\n TableRow,\n} from \"@/shadcn-bridge/heroui/table\";\nimport {\n Modal,\n ModalContent,\n ModalHeader,\n ModalBody,\n ModalFooter,\n} from \"@/shadcn-bridge/heroui/modal\";\nimport { Chip } from \"@/shadcn-bridge/heroui/chip\";\nimport { Spinner } from \"@/shadcn-bridge/heroui/spinner\";\nimport { Switch } from \"@/shadcn-bridge/heroui/switch\";\nimport { Alert } from \"@/shadcn-bridge/heroui/alert\";\nimport { Checkbox } from \"@/shadcn-bridge/heroui/checkbox\";\nimport {\n createForward,\n getForwardList,\n getSpeedLimitList,\n getPeerShareList,\n getPeerRemoteUsageList,\n updateForward,\n deleteForward,\n forceDeleteForward,\n userTunnel,\n getTunnelList,\n getNodeList,\n pauseForwardService,\n resumeForwardService,\n diagnoseForward,\n updateForwardOrder,\n getConfigByName,\n updateConfig,\n} from \"@/api\";\nimport {\n type ForwardAddressItem,\n formatInAddress,\n formatRemoteAddress,\n hasMultipleAddresses,\n resolveForwardAddressAction,\n} from \"@/pages/forward/address\";\nimport {\n buildForwardDiagnosisFallbackResult,\n getForwardDiagnosisQualityDisplay,\n type ForwardDiagnosisResult,\n} from \"@/pages/forward/diagnosis\";\nimport { diagnoseForwardStream } from \"@/api/diagnosis-stream\";\nimport {\n executeForwardBatchChangeTunnel,\n executeForwardBatchDelete,\n executeForwardBatchRedeploy,\n executeForwardBatchToggleService,\n} from \"@/pages/forward/batch-actions\";\nimport {\n convertNyItemToForwardInput,\n parseNyFormatData,\n} from \"@/pages/forward/import-format\";\nimport { buildForwardOrder, FORWARD_ORDER_KEY } from \"@/pages/forward/order\";\nimport { PageLoadingState } from \"@/components/page-state\";\nimport { useMobileBreakpoint } from \"@/hooks/useMobileBreakpoint\";\nimport { useLocalStorageState } from \"@/hooks/use-local-storage-state\";\nimport { saveOrder } from \"@/utils/order-storage\";\nimport { JwtUtil } from \"@/utils/jwt\";\n\ninterface Forward {\n id: number;\n name: string;\n tunnelId: number;\n tunnelName: string;\n inIp: string;\n inPort: number;\n remoteAddr: string;\n interfaceName?: string;\n strategy: string;\n status: number;\n inFlow: number;\n outFlow: number;\n serviceRunning: boolean;\n federationShareFlow?: number;\n createdTime: string;\n userName?: string;\n userId?: number;\n inx?: number;\n speedId?: number | null;\n}\n\ninterface Tunnel {\n id: number;\n name: string;\n type?: number;\n inIp?: string;\n inNodeId?: Array<{ nodeId: number }>;\n inNodePortSta?: number;\n inNodePortEnd?: number;\n}\n\ninterface Node {\n id: number;\n name?: string;\n serverIp?: string;\n serverIpV4?: string;\n serverIpV6?: string;\n extraIPs?: string;\n}\n\ninterface ForwardForm {\n id?: number;\n userId?: number;\n name: string;\n tunnelId: number | null;\n inPort: number | null;\n inIp: string;\n remoteAddr: string;\n interfaceName?: string;\n strategy: string;\n speedId: number | null;\n}\n\ninterface ForwardUserGroup {\n userId: number;\n userName: string;\n tunnels: ForwardTunnelGroup[];\n}\n\ninterface ForwardTunnelGroup {\n tunnelKey: string;\n tunnelName: string;\n items: Forward[];\n}\n\ntype ForwardGroupOrderMap = Record;\ntype ForwardGroupCollapsedMap = Record;\n\nconst UNKNOWN_FORWARD_USER_NAME = \"未知用户\";\nconst UNCATEGORIZED_FORWARD_TUNNEL_NAME = \"未分类\";\nconst FORWARD_COMPACT_MODE_CONFIG_KEY = \"forward_compact_mode\";\nconst FORWARD_COMPACT_MODE_EVENT = \"forwardCompactModeChanged\";\nconst FORWARD_GROUP_ORDER_CONFIG_KEY = \"forward_group_order_map\";\nconst FORWARD_GROUP_COLLAPSED_CONFIG_KEY = \"forward_group_collapsed_map\";\nconst FORWARD_GROUP_ORDER_LOCAL_STORAGE_PREFIX = \"forward-group-order\";\nconst FORWARD_GROUP_COLLAPSED_LOCAL_STORAGE_PREFIX = \"forward-group-collapsed\";\nconst FORWARD_TUNNEL_GROUP_SORTABLE_PREFIX = \"forward-tunnel-group\";\nconst FORWARD_GROUPED_TABLE_MIN_WIDTH_CLASS = \"min-w-[1320px]\";\nconst FORWARD_GROUPED_TABLE_COLUMN_CLASS = {\n select: \"w-14\",\n drag: \"w-10 pl-4\",\n name: \"w-[200px]\",\n inbound: \"w-[280px]\",\n target: \"w-[280px]\",\n strategy: \"w-[100px]\",\n totalFlow: \"w-[120px]\",\n status: \"w-[100px]\",\n actions: \"w-[144px] text-right\",\n} as const;\n\nconst normalizeForwardUserName = (userName?: string): string => {\n const normalized = (userName || UNKNOWN_FORWARD_USER_NAME).trim();\n\n return normalized || UNKNOWN_FORWARD_USER_NAME;\n};\n\nconst compareForwardUserNameAsc = (a: string, b: string): number => {\n return a.localeCompare(b, \"en\", {\n sensitivity: \"base\",\n numeric: true,\n });\n};\n\nconst normalizeForwardTunnelName = (tunnelName?: string): string => {\n const normalized = (tunnelName || \"\").trim();\n\n return normalized || UNCATEGORIZED_FORWARD_TUNNEL_NAME;\n};\n\nconst buildForwardTunnelGroupKey = (tunnelName?: string): string => {\n const normalized = normalizeForwardTunnelName(tunnelName);\n\n if (normalized === UNCATEGORIZED_FORWARD_TUNNEL_NAME) {\n return \"__uncategorized__\";\n }\n\n return normalized.toLocaleLowerCase();\n};\n\nconst compareForwardTunnelNameAsc = (a: string, b: string): number => {\n return a.localeCompare(b, \"en\", {\n sensitivity: \"base\",\n numeric: true,\n });\n};\n\nconst compareForwardTunnelGroupKeyAsc = (a: string, b: string): number => {\n const aIsUncategorized = a === \"__uncategorized__\";\n const bIsUncategorized = b === \"__uncategorized__\";\n\n if (aIsUncategorized !== bIsUncategorized) {\n return aIsUncategorized ? 1 : -1;\n }\n\n return compareForwardTunnelNameAsc(a, b);\n};\n\nconst buildForwardGroupOrderLocalKey = (tokenUserId: number): string => {\n return `${FORWARD_GROUP_ORDER_LOCAL_STORAGE_PREFIX}:u:${tokenUserId}`;\n};\n\nconst buildForwardGroupCollapsedLocalKey = (tokenUserId: number): string => {\n return `${FORWARD_GROUP_COLLAPSED_LOCAL_STORAGE_PREFIX}:u:${tokenUserId}`;\n};\n\nconst parsePreferenceMap = (\n raw: string | null,\n): Record | null => {\n if (!raw) {\n return null;\n }\n\n try {\n const parsed = JSON.parse(raw);\n\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return null;\n }\n\n return parsed as Record;\n } catch {\n return null;\n }\n};\n\nconst parseGroupOrderMap = (raw: string | null): ForwardGroupOrderMap => {\n const parsed = parsePreferenceMap(raw);\n\n if (!parsed) {\n return {};\n }\n\n const result: ForwardGroupOrderMap = {};\n\n Object.entries(parsed).forEach(([userId, value]) => {\n if (!Array.isArray(value)) {\n return;\n }\n\n const keys = value\n .map((item) => (typeof item === \"string\" ? item.trim() : \"\"))\n .filter((item) => item !== \"\");\n\n if (keys.length > 0) {\n result[userId] = Array.from(new Set(keys));\n }\n });\n\n return result;\n};\n\nconst parseGroupCollapsedMap = (\n raw: string | null,\n): ForwardGroupCollapsedMap => {\n const parsed = parsePreferenceMap(raw);\n\n if (!parsed) {\n return {};\n }\n\n const result: ForwardGroupCollapsedMap = {};\n\n Object.entries(parsed).forEach(([key, value]) => {\n if (typeof value === \"boolean\") {\n result[key] = value;\n }\n });\n\n return result;\n};\n\nconst sanitizeGroupOrderMap = (\n source: ForwardGroupOrderMap,\n availableTunnelKeysByUser: Map>,\n): ForwardGroupOrderMap => {\n const sanitized: ForwardGroupOrderMap = {};\n\n availableTunnelKeysByUser.forEach((availableKeys, userId) => {\n if (availableKeys.size === 0) {\n return;\n }\n\n const orderFromSource = source[userId.toString()] || [];\n const used = new Set();\n const merged: string[] = [];\n\n orderFromSource.forEach((key) => {\n if (!availableKeys.has(key) || used.has(key)) {\n return;\n }\n\n used.add(key);\n merged.push(key);\n });\n\n Array.from(availableKeys)\n .sort(compareForwardTunnelGroupKeyAsc)\n .forEach((key) => {\n if (!used.has(key)) {\n used.add(key);\n merged.push(key);\n }\n });\n\n if (merged.length > 0) {\n sanitized[userId.toString()] = merged;\n }\n });\n\n return sanitized;\n};\n\nconst sanitizeGroupCollapsedMap = (\n source: ForwardGroupCollapsedMap,\n availableCollapseKeys: Set,\n): ForwardGroupCollapsedMap => {\n const sanitized: ForwardGroupCollapsedMap = {};\n\n availableCollapseKeys.forEach((key) => {\n if (source[key] === true) {\n sanitized[key] = true;\n }\n });\n\n return sanitized;\n};\n\nconst buildTunnelGroupCollapseKey = (\n userId: number,\n tunnelKey: string,\n): string => {\n return `${userId}:${tunnelKey}`;\n};\n\nconst buildTunnelGroupSortableId = (\n userId: number,\n tunnelKey: string,\n): string => {\n return `${FORWARD_TUNNEL_GROUP_SORTABLE_PREFIX}:${userId}:${tunnelKey}`;\n};\n\nconst parseTunnelGroupSortableId = (\n value: unknown,\n): { userId: number; tunnelKey: string } | null => {\n if (typeof value !== \"string\") {\n return null;\n }\n\n if (!value.startsWith(`${FORWARD_TUNNEL_GROUP_SORTABLE_PREFIX}:`)) {\n return null;\n }\n\n const parts = value.split(\":\");\n\n if (parts.length < 3) {\n return null;\n }\n\n const userId = Number(parts[1]);\n const tunnelKey = parts.slice(2).join(\":\").trim();\n\n if (!Number.isFinite(userId) || tunnelKey === \"\") {\n return null;\n }\n\n return { userId, tunnelKey };\n};\n\nconst buildAvailableGroupData = (\n forwards: Forward[],\n): {\n availableTunnelKeysByUser: Map>;\n availableCollapseKeys: Set;\n} => {\n const availableTunnelKeysByUser = new Map>();\n const availableCollapseKeys = new Set();\n\n forwards.forEach((forward) => {\n const userId = forward.userId ?? 0;\n const tunnelKey = buildForwardTunnelGroupKey(forward.tunnelName);\n\n let set = availableTunnelKeysByUser.get(userId);\n\n if (!set) {\n set = new Set();\n availableTunnelKeysByUser.set(userId, set);\n }\n\n set.add(tunnelKey);\n availableCollapseKeys.add(buildTunnelGroupCollapseKey(userId, tunnelKey));\n });\n\n return { availableTunnelKeysByUser, availableCollapseKeys };\n};\n\nconst isSameStringArray = (a: string[], b: string[]): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n\n for (let i = 0; i < a.length; i += 1) {\n if (a[i] !== b[i]) {\n return false;\n }\n }\n\n return true;\n};\n\nconst isSameGroupOrderMap = (\n a: ForwardGroupOrderMap,\n b: ForwardGroupOrderMap,\n): boolean => {\n const aKeys = Object.keys(a).sort(compareForwardTunnelNameAsc);\n const bKeys = Object.keys(b).sort(compareForwardTunnelNameAsc);\n\n if (!isSameStringArray(aKeys, bKeys)) {\n return false;\n }\n\n for (const key of aKeys) {\n if (!isSameStringArray(a[key] || [], b[key] || [])) {\n return false;\n }\n }\n\n return true;\n};\n\nconst isSameGroupCollapsedMap = (\n a: ForwardGroupCollapsedMap,\n b: ForwardGroupCollapsedMap,\n): boolean => {\n const aKeys = Object.keys(a).sort(compareForwardTunnelNameAsc);\n const bKeys = Object.keys(b).sort(compareForwardTunnelNameAsc);\n\n if (!isSameStringArray(aKeys, bKeys)) {\n return false;\n }\n\n for (const key of aKeys) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n};\n\nexport default function ForwardPage() {\n const [loading, setLoading] = useState(true);\n const [forwards, setForwards] = useState([]);\n const [tunnels, setTunnels] = useState([]);\n const [allTunnels, setAllTunnels] = useState([]);\n const [nodes, setNodes] = useState([]);\n const [speedLimits, setSpeedLimits] = useState([]);\n const isMobile = useMobileBreakpoint();\n const [searchKeyword, setSearchKeyword] = useLocalStorageState(\n \"forward-search-keyword\",\n \"\",\n );\n const [isSearchVisible, setIsSearchVisible] = useState(false);\n const [compactMode, setCompactMode] = useState(false);\n\n // 显示模式状态 - 从localStorage读取,默认为平铺显示\n const [viewMode, setViewMode] = useState<\"grouped\" | \"direct\">(() => {\n try {\n const savedMode = localStorage.getItem(\"forward-view-mode\");\n\n return (savedMode as \"grouped\" | \"direct\") || \"direct\";\n } catch {\n return \"direct\";\n }\n });\n\n // 筛选状态\n const [filterUserId, setFilterUserId, resetFilterUserId] =\n useLocalStorageState(\"forward-filter-user-id\", \"all\");\n const [filterTunnelId, setFilterTunnelId, resetFilterTunnelId] =\n useLocalStorageState(\"forward-filter-tunnel-id\", \"all\");\n\n // 拖拽排序相关状态\n const [forwardOrder, setForwardOrder] = useState([]);\n\n // 模态框状态\n const [modalOpen, setModalOpen] = useState(false);\n const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);\n const [deleteModalOpen, setDeleteModalOpen] = useState(false);\n const [addressModalOpen, setAddressModalOpen] = useState(false);\n const [diagnosisModalOpen, setDiagnosisModalOpen] = useState(false);\n const [isEdit, setIsEdit] = useState(false);\n const [submitLoading, setSubmitLoading] = useState(false);\n const [deleteLoading, setDeleteLoading] = useState(false);\n const [diagnosisLoading, setDiagnosisLoading] = useState(false);\n const [forwardToDelete, setForwardToDelete] = useState(null);\n const [currentDiagnosisForward, setCurrentDiagnosisForward] =\n useState(null);\n const [diagnosisResult, setDiagnosisResult] =\n useState(null);\n const [diagnosisProgress, setDiagnosisProgress] = useState({\n total: 0,\n completed: 0,\n success: 0,\n failed: 0,\n timedOut: false,\n });\n const diagnosisAbortRef = useRef(null);\n const [addressModalTitle, setAddressModalTitle] = useState(\"\");\n const [addressList, setAddressList] = useState([]);\n\n // 导出相关状态\n const [exportModalOpen, setExportModalOpen] = useState(false);\n const [exportData, setExportData] = useState(\"\");\n const [exportLoading, setExportLoading] = useState(false);\n const [selectedTunnelForExport, setSelectedTunnelForExport] = useState<\n number | null\n >(null);\n\n // 导入相关状态\n type ImportFormat = \"flvx\" | \"ny\";\n const [importModalOpen, setImportModalOpen] = useState(false);\n const [importData, setImportData] = useState(\"\");\n const [importLoading, setImportLoading] = useState(false);\n const [importFormat, setImportFormat] = useState(\"flvx\");\n const [selectedTunnelForImport, setSelectedTunnelForImport] = useState<\n number | null\n >(null);\n const [importResults, setImportResults] = useState<\n Array<{\n line: string;\n success: boolean;\n message: string;\n forwardName?: string;\n }>\n >([]);\n\n // 表单状态\n const [form, setForm] = useState({\n name: \"\",\n tunnelId: null,\n inPort: null,\n inIp: \"\",\n remoteAddr: \"\",\n interfaceName: \"\",\n strategy: \"fifo\",\n speedId: null,\n });\n\n // 表单验证错误\n const [errors, setErrors] = useState<{ [key: string]: string }>({});\n\n // 批量操作相关状态\n const [selectMode, setSelectMode] = useState(false);\n const [selectedIds, setSelectedIds] = useState>(new Set());\n const [batchDeleteModalOpen, setBatchDeleteModalOpen] = useState(false);\n const [batchChangeTunnelModalOpen, setBatchChangeTunnelModalOpen] =\n useState(false);\n const [batchTargetTunnelId, setBatchTargetTunnelId] = useState(\n null,\n );\n const [batchLoading, setBatchLoading] = useState(false);\n const [groupOrderMap, setGroupOrderMap] = useState({});\n const [collapsedTunnelGroups, setCollapsedTunnelGroups] =\n useState({});\n const [groupPreferenceHydrated, setGroupPreferenceHydrated] = useState(false);\n const tokenUserId = JwtUtil.getUserIdFromToken();\n const tokenRoleId = JwtUtil.getRoleIdFromToken();\n const isAdmin = tokenRoleId === 0;\n\n const parseNodeIPs = (node?: Node): string[] => {\n if (!node) {\n return [];\n }\n\n const ips: string[] = [];\n const add = (value?: string) => {\n const trimmed = (value || \"\").trim();\n\n if (trimmed) {\n ips.push(trimmed);\n }\n };\n\n add(node.serverIpV4);\n add(node.serverIpV6);\n add(node.serverIp);\n\n (node.extraIPs || \"\")\n .split(\",\")\n .map((v) => v.trim())\n .filter((v) => v)\n .forEach((v) => ips.push(v));\n\n return Array.from(new Set(ips));\n };\n\n const tunnelInIpOptionMap = useMemo(() => {\n const map = new Map();\n const nodeMap = new Map(nodes.map((n) => [n.id, n]));\n\n for (const tunnel of allTunnels) {\n const collected: string[] = [];\n const entryNodes = tunnel.inNodeId || [];\n\n for (const entry of entryNodes) {\n collected.push(...parseNodeIPs(nodeMap.get(entry.nodeId)));\n }\n\n if (collected.length === 0) {\n (tunnel.inIp || \"\")\n .split(\",\")\n .map((v) => v.trim())\n .filter((v) => v)\n .forEach((v) => collected.push(v));\n }\n\n map.set(tunnel.id, Array.from(new Set(collected)));\n }\n\n return map;\n }, [allTunnels, nodes]);\n\n const currentTunnelIpOptions = useMemo(() => {\n if (!form.tunnelId) {\n return [];\n }\n\n return tunnelInIpOptionMap.get(form.tunnelId) || [];\n }, [form.tunnelId, tunnelInIpOptionMap]);\n\n useEffect(() => {\n return () => {\n diagnosisAbortRef.current?.abort();\n diagnosisAbortRef.current = null;\n };\n }, []);\n\n const persistGroupOrderToLocal = (nextOrderMap: ForwardGroupOrderMap) => {\n if (tokenUserId === null) {\n return;\n }\n\n try {\n localStorage.setItem(\n buildForwardGroupOrderLocalKey(tokenUserId),\n JSON.stringify(nextOrderMap),\n );\n } catch {}\n };\n\n const persistGroupCollapsedToLocal = (\n nextCollapsedMap: ForwardGroupCollapsedMap,\n ) => {\n if (tokenUserId === null) {\n return;\n }\n\n try {\n localStorage.setItem(\n buildForwardGroupCollapsedLocalKey(tokenUserId),\n JSON.stringify(nextCollapsedMap),\n );\n } catch {}\n };\n\n const persistGroupOrderToGlobal = async (\n nextOrderMap: ForwardGroupOrderMap,\n ): Promise => {\n if (!isAdmin || tokenUserId === null) {\n return;\n }\n\n try {\n const currentRes = await getConfigByName(FORWARD_GROUP_ORDER_CONFIG_KEY);\n const globalMap =\n parsePreferenceMap(\n currentRes.code === 0 && typeof currentRes.data?.value === \"string\"\n ? currentRes.data.value\n : null,\n ) || {};\n\n globalMap[tokenUserId.toString()] = nextOrderMap;\n\n const saveRes = await updateConfig(\n FORWARD_GROUP_ORDER_CONFIG_KEY,\n JSON.stringify(globalMap),\n );\n\n if (saveRes.code !== 0) {\n toast.error(saveRes.msg || \"保存分组排序失败\");\n }\n } catch {\n toast.error(\"保存分组排序失败\");\n }\n };\n\n const persistGroupCollapsedToGlobal = async (\n nextCollapsedMap: ForwardGroupCollapsedMap,\n ): Promise => {\n if (!isAdmin || tokenUserId === null) {\n return;\n }\n\n try {\n const currentRes = await getConfigByName(\n FORWARD_GROUP_COLLAPSED_CONFIG_KEY,\n );\n const globalMap =\n parsePreferenceMap(\n currentRes.code === 0 && typeof currentRes.data?.value === \"string\"\n ? currentRes.data.value\n : null,\n ) || {};\n\n globalMap[tokenUserId.toString()] = nextCollapsedMap;\n\n const saveRes = await updateConfig(\n FORWARD_GROUP_COLLAPSED_CONFIG_KEY,\n JSON.stringify(globalMap),\n );\n\n if (saveRes.code !== 0) {\n toast.error(saveRes.msg || \"保存分组折叠状态失败\");\n }\n } catch {\n toast.error(\"保存分组折叠状态失败\");\n }\n };\n\n useEffect(() => {\n let cancelled = false;\n\n const loadGroupPreferences = async () => {\n if (tokenUserId === null) {\n if (!cancelled) {\n setGroupOrderMap({});\n setCollapsedTunnelGroups({});\n setGroupPreferenceHydrated(true);\n }\n\n return;\n }\n\n let localOrderMap: ForwardGroupOrderMap = {};\n let localCollapsedMap: ForwardGroupCollapsedMap = {};\n\n try {\n localOrderMap = parseGroupOrderMap(\n localStorage.getItem(buildForwardGroupOrderLocalKey(tokenUserId)),\n );\n } catch {\n localOrderMap = {};\n }\n\n try {\n localCollapsedMap = parseGroupCollapsedMap(\n localStorage.getItem(buildForwardGroupCollapsedLocalKey(tokenUserId)),\n );\n } catch {\n localCollapsedMap = {};\n }\n\n if (isAdmin) {\n try {\n const [globalOrderRes, globalCollapsedRes] = await Promise.all([\n getConfigByName(FORWARD_GROUP_ORDER_CONFIG_KEY),\n getConfigByName(FORWARD_GROUP_COLLAPSED_CONFIG_KEY),\n ]);\n\n const globalOrderMap = parsePreferenceMap(\n globalOrderRes.code === 0 &&\n typeof globalOrderRes.data?.value === \"string\"\n ? globalOrderRes.data.value\n : null,\n );\n const globalCollapsedMap =\n parsePreferenceMap(\n globalCollapsedRes.code === 0 &&\n typeof globalCollapsedRes.data?.value === \"string\"\n ? globalCollapsedRes.data.value\n : null,\n );\n\n const globalOrderBucket = globalOrderMap?.[tokenUserId.toString()];\n const globalCollapsedBucket =\n globalCollapsedMap?.[tokenUserId.toString()];\n\n if (\n globalOrderBucket &&\n typeof globalOrderBucket === \"object\" &&\n !Array.isArray(globalOrderBucket)\n ) {\n localOrderMap = parseGroupOrderMap(\n JSON.stringify(globalOrderBucket),\n );\n }\n\n if (\n globalCollapsedBucket &&\n typeof globalCollapsedBucket === \"object\" &&\n !Array.isArray(globalCollapsedBucket)\n ) {\n localCollapsedMap = parseGroupCollapsedMap(\n JSON.stringify(globalCollapsedBucket),\n );\n }\n } catch {}\n }\n\n if (cancelled) {\n return;\n }\n\n setGroupOrderMap(localOrderMap);\n setCollapsedTunnelGroups(localCollapsedMap);\n persistGroupOrderToLocal(localOrderMap);\n persistGroupCollapsedToLocal(localCollapsedMap);\n setGroupPreferenceHydrated(true);\n };\n\n setGroupPreferenceHydrated(false);\n loadGroupPreferences();\n\n return () => {\n cancelled = true;\n };\n }, [tokenUserId, isAdmin]);\n\n useEffect(() => {\n const loadForwardCompactMode = async () => {\n try {\n const response = await getConfigByName(FORWARD_COMPACT_MODE_CONFIG_KEY);\n const enabled =\n response.code === 0 &&\n typeof response.data?.value === \"string\" &&\n response.data.value === \"true\";\n\n setCompactMode(enabled);\n } catch {\n setCompactMode(false);\n }\n };\n\n const handleCompactModeChanged = (event: Event) => {\n const customEvent = event as CustomEvent<{ enabled?: boolean }>;\n\n if (typeof customEvent.detail?.enabled === \"boolean\") {\n setCompactMode(customEvent.detail.enabled);\n }\n };\n\n loadForwardCompactMode();\n window.addEventListener(\n FORWARD_COMPACT_MODE_EVENT,\n handleCompactModeChanged,\n );\n\n return () => {\n window.removeEventListener(\n FORWARD_COMPACT_MODE_EVENT,\n handleCompactModeChanged,\n );\n };\n }, []);\n\n const parseShareIdFromTunnelName = (tunnelName: string): number | null => {\n const normalized = (tunnelName || \"\").trim();\n\n if (!normalized.startsWith(\"Share-\")) {\n return null;\n }\n\n const raw = normalized.slice(\"Share-\".length);\n const idx = raw.indexOf(\"-Port-\");\n\n if (idx <= 0) {\n return null;\n }\n\n const shareId = Number(raw.slice(0, idx).trim());\n\n return Number.isFinite(shareId) && shareId > 0 ? shareId : null;\n };\n\n const mergeFederationShareFlow = async (\n forwardsData: Forward[],\n ): Promise => {\n if (forwardsData.length === 0) {\n return forwardsData;\n }\n\n try {\n const [usageRes, localShareRes] = await Promise.all([\n getPeerRemoteUsageList(),\n getPeerShareList(),\n ]);\n\n const flowByShare = new Map();\n const shareIdsByTunnel = new Map>();\n\n if (usageRes.code === 0 && Array.isArray(usageRes.data)) {\n usageRes.data.forEach((item: Record) => {\n const shareId = Number(item.shareId || 0);\n const currentFlow = Number(item.currentFlow || 0);\n\n if (\n Number.isFinite(shareId) &&\n shareId > 0 &&\n Number.isFinite(currentFlow) &&\n currentFlow > 0\n ) {\n const prev = flowByShare.get(shareId) || 0;\n\n flowByShare.set(shareId, Math.max(prev, currentFlow));\n }\n\n if (Number.isFinite(shareId) && shareId > 0) {\n const bindings = Array.isArray(item.bindings)\n ? (item.bindings as Array>)\n : [];\n\n bindings.forEach((binding) => {\n const tunnelId = Number(binding.tunnelId || 0);\n const chainType = Number(binding.chainType || 0);\n\n if (!Number.isFinite(tunnelId) || tunnelId <= 0) {\n return;\n }\n\n if (Number.isFinite(chainType) && chainType !== 1) {\n return;\n }\n\n let shareSet = shareIdsByTunnel.get(tunnelId);\n\n if (!shareSet) {\n shareSet = new Set();\n shareIdsByTunnel.set(tunnelId, shareSet);\n }\n\n shareSet.add(shareId);\n });\n }\n });\n }\n\n if (localShareRes.code === 0 && Array.isArray(localShareRes.data)) {\n localShareRes.data.forEach((item: Record) => {\n const shareId = Number(item.id || 0);\n const currentFlow = Number(item.currentFlow || 0);\n\n if (\n Number.isFinite(shareId) &&\n shareId > 0 &&\n Number.isFinite(currentFlow) &&\n currentFlow > 0\n ) {\n const prev = flowByShare.get(shareId) || 0;\n\n flowByShare.set(shareId, Math.max(prev, currentFlow));\n }\n });\n }\n\n if (flowByShare.size === 0) {\n return forwardsData;\n }\n\n const resolveShareIdForForward = (forward: Forward): number | null => {\n const candidates = new Set();\n const shareIdFromName = parseShareIdFromTunnelName(\n forward.tunnelName || \"\",\n );\n\n if (shareIdFromName) {\n candidates.add(shareIdFromName);\n }\n\n const tunnelId = Number(forward.tunnelId || 0);\n const shareSetByTunnel = shareIdsByTunnel.get(tunnelId);\n\n if (shareSetByTunnel && shareSetByTunnel.size > 0) {\n shareSetByTunnel.forEach((shareId) => {\n if (Number.isFinite(shareId) && shareId > 0) {\n candidates.add(shareId);\n }\n });\n }\n\n if (candidates.size === 0) {\n return null;\n }\n\n let bestShareId: number | null = null;\n let bestFlow = 0;\n\n candidates.forEach((shareId) => {\n const shareFlow = flowByShare.get(shareId) || 0;\n\n if (shareFlow > bestFlow) {\n bestFlow = shareFlow;\n bestShareId = shareId;\n }\n });\n\n return bestShareId;\n };\n\n const resolvedShareByForwardId = new Map();\n\n forwardsData.forEach((forward) => {\n const shareId = resolveShareIdForForward(forward);\n\n if (shareId) {\n resolvedShareByForwardId.set(forward.id, shareId);\n }\n });\n\n const forwardCountByShare = new Map();\n\n forwardsData.forEach((forward) => {\n const shareId = resolvedShareByForwardId.get(forward.id) || null;\n\n if (!shareId || !flowByShare.has(shareId)) {\n return;\n }\n\n forwardCountByShare.set(\n shareId,\n (forwardCountByShare.get(shareId) || 0) + 1,\n );\n });\n\n return forwardsData.map((forward) => {\n const shareId = resolvedShareByForwardId.get(forward.id) || null;\n\n if (!shareId) {\n return { ...forward, federationShareFlow: undefined };\n }\n\n const shareFlow = flowByShare.get(shareId) || 0;\n\n if (shareFlow <= 0) {\n return { ...forward, federationShareFlow: undefined };\n }\n\n const directFlow = (forward.inFlow || 0) + (forward.outFlow || 0);\n\n if (directFlow > 0) {\n return { ...forward, federationShareFlow: undefined };\n }\n\n const count = forwardCountByShare.get(shareId) || 1;\n const estimated = Math.max(1, Math.floor(shareFlow / count));\n\n return { ...forward, federationShareFlow: estimated };\n });\n } catch {\n return forwardsData;\n }\n };\n\n const getForwardDisplayFlow = (forward: Forward): number => {\n const directFlow = (forward.inFlow || 0) + (forward.outFlow || 0);\n\n if (directFlow > 0) {\n return directFlow;\n }\n\n return forward.federationShareFlow || 0;\n };\n\n useEffect(() => {\n loadData();\n }, []);\n\n // 切换显示模式并保存到localStorage\n const handleViewModeChange = () => {\n const newMode = viewMode === \"grouped\" ? \"direct\" : \"grouped\";\n\n setViewMode(newMode);\n try {\n localStorage.setItem(\"forward-view-mode\", newMode);\n } catch {}\n };\n\n // 加载所有数据\n const loadData = async (lod = true) => {\n setLoading(lod);\n try {\n const [forwardsRes, tunnelsRes, speedLimitsRes] = await Promise.all([\n getForwardList(),\n userTunnel(),\n getSpeedLimitList(),\n ]);\n const [allTunnelsRes, nodesRes] = await Promise.allSettled([\n getTunnelList(),\n getNodeList(),\n ]);\n\n if (forwardsRes.code === 0) {\n const forwardsData =\n forwardsRes.data?.map((forward: any) => ({\n ...forward,\n serviceRunning: forward.status === 1,\n })) || [];\n\n const mergedForwards = await mergeFederationShareFlow(forwardsData);\n\n setForwards(mergedForwards);\n\n // 初始化拖拽排序顺序\n const currentUserId = JwtUtil.getUserIdFromToken();\n const { order, fromDatabase } = buildForwardOrder(\n mergedForwards,\n currentUserId,\n );\n\n setForwardOrder(order);\n\n if (fromDatabase) {\n saveOrder(FORWARD_ORDER_KEY, order);\n }\n } else {\n toast.error(forwardsRes.msg || \"获取转发列表失败\");\n }\n\n if (tunnelsRes.code === 0) {\n setTunnels(tunnelsRes.data || []);\n } else {\n }\n\n if (\n allTunnelsRes.status === \"fulfilled\" &&\n allTunnelsRes.value.code === 0\n ) {\n setAllTunnels((allTunnelsRes.value.data || []) as Tunnel[]);\n }\n\n if (nodesRes.status === \"fulfilled\" && nodesRes.value.code === 0) {\n setNodes((nodesRes.value.data || []) as Node[]);\n }\n\n if (speedLimitsRes.code === 0) {\n setSpeedLimits(speedLimitsRes.data || []);\n }\n } catch {\n toast.error(\"加载数据失败\");\n } finally {\n setLoading(false);\n }\n };\n\n // 表单验证\n const noLimitSpeedLimitIds = useMemo(() => {\n return new Set(\n speedLimits\n .filter((speedLimit) => speedLimit.name.trim() === \"不限速\")\n .map((speedLimit) => speedLimit.id),\n );\n }, [speedLimits]);\n\n const availableSpeedLimits = useMemo(() => {\n return speedLimits.filter(\n (speedLimit) => !noLimitSpeedLimitIds.has(speedLimit.id),\n );\n }, [speedLimits, noLimitSpeedLimitIds]);\n\n const normalizeSpeedId = (speedId?: number | null): number | null => {\n if (speedId === null || speedId === undefined) {\n return null;\n }\n\n return noLimitSpeedLimitIds.has(speedId) ? null : speedId;\n };\n\n const selectedSpeedId = normalizeSpeedId(form.speedId);\n\n const validateForm = (): boolean => {\n const newErrors: { [key: string]: string } = {};\n\n if (!form.name.trim()) {\n newErrors.name = \"请输入转发名称\";\n } else if (form.name.length < 2 || form.name.length > 50) {\n newErrors.name = \"转发名称长度应在2-50个字符之间\";\n }\n\n if (!form.tunnelId) {\n newErrors.tunnelId = \"请选择关联隧道\";\n }\n\n // 验证入口端口(可选,如果填写则验证)\n if (form.inPort !== null && form.inPort !== undefined) {\n const port = Number(form.inPort);\n\n if (isNaN(port) || port < 1 || port > 65535) {\n newErrors.inPort = \"端口必须在 1-65535 之间\";\n }\n }\n\n if (!form.remoteAddr.trim()) {\n newErrors.remoteAddr = \"请输入远程地址\";\n } else {\n // 验证地址格式\n const addresses = form.remoteAddr\n .split(\"\\n\")\n .map((addr) => addr.trim())\n .filter((addr) => addr);\n const ipv4Pattern =\n /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):\\d+$/;\n const ipv6FullPattern =\n /^\\[((([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:))|(([0-9a-fA-F]{1,4}:){6}(:[0-9a-fA-F]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9a-fA-F]{1,4}:){5}(((:[0-9a-fA-F]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9a-fA-F]{1,4}:){4}(((:[0-9a-fA-F]{1,4}){1,3})|((:[0-9a-fA-F]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){3}(((:[0-9a-fA-F]{1,4}){1,4})|((:[0-9a-fA-F]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){2}(((:[0-9a-fA-F]{1,4}){1,5})|((:[0-9a-fA-F]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){1}(((:[0-9a-fA-F]{1,4}){1,6})|((:[0-9a-fA-F]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9a-fA-F]{1,4}){1,7})|((:[0-9a-fA-F]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))\\]:\\d+$/;\n const domainPattern =\n /^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*:\\d+$/;\n\n for (let i = 0; i < addresses.length; i++) {\n const addr = addresses[i];\n\n if (\n !ipv4Pattern.test(addr) &&\n !ipv6FullPattern.test(addr) &&\n !domainPattern.test(addr)\n ) {\n newErrors.remoteAddr = `第${i + 1}行地址格式错误`;\n break;\n }\n }\n }\n\n setErrors(newErrors);\n\n return Object.keys(newErrors).length === 0;\n };\n\n // 新增转发\n const handleAdd = () => {\n setIsEdit(false);\n setForm({\n name: \"\",\n tunnelId: null,\n inPort: null,\n inIp: \"\",\n remoteAddr: \"\",\n interfaceName: \"\",\n strategy: \"fifo\",\n speedId: null,\n });\n setErrors({});\n setModalOpen(true);\n };\n\n // 编辑转发\n const handleEdit = (forward: Forward) => {\n setIsEdit(true);\n setForm({\n id: forward.id,\n userId: forward.userId,\n name: forward.name,\n tunnelId: forward.tunnelId,\n inPort: forward.inPort,\n inIp: forward.inIp || \"\",\n remoteAddr: forward.remoteAddr.split(\",\").join(\"\\n\"),\n interfaceName: forward.interfaceName || \"\",\n strategy: forward.strategy || \"fifo\",\n speedId: normalizeSpeedId(forward.speedId),\n });\n setErrors({});\n setModalOpen(true);\n };\n\n // 显示删除确认\n const handleDelete = (forward: Forward) => {\n setForwardToDelete(forward);\n setDeleteModalOpen(true);\n };\n\n // 确认删除转发\n const confirmDelete = async () => {\n if (!forwardToDelete) return;\n\n setDeleteLoading(true);\n try {\n const res = await deleteForward(forwardToDelete.id);\n\n if (res.code === 0) {\n toast.success(\"删除成功\");\n setDeleteModalOpen(false);\n loadData();\n } else {\n // 删除失败,询问是否强制删除\n const confirmed = window.confirm(\n `常规删除失败:${res.msg || \"删除失败\"}\\n\\n是否需要强制删除?\\n\\n⚠️ 注意:强制删除不会去验证节点端是否已经删除对应的转发服务。`,\n );\n\n if (confirmed) {\n const forceRes = await forceDeleteForward(forwardToDelete.id);\n\n if (forceRes.code === 0) {\n toast.success(\"强制删除成功\");\n setDeleteModalOpen(false);\n loadData();\n } else {\n toast.error(forceRes.msg || \"强制删除失败\");\n }\n }\n }\n } catch {\n toast.error(\"删除失败\");\n } finally {\n setDeleteLoading(false);\n }\n };\n\n // 处理隧道选择变化\n const handleTunnelChange = (tunnelId: string) => {\n const nextTunnelId = parseInt(tunnelId);\n const options = tunnelInIpOptionMap.get(nextTunnelId) || [];\n\n setForm((prev) => ({\n ...prev,\n tunnelId: nextTunnelId,\n inIp: options.includes(prev.inIp) ? prev.inIp : \"\",\n }));\n };\n\n // 提交表单\n const handleSubmit = async () => {\n if (!validateForm()) return;\n\n setSubmitLoading(true);\n try {\n const processedRemoteAddr = form.remoteAddr\n .split(\"\\n\")\n .map((addr) => addr.trim())\n .filter((addr) => addr)\n .join(\",\");\n\n const addressCount = processedRemoteAddr.split(\",\").length;\n\n let res: { code: number; msg: string };\n\n if (isEdit) {\n // 更新时确保包含必要字段\n const updateData = {\n id: form.id,\n userId: form.userId,\n name: form.name,\n tunnelId: form.tunnelId,\n inPort: form.inPort,\n inIp: form.inIp || undefined,\n remoteAddr: processedRemoteAddr,\n strategy: addressCount > 1 ? form.strategy : \"fifo\",\n speedId: normalizeSpeedId(form.speedId),\n };\n\n res = await updateForward(updateData);\n } else {\n const createData = {\n name: form.name,\n tunnelId: form.tunnelId,\n inPort: form.inPort,\n inIp: form.inIp || undefined,\n remoteAddr: processedRemoteAddr,\n strategy: addressCount > 1 ? form.strategy : \"fifo\",\n speedId: normalizeSpeedId(form.speedId),\n };\n\n res = await createForward(createData);\n }\n\n if (res.code === 0) {\n toast.success(isEdit ? \"修改成功\" : \"创建成功\");\n setModalOpen(false);\n loadData();\n } else {\n toast.error(res.msg || \"操作失败\");\n }\n } catch {\n toast.error(\"操作失败\");\n } finally {\n setSubmitLoading(false);\n }\n };\n\n // 处理服务开关\n const handleServiceToggle = async (forward: Forward) => {\n if (forward.status !== 1 && forward.status !== 0) {\n toast.error(\"转发状态异常,无法操作\");\n\n return;\n }\n\n const targetState = !forward.serviceRunning;\n\n try {\n // 乐观更新UI\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, serviceRunning: targetState } : f,\n ),\n );\n\n let res: { code: number; msg: string };\n\n if (targetState) {\n res = await resumeForwardService(forward.id);\n } else {\n res = await pauseForwardService(forward.id);\n }\n\n if (res.code === 0) {\n toast.success(targetState ? \"服务已启动\" : \"服务已暂停\");\n // 更新转发状态\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, status: targetState ? 1 : 0 } : f,\n ),\n );\n } else {\n // 操作失败,恢复UI状态\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, serviceRunning: !targetState } : f,\n ),\n );\n toast.error(res.msg || \"操作失败\");\n }\n } catch {\n // 操作失败,恢复UI状态\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, serviceRunning: !targetState } : f,\n ),\n );\n toast.error(\"网络错误,操作失败\");\n }\n };\n\n // 诊断转发\n const handleDiagnose = async (forward: Forward) => {\n diagnosisAbortRef.current?.abort();\n const abortController = new AbortController();\n\n diagnosisAbortRef.current = abortController;\n\n setCurrentDiagnosisForward(forward);\n setDiagnosisModalOpen(true);\n setDiagnosisLoading(true);\n setDiagnosisProgress({\n total: 0,\n completed: 0,\n success: 0,\n failed: 0,\n timedOut: false,\n });\n setDiagnosisResult({\n forwardName: forward.name,\n timestamp: Date.now(),\n results: [],\n });\n\n try {\n let streamErrorMessage = \"\";\n const streamResult = await diagnoseForwardStream(\n forward.id,\n {\n onStart: (payload) => {\n const startForwardName =\n typeof payload.forwardName === \"string\" &&\n payload.forwardName.trim() !== \"\"\n ? payload.forwardName\n : forward.name;\n const startTotal = Number(payload.total);\n const startItems = Array.isArray(payload.items)\n ? (payload.items as ForwardDiagnosisResult[\"results\"])\n : [];\n\n setDiagnosisResult((prev) => ({\n forwardName: startForwardName,\n timestamp: Date.now(),\n results: startItems.length > 0 ? startItems : prev?.results || [],\n }));\n if (Number.isFinite(startTotal) && startTotal >= 0) {\n setDiagnosisProgress((prev) => ({\n ...prev,\n total: startTotal,\n }));\n }\n },\n onItem: ({ result, progress }) => {\n setDiagnosisResult((prev) => {\n const base: ForwardDiagnosisResult = prev || {\n forwardName: forward.name,\n timestamp: Date.now(),\n results: [],\n };\n const nextResults = [...base.results];\n const existingIndex = nextResults.findIndex(\n (item) =>\n item.description === result.description &&\n item.nodeId === result.nodeId &&\n item.targetIp === result.targetIp &&\n item.targetPort === result.targetPort,\n );\n\n if (existingIndex >= 0) {\n nextResults[existingIndex] = {\n ...result,\n diagnosing: false,\n };\n } else {\n nextResults.push({\n ...result,\n diagnosing: false,\n });\n }\n\n return {\n ...base,\n timestamp: Date.now(),\n results: nextResults,\n };\n });\n setDiagnosisProgress({\n total: progress.total,\n completed: progress.completed,\n success: progress.success,\n failed: progress.failed,\n timedOut: Boolean(progress.timedOut),\n });\n },\n onDone: (progress) => {\n setDiagnosisProgress({\n total: progress.total,\n completed: progress.completed,\n success: progress.success,\n failed: progress.failed,\n timedOut: Boolean(progress.timedOut),\n });\n },\n onError: (message) => {\n streamErrorMessage = message;\n },\n },\n abortController.signal,\n );\n\n if (streamResult.fallback) {\n const response = await diagnoseForward(forward.id);\n\n if (response.code === 0) {\n const resultData = response.data as ForwardDiagnosisResult;\n const successCount = resultData.results.filter(\n (r) => r.success,\n ).length;\n const failedCount = resultData.results.length - successCount;\n\n setDiagnosisResult(resultData);\n setDiagnosisProgress({\n total: resultData.results.length,\n completed: resultData.results.length,\n success: successCount,\n failed: failedCount,\n timedOut: false,\n });\n } else {\n toast.error(response.msg || \"诊断失败\");\n setDiagnosisResult(\n buildForwardDiagnosisFallbackResult({\n forwardName: forward.name,\n remoteAddr: forward.remoteAddr,\n description: \"诊断失败\",\n message: response.msg || \"诊断过程中发生错误\",\n }),\n );\n setDiagnosisProgress({\n total: 1,\n completed: 1,\n success: 0,\n failed: 1,\n timedOut: false,\n });\n }\n\n return;\n }\n\n if (streamErrorMessage) {\n toast.error(streamErrorMessage);\n }\n if (streamResult.timedOut) {\n toast.error(\"诊断超时(单条30秒 / 整体2分钟),已返回当前结果\");\n }\n } catch {\n if (abortController.signal.aborted) {\n return;\n }\n toast.error(\"网络错误,请重试\");\n setDiagnosisResult(\n buildForwardDiagnosisFallbackResult({\n forwardName: forward.name,\n remoteAddr: forward.remoteAddr,\n description: \"网络错误\",\n message: \"无法连接到服务器\",\n }),\n );\n setDiagnosisProgress({\n total: 1,\n completed: 1,\n success: 0,\n failed: 1,\n timedOut: false,\n });\n } finally {\n if (diagnosisAbortRef.current === abortController) {\n diagnosisAbortRef.current = null;\n }\n setDiagnosisLoading(false);\n }\n };\n\n // 格式化流量\n const formatFlow = (value: number): string => {\n if (value === 0) return \"0 B\";\n if (value < 1024) return value + \" B\";\n if (value < 1024 * 1024) return (value / 1024).toFixed(2) + \" KB\";\n if (value < 1024 * 1024 * 1024)\n return (value / (1024 * 1024)).toFixed(2) + \" MB\";\n\n return (value / (1024 * 1024 * 1024)).toFixed(2) + \" GB\";\n };\n\n // 显示地址列表弹窗\n const showAddressModal = (\n addressString: string,\n port: number | null,\n title: string,\n ) => {\n const action = resolveForwardAddressAction(addressString, port, title);\n\n if (action.type === \"none\") {\n return;\n }\n\n if (action.type === \"copy\") {\n copyToClipboard(action.text, action.label);\n\n return;\n }\n\n setAddressList(action.items);\n setAddressModalTitle(action.title);\n setAddressModalOpen(true);\n };\n\n // 复制到剪贴板\n const copyToClipboard = async (text: string, label: string = \"内容\") => {\n try {\n await navigator.clipboard.writeText(text);\n toast.success(`已复制${label}`);\n } catch {\n toast.error(\"复制失败\");\n }\n };\n\n // 复制地址\n const copyAddress = async (addressItem: ForwardAddressItem) => {\n try {\n setAddressList((prev) =>\n prev.map((item) =>\n item.id === addressItem.id ? { ...item, copying: true } : item,\n ),\n );\n await copyToClipboard(addressItem.address, \"地址\");\n } catch {\n toast.error(\"复制失败\");\n } finally {\n setAddressList((prev) =>\n prev.map((item) =>\n item.id === addressItem.id ? { ...item, copying: false } : item,\n ),\n );\n }\n };\n\n // 复制所有地址\n const copyAllAddresses = async () => {\n if (addressList.length === 0) return;\n const allAddresses = addressList.map((item) => item.address).join(\"\\n\");\n\n await copyToClipboard(allAddresses, \"所有地址\");\n };\n\n // 导出转发数据\n const handleExport = () => {\n setSelectedTunnelForExport(null);\n setExportData(\"\");\n setExportModalOpen(true);\n };\n\n // 执行导出\n const executeExport = () => {\n if (!selectedTunnelForExport) {\n toast.error(\"请选择要导出的隧道\");\n\n return;\n }\n\n setExportLoading(true);\n\n try {\n // 获取要导出的转发列表\n const forwardsToExport = sortedForwards.filter(\n (forward) => forward.tunnelId === selectedTunnelForExport,\n );\n\n if (forwardsToExport.length === 0) {\n toast.error(\"所选隧道没有转发数据\");\n setExportLoading(false);\n\n return;\n }\n\n // 格式化导出数据:remoteAddr|name|inPort\n const exportLines = forwardsToExport.map((forward) => {\n return `${forward.remoteAddr}|${forward.name}|${forward.inPort}`;\n });\n\n const exportText = exportLines.join(\"\\n\");\n\n setExportData(exportText);\n } catch {\n toast.error(\"导出失败\");\n } finally {\n setExportLoading(false);\n }\n };\n\n // 复制导出数据\n const copyExportData = async () => {\n await copyToClipboard(exportData, \"转发数据\");\n };\n\n // 导入转发数据\n const handleImport = () => {\n setImportData(\"\");\n setImportResults([]);\n setSelectedTunnelForImport(null);\n setImportModalOpen(true);\n };\n\n // 执行导入\n const executeImport = async () => {\n if (!importData.trim()) {\n toast.error(\"请输入要导入的数据\");\n\n return;\n }\n\n if (!selectedTunnelForImport) {\n toast.error(\"请选择要导入的隧道\");\n\n return;\n }\n\n setImportLoading(true);\n setImportResults([]);\n\n try {\n if (importFormat === \"ny\") {\n const parsedItems = parseNyFormatData(importData);\n\n if (parsedItems.length === 0) {\n toast.error(\"未解析到有效的ny格式数据\");\n\n setImportLoading(false);\n\n return;\n }\n\n for (const item of parsedItems) {\n if (item.error) {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: item.error || \"解析失败\",\n },\n ...prev,\n ]);\n\n continue;\n }\n\n if (!item.parsed) {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: \"解析失败\",\n },\n ...prev,\n ]);\n\n continue;\n }\n\n const parsedNyItem = item.parsed;\n const nyForwardInput = convertNyItemToForwardInput(parsedNyItem);\n\n try {\n const response = await createForward({\n name: nyForwardInput.name,\n tunnelId: selectedTunnelForImport,\n inPort: nyForwardInput.inPort,\n remoteAddr: nyForwardInput.remoteAddr,\n strategy: nyForwardInput.strategy,\n });\n\n if (response.code === 0) {\n setImportResults((prev) => [\n {\n line: item.line,\n success: true,\n message: `创建成功 (${parsedNyItem.dest.length}个目标)`,\n forwardName: nyForwardInput.name,\n },\n ...prev,\n ]);\n } else {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: response.msg || \"创建失败\",\n },\n ...prev,\n ]);\n }\n } catch {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: \"网络错误,创建失败\",\n },\n ...prev,\n ]);\n }\n }\n } else {\n const lines = importData\n .trim()\n .split(\"\\n\")\n .filter((line) => line.trim());\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n const parts = line.split(\"|\");\n\n if (parts.length < 2) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"格式错误:需要至少包含目标地址和转发名称\",\n },\n ...prev,\n ]);\n continue;\n }\n\n const [remoteAddr, name, inPort] = parts;\n\n if (!remoteAddr.trim() || !name.trim()) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"目标地址和转发名称不能为空\",\n },\n ...prev,\n ]);\n continue;\n }\n\n const addresses = remoteAddr.trim().split(\",\");\n const addressPattern = /^[^:]+:\\d+$/;\n const isValidFormat = addresses.every((addr) =>\n addressPattern.test(addr.trim()),\n );\n\n if (!isValidFormat) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message:\n \"目标地址格式错误,应为 地址:端口 格式,多个地址用逗号分隔\",\n },\n ...prev,\n ]);\n continue;\n }\n\n try {\n let portNumber: number | null = null;\n\n if (inPort && inPort.trim()) {\n const port = parseInt(inPort.trim());\n\n if (isNaN(port) || port < 1 || port > 65535) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"入口端口格式错误,应为1-65535之间的数字\",\n },\n ...prev,\n ]);\n continue;\n }\n portNumber = port;\n }\n\n const response = await createForward({\n name: name.trim(),\n tunnelId: selectedTunnelForImport,\n inPort: portNumber,\n remoteAddr: remoteAddr.trim(),\n strategy: \"fifo\",\n });\n\n if (response.code === 0) {\n setImportResults((prev) => [\n {\n line,\n success: true,\n message: \"创建成功\",\n forwardName: name.trim(),\n },\n ...prev,\n ]);\n } else {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: response.msg || \"创建失败\",\n },\n ...prev,\n ]);\n }\n } catch {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"网络错误,创建失败\",\n },\n ...prev,\n ]);\n }\n }\n }\n\n toast.success(\"导入执行完成\");\n\n await loadData(false);\n } catch {\n toast.error(\"导入过程中发生错误\");\n } finally {\n setImportLoading(false);\n }\n };\n\n // 获取状态显示\n const getStatusDisplay = (status: number) => {\n switch (status) {\n case 1:\n return { color: \"success\", text: \"正常\" };\n case 0:\n return { color: \"warning\", text: \"暂停\" };\n case -1:\n return { color: \"danger\", text: \"异常\" };\n default:\n return { color: \"default\", text: \"未知\" };\n }\n };\n\n // 获取策略显示\n const getStrategyDisplay = (strategy: string) => {\n switch (strategy) {\n case \"fifo\":\n return { color: \"primary\", text: \"主备\" };\n case \"round\":\n return { color: \"success\", text: \"轮询\" };\n case \"rand\":\n return { color: \"warning\", text: \"随机\" };\n default:\n return { color: \"default\", text: \"未知\" };\n }\n };\n\n // 获取地址数量\n const getAddressCount = (addressString: string): number => {\n if (!addressString) return 0;\n const addresses = addressString\n .split(\"\\n\")\n .map((addr) => addr.trim())\n .filter((addr) => addr);\n\n return addresses.length;\n };\n\n // 处理拖拽结束\n const handleDragEnd = async (event: DragEndEvent) => {\n const { active, over } = event;\n\n if (!active || !over || active.id === over.id) return;\n\n const activeGroup = parseTunnelGroupSortableId(active.id);\n const overGroup = parseTunnelGroupSortableId(over.id);\n\n if (activeGroup && overGroup) {\n if (compactMode || !groupPreferenceHydrated) {\n return;\n }\n\n if (activeGroup.userId !== overGroup.userId) {\n return;\n }\n\n const userIdKey = activeGroup.userId.toString();\n const currentOrder = groupOrderMap[userIdKey] || [];\n const oldIndex = currentOrder.indexOf(activeGroup.tunnelKey);\n const newIndex = currentOrder.indexOf(overGroup.tunnelKey);\n\n if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) {\n return;\n }\n\n const moved = arrayMove(currentOrder, oldIndex, newIndex);\n const nextOrderMap: ForwardGroupOrderMap = {\n ...groupOrderMap,\n [userIdKey]: moved,\n };\n\n setGroupOrderMap(nextOrderMap);\n persistGroupOrderToLocal(nextOrderMap);\n void persistGroupOrderToGlobal(nextOrderMap);\n\n return;\n }\n\n // 确保 forwardOrder 存在且有效\n if (!forwardOrder || forwardOrder.length === 0) return;\n\n const activeId = Number(active.id);\n const overId = Number(over.id);\n\n // 检查 ID 是否有效\n if (isNaN(activeId) || isNaN(overId)) return;\n\n const activeForward = forwards.find((forward) => forward.id === activeId);\n const overForward = forwards.find((forward) => forward.id === overId);\n const activeUserId = activeForward?.userId ?? 0;\n const overUserId = overForward?.userId ?? 0;\n const activeTunnelGroupKey = buildForwardTunnelGroupKey(\n activeForward?.tunnelName,\n );\n const overTunnelGroupKey = buildForwardTunnelGroupKey(\n overForward?.tunnelName,\n );\n\n // 非精简模式仅允许在同一用户+隧道分组内拖拽,避免混排\n if (!compactMode) {\n if (\n activeUserId !== overUserId ||\n activeTunnelGroupKey !== overTunnelGroupKey\n ) {\n return;\n }\n }\n\n const oldIndex = forwardOrder.indexOf(activeId);\n const newIndex = forwardOrder.indexOf(overId);\n\n if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {\n const newOrder = arrayMove(forwardOrder, oldIndex, newIndex);\n\n setForwardOrder(newOrder);\n\n saveOrder(FORWARD_ORDER_KEY, newOrder);\n\n // 持久化到数据库\n try {\n const forwardsToUpdate = newOrder.map((id, index) => ({\n id,\n inx: index,\n }));\n\n const response = await updateForwardOrder({\n forwards: forwardsToUpdate,\n });\n\n if (response.code === 0) {\n // 更新本地数据中的 inx 字段\n setForwards((prev) =>\n prev.map((forward) => {\n const updatedForward = forwardsToUpdate.find(\n (f) => f.id === forward.id,\n );\n\n if (updatedForward) {\n return { ...forward, inx: updatedForward.inx };\n }\n\n return forward;\n }),\n );\n } else {\n toast.error(\"保存排序失败:\" + (response.msg || \"未知错误\"));\n }\n } catch {\n toast.error(\"保存排序失败,请重试\");\n }\n }\n };\n\n const toggleSelectMode = () => {\n setSelectMode(!selectMode);\n if (selectMode) {\n setSelectedIds(new Set());\n }\n };\n\n const toggleSelect = (id: number) => {\n const newSet = new Set(selectedIds);\n\n if (newSet.has(id)) {\n newSet.delete(id);\n } else {\n newSet.add(id);\n }\n setSelectedIds(newSet);\n };\n\n const selectAll = () => {\n const allIds = sortedForwards.map((f) => f.id);\n\n setSelectedIds(new Set(allIds));\n };\n\n const deselectAll = () => {\n setSelectedIds(new Set());\n };\n\n const handleBatchDelete = async () => {\n if (selectedIds.size === 0) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchDelete(Array.from(selectedIds));\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n if (outcome.closeDeleteModal) {\n setBatchDeleteModalOpen(false);\n }\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n const handleBatchToggleService = async (enable: boolean) => {\n if (selectedIds.size === 0) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchToggleService(\n Array.from(selectedIds),\n enable,\n );\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n const handleBatchRedeploy = async () => {\n if (selectedIds.size === 0) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchRedeploy(\n Array.from(selectedIds),\n );\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n const handleBatchChangeTunnel = async () => {\n if (selectedIds.size === 0 || !batchTargetTunnelId) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchChangeTunnel(\n Array.from(selectedIds),\n batchTargetTunnelId,\n );\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n if (outcome.closeChangeTunnelModal) {\n setBatchChangeTunnelModalOpen(false);\n }\n if (outcome.resetTargetTunnel) {\n setBatchTargetTunnelId(null);\n }\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n // 传感器配置 - 使用默认配置避免错误\n const sensors = useSensors(\n useSensor(MouseSensor, {\n activationConstraint: {\n distance: 8,\n },\n }),\n useSensor(TouchSensor, {\n activationConstraint: {\n delay: 250,\n tolerance: 8,\n },\n }),\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n }),\n );\n\n // 根据排序顺序获取转发列表\n const orderedForwards = useMemo((): Forward[] => {\n // 确保 forwards 数组存在且有效\n if (!forwards || forwards.length === 0) {\n return [];\n }\n\n let filteredForwards = forwards;\n\n if (filterUserId !== \"all\") {\n const targetUserId = parseInt(filterUserId);\n\n filteredForwards = filteredForwards.filter(\n (f) => f.userId === targetUserId || (targetUserId === 0 && !f.userId),\n );\n }\n if (filterTunnelId !== \"all\") {\n const targetTunnelId = parseInt(filterTunnelId);\n\n filteredForwards = filteredForwards.filter(\n (f) => f.tunnelId === targetTunnelId,\n );\n }\n\n if (searchKeyword.trim()) {\n const lowerKeyword = searchKeyword.toLowerCase();\n\n filteredForwards = filteredForwards.filter(\n (f) =>\n (f.name && f.name.toLowerCase().includes(lowerKeyword)) ||\n (f.remoteAddr && f.remoteAddr.toLowerCase().includes(lowerKeyword)) ||\n (f.userName && f.userName.toLowerCase().includes(lowerKeyword)),\n );\n }\n\n // 确保过滤后的转发列表有效\n if (!filteredForwards || filteredForwards.length === 0) {\n return [];\n }\n\n // 优先使用数据库中的 inx 字段进行排序\n const sortedByDb = [...filteredForwards].sort((a, b) => {\n const aInx = a.inx ?? 0;\n const bInx = b.inx ?? 0;\n\n if (aInx !== bInx) {\n return aInx - bInx;\n }\n\n return (a.id ?? 0) - (b.id ?? 0);\n });\n\n // 如果数据库中没有排序信息,则使用本地存储的顺序\n if (\n forwardOrder &&\n forwardOrder.length > 0 &&\n sortedByDb.every((f) => f.inx === undefined || f.inx === 0)\n ) {\n const forwardMap = new Map(filteredForwards.map((f) => [f.id, f]));\n const localSortedForwards: Forward[] = [];\n\n forwardOrder.forEach((id) => {\n const forward = forwardMap.get(id);\n\n if (forward) {\n localSortedForwards.push(forward);\n }\n });\n\n // 添加不在排序列表中的转发(新添加的)\n filteredForwards.forEach((forward) => {\n if (!forwardOrder.includes(forward.id)) {\n localSortedForwards.push(forward);\n }\n });\n\n return localSortedForwards;\n }\n\n return sortedByDb;\n }, [forwards, forwardOrder, filterUserId, filterTunnelId, searchKeyword]);\n\n const availableGroupData = useMemo(\n () => buildAvailableGroupData(forwards),\n [forwards],\n );\n\n const sanitizedGroupOrderMap = useMemo(\n () =>\n sanitizeGroupOrderMap(\n groupOrderMap,\n availableGroupData.availableTunnelKeysByUser,\n ),\n [groupOrderMap, availableGroupData],\n );\n\n const sanitizedCollapsedTunnelGroups = useMemo(\n () =>\n sanitizeGroupCollapsedMap(\n collapsedTunnelGroups,\n availableGroupData.availableCollapseKeys,\n ),\n [collapsedTunnelGroups, availableGroupData],\n );\n\n useEffect(() => {\n if (!groupPreferenceHydrated || tokenUserId === null) {\n return;\n }\n\n if (!isSameGroupOrderMap(groupOrderMap, sanitizedGroupOrderMap)) {\n setGroupOrderMap(sanitizedGroupOrderMap);\n persistGroupOrderToLocal(sanitizedGroupOrderMap);\n void persistGroupOrderToGlobal(sanitizedGroupOrderMap);\n }\n\n if (\n !isSameGroupCollapsedMap(\n collapsedTunnelGroups,\n sanitizedCollapsedTunnelGroups,\n )\n ) {\n setCollapsedTunnelGroups(sanitizedCollapsedTunnelGroups);\n persistGroupCollapsedToLocal(sanitizedCollapsedTunnelGroups);\n void persistGroupCollapsedToGlobal(sanitizedCollapsedTunnelGroups);\n }\n }, [\n groupPreferenceHydrated,\n tokenUserId,\n groupOrderMap,\n sanitizedGroupOrderMap,\n collapsedTunnelGroups,\n sanitizedCollapsedTunnelGroups,\n ]);\n\n const groupedForwards = useMemo((): ForwardUserGroup[] => {\n if (orderedForwards.length === 0) {\n return [];\n }\n\n type MutableForwardUserGroup = {\n userId: number;\n userName: string;\n tunnelMap: Map;\n };\n\n const userGroupMap = new Map();\n\n orderedForwards.forEach((forward) => {\n const userId = forward.userId ?? 0;\n const userName = normalizeForwardUserName(forward.userName);\n const tunnelName = normalizeForwardTunnelName(forward.tunnelName);\n const tunnelKey = buildForwardTunnelGroupKey(forward.tunnelName);\n\n let existingGroup = userGroupMap.get(userId);\n\n if (!existingGroup) {\n existingGroup = {\n userId,\n userName,\n tunnelMap: new Map(),\n };\n userGroupMap.set(userId, existingGroup);\n } else if (\n existingGroup.userName === UNKNOWN_FORWARD_USER_NAME &&\n userName !== UNKNOWN_FORWARD_USER_NAME\n ) {\n existingGroup.userName = userName;\n }\n\n const existingTunnelGroup = existingGroup.tunnelMap.get(tunnelKey);\n\n if (!existingTunnelGroup) {\n existingGroup.tunnelMap.set(tunnelKey, {\n tunnelKey,\n tunnelName,\n items: [forward],\n });\n\n return;\n }\n\n existingTunnelGroup.items.push(forward);\n\n if (\n existingTunnelGroup.tunnelName === UNCATEGORIZED_FORWARD_TUNNEL_NAME &&\n tunnelName !== UNCATEGORIZED_FORWARD_TUNNEL_NAME\n ) {\n existingTunnelGroup.tunnelName = tunnelName;\n }\n });\n\n const groups = Array.from(userGroupMap.values()).map((group) => {\n const tunnels = Array.from(group.tunnelMap.values());\n const tunnelOrder = sanitizedGroupOrderMap[group.userId.toString()] || [];\n const tunnelOrderIndex = new Map();\n\n tunnelOrder.forEach((key, index) => {\n tunnelOrderIndex.set(key, index);\n });\n\n tunnels.sort((a, b) => {\n const aIndex = tunnelOrderIndex.get(a.tunnelKey);\n const bIndex = tunnelOrderIndex.get(b.tunnelKey);\n\n if (aIndex !== undefined || bIndex !== undefined) {\n if (aIndex === undefined) {\n return 1;\n }\n\n if (bIndex === undefined) {\n return -1;\n }\n\n return aIndex - bIndex;\n }\n\n const nameCompare = compareForwardTunnelNameAsc(\n a.tunnelName,\n b.tunnelName,\n );\n\n if (nameCompare !== 0) {\n return nameCompare;\n }\n\n return compareForwardTunnelNameAsc(a.tunnelKey, b.tunnelKey);\n });\n\n return {\n userId: group.userId,\n userName: group.userName,\n tunnels,\n };\n });\n\n groups.sort((a, b) => {\n if (isAdmin && tokenUserId !== null) {\n const aIsSelf = a.userId === tokenUserId;\n const bIsSelf = b.userId === tokenUserId;\n\n if (aIsSelf !== bIsSelf) {\n return aIsSelf ? -1 : 1;\n }\n }\n\n const nameCompare = compareForwardUserNameAsc(a.userName, b.userName);\n\n if (nameCompare !== 0) {\n return nameCompare;\n }\n\n return a.userId - b.userId;\n });\n\n return groups;\n }, [orderedForwards, isAdmin, tokenUserId, sanitizedGroupOrderMap]);\n\n const sortedForwards = useMemo(() => {\n if (compactMode) {\n return orderedForwards;\n }\n\n return groupedForwards.flatMap((group) =>\n group.tunnels.flatMap((tunnel) => tunnel.items),\n );\n }, [compactMode, orderedForwards, groupedForwards]);\n\n const sortableForwardIds = useMemo(\n () => sortedForwards.map((f) => f.id).filter((id) => id > 0),\n [sortedForwards],\n );\n\n const toggleTunnelGroupCollapsed = (userId: number, tunnelKey: string) => {\n const collapseKey = buildTunnelGroupCollapseKey(userId, tunnelKey);\n const nextCollapsedMap: ForwardGroupCollapsedMap = {\n ...sanitizedCollapsedTunnelGroups,\n };\n\n if (nextCollapsedMap[collapseKey] === true) {\n delete nextCollapsedMap[collapseKey];\n } else {\n nextCollapsedMap[collapseKey] = true;\n }\n\n setCollapsedTunnelGroups(nextCollapsedMap);\n persistGroupCollapsedToLocal(nextCollapsedMap);\n void persistGroupCollapsedToGlobal(nextCollapsedMap);\n };\n\n const SortableTunnelGroupContainer = ({\n groupUserId,\n tunnel,\n collapsed,\n onToggleCollapsed,\n wrapperClassName,\n headerClassName,\n titleClassName,\n countClassName,\n bodyClassName,\n children,\n }: {\n groupUserId: number;\n tunnel: ForwardTunnelGroup;\n collapsed: boolean;\n onToggleCollapsed: () => void;\n wrapperClassName: string;\n headerClassName: string;\n titleClassName: string;\n countClassName: string;\n bodyClassName: string;\n children: React.ReactNode;\n }) => {\n const sortableId = buildTunnelGroupSortableId(\n groupUserId,\n tunnel.tunnelKey,\n );\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: sortableId });\n\n const style: React.CSSProperties = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.55 : 1,\n willChange: isDragging ? \"transform\" : undefined,\n zIndex: isDragging ? 1 : undefined,\n };\n\n return (\n
\n
\n
\n \n \n \n \n \n {tunnel.tunnelName}\n
\n
\n {tunnel.items.length} 条转发\n \n \n \n \n
\n
\n
\n {!collapsed &&
{children}
}\n \n );\n };\n\n // 可拖拽的转发卡片组件\n const SortableForwardCard = ({ forward }: { forward: Forward }) => {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: forward.id });\n\n const style: React.CSSProperties = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.5 : 1,\n willChange: isDragging ? \"transform\" : undefined,\n };\n\n return (\n
\n {renderForwardCard(forward, listeners)}\n
\n );\n };\n\n // 生成用作筛选项的用户和隧道列表\n const uniqueUsers = useMemo(() => {\n const userMap = new Map();\n\n forwards.forEach((f) => {\n const uId = f.userId ?? 0;\n const userName = normalizeForwardUserName(f.userName);\n const existingUser = userMap.get(uId);\n\n if (!existingUser) {\n userMap.set(uId, { id: uId, name: userName });\n\n return;\n }\n\n if (\n existingUser.name === UNKNOWN_FORWARD_USER_NAME &&\n userName !== UNKNOWN_FORWARD_USER_NAME\n ) {\n existingUser.name = userName;\n }\n });\n\n const users = Array.from(userMap.values());\n\n users.sort((a, b) => {\n if (isAdmin && tokenUserId !== null) {\n const aIsSelf = a.id === tokenUserId;\n const bIsSelf = b.id === tokenUserId;\n\n if (aIsSelf !== bIsSelf) {\n return aIsSelf ? -1 : 1;\n }\n }\n\n const nameCompare = compareForwardUserNameAsc(a.name, b.name);\n\n if (nameCompare !== 0) {\n return nameCompare;\n }\n\n return a.id - b.id;\n });\n\n return users;\n }, [forwards, isAdmin, tokenUserId]);\n\n // 可拖拽的表格行组件\n const SortableTableRow = ({\n forward,\n selectMode,\n selectedIds,\n toggleSelect,\n getStrategyDisplay,\n formatInAddress,\n formatRemoteAddress,\n handleServiceToggle,\n handleEdit,\n handleDelete,\n handleDiagnose,\n showAddressModal,\n hasMultipleAddresses,\n formatFlow,\n }: any) => {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: forward.id });\n\n const style = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.5 : 1,\n backgroundColor: isDragging ? \"var(--nextui-default-100)\" : undefined,\n };\n\n const strategyDisplay = getStrategyDisplay(forward.strategy);\n\n return (\n \n {selectMode && (\n \n toggleSelect(forward.id)}\n />\n \n )}\n \n \n \n \n \n \n \n \n {forward.name}\n \n \n \n showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n }\n >\n {formatInAddress(forward.inIp, forward.inPort)}\n \n \n \n \n showAddressModal(forward.remoteAddr, null, \"目标地址\")\n }\n >\n {formatRemoteAddress(forward.remoteAddr)}\n \n \n \n \n {strategyDisplay.text}\n \n \n \n \n {formatFlow(getForwardDisplayFlow(forward))}\n \n \n \n
\n handleServiceToggle(forward)}\n />\n
\n
\n \n
\n handleEdit(forward)}\n >\n \n \n \n \n handleDiagnose(forward)}\n >\n \n \n \n \n handleDelete(forward)}\n >\n \n \n \n \n
\n
\n
\n );\n };\n\n const SortableCompactTableRow = ({\n forward,\n selectMode,\n selectedIds,\n toggleSelect,\n getStrategyDisplay,\n formatInAddress,\n formatRemoteAddress,\n handleServiceToggle,\n handleEdit,\n handleDelete,\n handleDiagnose,\n showAddressModal,\n hasMultipleAddresses,\n formatFlow,\n }: any) => {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: forward.id });\n\n const style = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.5 : 1,\n backgroundColor: isDragging ? \"var(--nextui-default-100)\" : undefined,\n };\n\n const strategyDisplay = getStrategyDisplay(forward.strategy);\n\n return (\n \n {selectMode && (\n \n toggleSelect(forward.id)}\n />\n \n )}\n \n \n \n \n \n \n \n \n \n {forward.userName || \"未知用户\"}\n \n \n \n {forward.name}\n \n \n \n \n {forward.tunnelName}\n \n \n \n \n \n showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n }\n >\n {formatInAddress(forward.inIp, forward.inPort)}\n \n \n \n \n showAddressModal(forward.remoteAddr, null, \"目标地址\")\n }\n >\n {formatRemoteAddress(forward.remoteAddr)}\n \n \n \n \n {strategyDisplay.text}\n \n \n \n \n {formatFlow(getForwardDisplayFlow(forward))}\n \n \n \n
\n handleServiceToggle(forward)}\n />\n
\n
\n \n
\n handleEdit(forward)}\n >\n \n \n \n \n handleDiagnose(forward)}\n >\n \n \n \n \n handleDelete(forward)}\n >\n \n \n \n \n
\n
\n
\n );\n };\n\n // 渲染转发卡片\n const renderForwardCard = (forward: Forward, listeners?: any) => {\n const statusDisplay = getStatusDisplay(forward.status);\n const strategyDisplay = getStrategyDisplay(forward.strategy);\n\n return (\n \n \n
\n {selectMode && (\n toggleSelect(forward.id)}\n />\n )}\n
\n

\n {forward.name}\n

\n

\n {normalizeForwardTunnelName(forward.tunnelName)}\n

\n
\n
\n {viewMode === \"direct\" && (\n \n \n \n \n
\n )}\n handleServiceToggle(forward)}\n />\n \n {statusDisplay.text}\n \n
\n \n
\n\n \n
\n {/* 地址信息 */}\n
\n \n showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n }\n >\n
\n
\n \n 入口:\n \n \n {formatInAddress(forward.inIp, forward.inPort)}\n \n
\n {hasMultipleAddresses(forward.inIp) && (\n \n \n \n )}\n
\n \n\n \n showAddressModal(forward.remoteAddr, null, \"目标地址\")\n }\n >\n
\n
\n \n 目标:\n \n \n {formatRemoteAddress(forward.remoteAddr)}\n \n
\n {hasMultipleAddresses(forward.remoteAddr) && (\n \n \n \n )}\n
\n \n
\n\n {/* 统计信息 */}\n
\n \n {strategyDisplay.text}\n \n {(forward.inFlow || 0) + (forward.outFlow || 0) > 0 ? (\n <>\n
\n \n ↑{formatFlow(forward.inFlow || 0)}\n \n
\n \n ↓{formatFlow(forward.outFlow || 0)}\n \n \n ) : (forward.federationShareFlow || 0) > 0 ? (\n \n 共享 {formatFlow(forward.federationShareFlow || 0)}\n \n ) : (\n \n 总流量 {formatFlow(0)}\n \n )}\n
\n
\n\n
\n \n \n \n }\n variant=\"flat\"\n onPress={() => handleEdit(forward)}\n >\n 编辑\n \n \n \n \n }\n variant=\"flat\"\n onPress={() => handleDiagnose(forward)}\n >\n 诊断\n \n \n \n \n \n }\n variant=\"flat\"\n onPress={() => handleDelete(forward)}\n >\n 删除\n \n
\n
\n \n );\n };\n\n if (loading || !groupPreferenceHydrated) {\n return ;\n }\n\n return (\n \n {/* 页面头部 */}\n
\n
\n setIsSearchVisible(false)}\n onOpen={() => setIsSearchVisible(true)}\n />\n
\n
\n
*]:shrink-0\">\n {selectMode ? (\n <>\n \n 已选择 {selectedIds.size} 项\n \n \n 全选\n \n \n 清空\n \n setBatchDeleteModalOpen(true)}\n >\n 删除\n \n handleBatchToggleService(false)}\n >\n 停用\n \n handleBatchToggleService(true)}\n >\n 启用\n \n \n 下发\n \n setBatchChangeTunnelModalOpen(true)}\n >\n 隧道\n \n \n 退出\n \n \n ) : (\n <>\n {/* 筛选按钮 */}\n setIsFilterModalOpen(true)}\n >\n \n \n \n {(filterUserId !== \"all\" || filterTunnelId !== \"all\") && (\n \n )}\n \n {/* 显示模式切换按钮 */}\n \n {viewMode === \"grouped\" ? (\n \n \n \n ) : (\n \n \n \n )}\n \n\n {/* 导入按钮 */}\n \n 导入\n \n\n {/* 导出按钮 */}\n \n 导出\n \n\n \n 批量\n \n\n \n 新增\n \n \n )}\n
\n
\n
\n\n {/* 根据显示模式渲染不同内容 */}\n {compactMode ? (\n viewMode === \"grouped\" ? (\n sortedForwards.length > 0 ? (\n
\n \n \n \n {selectMode && (\n 选择\n )}\n \n 用户\n 名称\n 隧道\n 入口\n 目标\n 策略\n 总流量\n 状态\n 操作\n \n \n {(forward) => (\n \n \n \n )}\n \n \n \n
\n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )\n ) : sortedForwards.length > 0 ? (\n {}}\n >\n \n
\n {sortedForwards.map((forward) =>\n forward && forward.id ? (\n \n ) : null,\n )}\n
\n \n \n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )\n ) : viewMode === \"grouped\" ? (\n sortedForwards.length > 0 ? (\n
\n {groupedForwards.map((group) => {\n const isSelfGroup =\n isAdmin && tokenUserId !== null && group.userId === tokenUserId;\n const groupForwardCount = group.tunnels.reduce(\n (total, tunnel) => total + tunnel.items.length,\n 0,\n );\n\n return (\n \n
\n
\n \n {group.userName}\n \n {isSelfGroup && (\n \n 管理员本人\n \n )}\n
\n \n {groupForwardCount} 条转发\n \n
\n\n
\n \n \n buildTunnelGroupSortableId(\n group.userId,\n tunnel.tunnelKey,\n ),\n )}\n strategy={verticalListSortingStrategy}\n >\n {group.tunnels.map((tunnel) => {\n const tunnelSortableForwardIds = tunnel.items\n .map((item) => item.id)\n .filter((id) => id > 0);\n const collapsed =\n sanitizedCollapsedTunnelGroups[\n buildTunnelGroupCollapseKey(\n group.userId,\n tunnel.tunnelKey,\n )\n ] === true;\n\n return (\n \n toggleTunnelGroupCollapsed(\n group.userId,\n tunnel.tunnelKey,\n )\n }\n >\n \n \n \n {selectMode && (\n \n 选择\n \n )}\n \n \n 名称\n \n \n 入口\n \n \n 目标\n \n \n 策略\n \n \n 总流量\n \n \n 状态\n \n \n 操作\n \n \n \n {(forward) => (\n \n \n \n )}\n \n \n \n \n );\n })}\n \n \n
\n
\n );\n })}\n \n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )\n ) : sortedForwards.length > 0 ? (\n
\n {groupedForwards.map((group) => {\n const isSelfGroup =\n isAdmin && tokenUserId !== null && group.userId === tokenUserId;\n const groupForwardCount = group.tunnels.reduce(\n (total, tunnel) => total + tunnel.items.length,\n 0,\n );\n\n return (\n \n
\n
\n \n {group.userName}\n \n {isSelfGroup && (\n \n 管理员本人\n \n )}\n
\n \n {groupForwardCount} 条转发\n \n
\n\n
\n \n \n buildTunnelGroupSortableId(\n group.userId,\n tunnel.tunnelKey,\n ),\n )}\n strategy={verticalListSortingStrategy}\n >\n {group.tunnels.map((tunnel) => {\n const tunnelSortableForwardIds = tunnel.items\n .map((item) => item.id)\n .filter((id) => id > 0);\n const collapsed =\n sanitizedCollapsedTunnelGroups[\n buildTunnelGroupCollapseKey(\n group.userId,\n tunnel.tunnelKey,\n )\n ] === true;\n\n return (\n \n toggleTunnelGroupCollapsed(\n group.userId,\n tunnel.tunnelKey,\n )\n }\n >\n {}}\n >\n \n
\n {tunnel.items.map((forward) =>\n forward && forward.id ? (\n \n ) : null,\n )}\n
\n \n \n \n );\n })}\n \n \n
\n
\n );\n })}\n \n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )}\n\n {/* 新增/编辑模态框 */}\n \n \n {(onClose) => (\n <>\n \n

\n {isEdit ? \"编辑转发\" : \"新增转发\"}\n

\n

\n {isEdit ? \"修改现有转发配置的信息\" : \"创建新的转发配置\"}\n

\n
\n \n
\n \n setForm((prev) => ({ ...prev, name: e.target.value }))\n }\n />\n\n {\n const selectedKey = Array.from(keys)[0] as\n | string\n | undefined;\n\n setForm((prev) => ({\n ...prev,\n speedId: selectedKey ? Number(selectedKey) : null,\n }));\n }}\n >\n {availableSpeedLimits.map((speedLimit) => (\n \n {speedLimit.name}\n \n ))}\n \n\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n if (selectedKey) {\n handleTunnelChange(selectedKey);\n }\n }}\n >\n {tunnels.map((tunnel) => (\n {tunnel.name}\n ))}\n \n\n {\n const value = e.target.value;\n\n setForm((prev) => ({\n ...prev,\n inPort: value ? parseInt(value) : null,\n }));\n }}\n />\n\n 0\n ? \"选择入口监听IP\"\n : \"当前隧道入口节点暂无可选IP\"\n : \"请先选择隧道\"\n }\n selectedKeys={[form.inIp || \"__default__\"]}\n variant=\"bordered\"\n onSelectionChange={(keys) => {\n const selectedKey = Array.from(keys)[0] as string;\n\n setForm((prev) => ({\n ...prev,\n inIp: selectedKey === \"__default__\" ? \"\" : selectedKey,\n }));\n }}\n >\n 默认入口IP\n {currentTunnelIpOptions.map((ip) => (\n {ip}\n ))}\n \n\n \n setForm((prev) => ({\n ...prev,\n remoteAddr: e.target.value,\n }))\n }\n />\n\n {getAddressCount(form.remoteAddr) > 1 && (\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n setForm((prev) => ({ ...prev, strategy: selectedKey }));\n }}\n >\n 主备模式 - 自上而下\n 轮询模式 - 依次轮换\n 随机模式 - 随机选择\n 哈希模式 - IP哈希\n \n )}\n
\n
\n \n \n \n {isEdit ? \"保存修改\" : \"创建转发\"}\n \n \n \n )}\n
\n \n\n {/* 删除确认模态框 */}\n \n \n {(onClose) => (\n <>\n \n

确认删除

\n
\n \n

\n 确定要删除转发{\" \"}\n \n "{forwardToDelete?.name}"\n {\" \"}\n 吗?\n

\n

\n 此操作无法撤销,删除后该转发将永久消失。\n

\n
\n \n \n \n 确认删除\n \n \n \n )}\n
\n \n\n {/* 地址列表弹窗 */}\n setAddressModalOpen(false)}\n >\n \n {addressModalTitle}\n \n
\n \n
\n\n
\n {addressList.map((item) => (\n \n \n {item.address}\n \n copyAddress(item)}\n >\n 复制\n \n
\n ))}\n \n
\n
\n \n\n {/* 导出数据模态框 */}\n {\n setExportModalOpen(false);\n setSelectedTunnelForExport(null);\n setExportData(\"\");\n }}\n >\n \n \n

导出转发数据

\n

\n 格式:目标地址|转发名称|入口端口\n

\n
\n \n
\n {/* 隧道选择 */}\n
\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n setSelectedTunnelForExport(\n selectedKey ? parseInt(selectedKey) : null,\n );\n }}\n >\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n
\n\n {/* 导出按钮和数据 */}\n {exportData && (\n
\n \n \n \n }\n onPress={executeExport}\n >\n 重新生成\n \n \n \n \n \n }\n onPress={copyExportData}\n >\n 复制\n \n
\n )}\n\n {/* 初始导出按钮 */}\n {!exportData && (\n
\n \n \n \n }\n onPress={executeExport}\n >\n 生成导出数据\n \n
\n )}\n\n {/* 导出数据显示 */}\n {exportData && (\n
\n \n
\n )}\n
\n
\n \n \n \n
\n \n\n {/* 导入数据模态框 */}\n setImportModalOpen(false)}\n >\n \n \n

导入转发数据

\n {importFormat === \"flvx\" ? (\n <>\n

\n 格式:目标地址|转发名称|入口端口,每行一个,入口端口留空将自动分配可用端口\n

\n

\n 目标地址支持单个地址(如:example.com:8080)或多个地址用逗号分隔(如:3.3.3.3:3,4.4.4.4:4)\n

\n \n ) : (\n <>\n

\n ny格式:JSON对象,支持多个目标地址(负载均衡)\n

\n

\n 格式:{"dest":["地址:端口"],"listen_port":端口,"name":"名称"}\n

\n \n )}\n
\n \n
\n {/* 格式选择 */}\n {\n const selectedKey = Array.from(keys)[0] as ImportFormat;\n\n if (selectedKey) {\n setImportFormat(selectedKey);\n setSelectedTunnelForImport(null);\n setImportData(\"\");\n setImportResults([]);\n }\n }}\n >\n \n flvx格式(管道分隔)\n \n \n ny格式(JSON)\n \n \n\n {/* 隧道选择 - 两种格式都需要 */}\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n setSelectedTunnelForImport(\n selectedKey ? parseInt(selectedKey) : null,\n );\n }}\n >\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n\n {/* 输入区域 */}\n setImportData(e.target.value)}\n />\n\n {/* 导入结果 */}\n {importResults.length > 0 && (\n
\n
\n

导入结果

\n \n 成功:{importResults.filter((r) => r.success).length} /\n 总计:{importResults.length}\n \n
\n\n \n {importResults.map((result, index) => (\n \n
\n {result.success ? (\n \n \n \n ) : (\n \n \n \n )}\n
\n
\n \n {result.success ? \"成功\" : \"失败\"}\n \n \n |\n \n \n {result.line}\n \n
\n \n {result.message}\n
\n
\n
\n
\n ))}\n \n \n )}\n \n
\n \n \n \n 开始导入\n \n \n
\n \n\n {/* 诊断结果模态框 */}\n {\n setDiagnosisModalOpen(open);\n if (!open) {\n diagnosisAbortRef.current?.abort();\n diagnosisAbortRef.current = null;\n setDiagnosisLoading(false);\n }\n }}\n >\n \n {(onClose) => (\n <>\n \n

转发诊断结果

\n {currentDiagnosisForward && (\n
\n \n {currentDiagnosisForward.name}\n \n \n 转发服务\n \n
\n )}\n
\n \n {diagnosisResult ? (\n
\n {diagnosisLoading && (\n
\n
\n \n \n 正在诊断 {diagnosisProgress.completed}/\n {diagnosisProgress.total > 0\n ? diagnosisProgress.total\n : \"?\"}\n \n
\n \n 流式更新中\n \n
\n )}\n\n {diagnosisProgress.timedOut && (\n \n )}\n\n {/* 统计摘要 */}\n
\n
\n
\n {diagnosisProgress.total > 0\n ? diagnosisProgress.total\n : diagnosisResult.results.length}\n
\n
\n 总测试数\n
\n
\n
\n
\n {diagnosisProgress.completed > 0 ||\n diagnosisProgress.total > 0\n ? diagnosisProgress.success\n : diagnosisResult.results.filter((r) => r.success)\n .length}\n
\n
\n 成功\n
\n
\n
\n
\n {diagnosisProgress.completed > 0 ||\n diagnosisProgress.total > 0\n ? diagnosisProgress.failed\n : diagnosisResult.results.filter((r) => !r.success)\n .length}\n
\n
\n 失败\n
\n
\n
\n\n {/* 桌面端表格展示 */}\n
\n {(() => {\n // 使用后端返回的 chainType 和 inx 字段进行分组\n const groupedResults = {\n entry: diagnosisResult.results.filter(\n (r) => r.fromChainType === 1,\n ),\n chains: {} as Record<\n number,\n typeof diagnosisResult.results\n >,\n exit: diagnosisResult.results.filter(\n (r) => r.fromChainType === 3,\n ),\n };\n\n // 按 inx 分组链路测试\n diagnosisResult.results.forEach((r) => {\n if (r.fromChainType === 2 && r.fromInx != null) {\n if (!groupedResults.chains[r.fromInx]) {\n groupedResults.chains[r.fromInx] = [];\n }\n groupedResults.chains[r.fromInx].push(r);\n }\n });\n\n const renderTableSection = (\n title: string,\n results: typeof diagnosisResult.results,\n ) => {\n if (results.length === 0) return null;\n\n return (\n \n
\n

\n {title}\n

\n
\n \n \n \n \n \n \n \n \n \n \n \n {results.map((result, index) => {\n const isDiagnosing = Boolean(\n result.diagnosing,\n );\n const isSuccess = result.success === true;\n const quality =\n getForwardDiagnosisQualityDisplay(\n result.averageTime,\n result.packetLoss,\n );\n\n return (\n \n \n \n \n \n \n \n );\n })}\n \n
\n 路径\n \n 状态\n \n 延迟(ms)\n \n 丢包率\n \n 质量\n
\n
\n {isDiagnosing ? (\n \n ) : (\n \n {isSuccess ? \"✓\" : \"✗\"}\n \n )}\n
\n
\n {result.description}\n
\n
\n {result.targetIp}:\n {result.targetPort}\n
\n
\n
\n
\n \n {isDiagnosing\n ? \"诊断中\"\n : isSuccess\n ? \"成功\"\n : \"失败\"}\n \n \n {isSuccess ? (\n \n {result.averageTime?.toFixed(0)}\n \n ) : (\n \n -\n \n )}\n \n {isSuccess ? (\n 0\n ? \"text-warning\"\n : \"text-success\"\n }`}\n >\n {result.packetLoss?.toFixed(1)}%\n \n ) : (\n \n -\n \n )}\n \n {isSuccess && quality ? (\n \n {quality.text}\n \n ) : (\n \n -\n \n )}\n
\n
\n );\n };\n\n return (\n <>\n {/* 入口测试 */}\n {renderTableSection(\n \"🚪 入口测试\",\n groupedResults.entry,\n )}\n\n {/* 链路测试(按跳数排序) */}\n {Object.keys(groupedResults.chains)\n .map(Number)\n .sort((a, b) => a - b)\n .map((hop) =>\n renderTableSection(\n `🔗 转发链 - 第${hop}跳`,\n groupedResults.chains[hop],\n ),\n )}\n\n {/* 出口测试 */}\n {renderTableSection(\n \"🚀 出口测试\",\n groupedResults.exit,\n )}\n \n );\n })()}\n
\n\n {/* 移动端卡片展示 */}\n
\n {(() => {\n // 使用后端返回的 chainType 和 inx 字段进行分组\n const groupedResults = {\n entry: diagnosisResult.results.filter(\n (r) => r.fromChainType === 1,\n ),\n chains: {} as Record<\n number,\n typeof diagnosisResult.results\n >,\n exit: diagnosisResult.results.filter(\n (r) => r.fromChainType === 3,\n ),\n };\n\n // 按 inx 分组链路测试\n diagnosisResult.results.forEach((r) => {\n if (r.fromChainType === 2 && r.fromInx != null) {\n if (!groupedResults.chains[r.fromInx]) {\n groupedResults.chains[r.fromInx] = [];\n }\n groupedResults.chains[r.fromInx].push(r);\n }\n });\n\n const renderCardSection = (\n title: string,\n results: typeof diagnosisResult.results,\n ) => {\n if (results.length === 0) return null;\n\n return (\n
\n
\n

\n {title}\n

\n
\n {results.map((result, index) => {\n const isDiagnosing = Boolean(result.diagnosing);\n const isSuccess = result.success === true;\n const quality =\n getForwardDiagnosisQualityDisplay(\n result.averageTime,\n result.packetLoss,\n );\n\n return (\n \n
\n {isDiagnosing ? (\n \n ) : (\n \n {isSuccess ? \"✓\" : \"✗\"}\n \n )}\n
\n
\n {result.description}\n
\n
\n {result.targetIp}:{result.targetPort}\n
\n
\n \n {isDiagnosing\n ? \"诊断中\"\n : isSuccess\n ? \"成功\"\n : \"失败\"}\n \n
\n\n {isSuccess ? (\n
\n
\n
\n {result.averageTime?.toFixed(0)}\n
\n
\n 延迟(ms)\n
\n
\n
\n 0\n ? \"text-warning\"\n : \"text-success\"\n }`}\n >\n {result.packetLoss?.toFixed(1)}%\n
\n
\n 丢包率\n
\n
\n
\n {quality && (\n <>\n \n {quality.text}\n \n
\n 质量\n
\n \n )}\n
\n
\n ) : (\n
\n \n {isDiagnosing\n ? result.message || \"诊断中...\"\n : result.message || \"连接失败\"}\n
\n
\n )}\n \n );\n })}\n \n );\n };\n\n return (\n <>\n {/* 入口测试 */}\n {renderCardSection(\n \"🚪 入口测试\",\n groupedResults.entry,\n )}\n\n {/* 链路测试(按跳数排序) */}\n {Object.keys(groupedResults.chains)\n .map(Number)\n .sort((a, b) => a - b)\n .map((hop) =>\n renderCardSection(\n `🔗 转发链 - 第${hop}跳`,\n groupedResults.chains[hop],\n ),\n )}\n\n {/* 出口测试 */}\n {renderCardSection(\n \"🚀 出口测试\",\n groupedResults.exit,\n )}\n \n );\n })()}\n \n\n {/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */}\n {diagnosisResult.results.some(\n (r) => r.success === false && !r.diagnosing,\n ) && (\n \n )}\n \n ) : (\n
\n
\n \n \n \n
\n

\n 暂无诊断数据\n

\n
\n )}\n
\n \n \n {currentDiagnosisForward && (\n handleDiagnose(currentDiagnosisForward)}\n >\n 重新诊断\n \n )}\n \n \n )}\n
\n \n\n {/* 批量删除确认模态框 */}\n \n \n {(onClose) => (\n <>\n 确认删除\n \n

\n 确定要删除选中的 {selectedIds.size} 项转发吗?此操作不可撤销。\n

\n
\n \n \n \n 确认删除\n \n \n \n )}\n
\n \n\n {/* 批量换隧道模态框 */}\n \n \n {(onClose) => (\n <>\n 隧道\n \n

\n 将选中的 {selectedIds.size} 项转发迁移到新隧道:\n

\n {\n const selected = Array.from(keys)[0];\n\n setBatchTargetTunnelId(selected ? Number(selected) : null);\n }}\n >\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n
\n \n \n \n 确认换隧道\n \n \n \n )}\n
\n \n\n {/* 筛选模态框 */}\n \n \n {(onClose) => (\n <>\n \n 筛选条件\n \n \n
\n
\n

按用户筛选

\n {\n const key = Array.from(keys)[0] as string;\n\n setFilterUserId(key || \"all\");\n }}\n >\n 全部用户\n {uniqueUsers.map((user) => (\n \n {user.name}\n \n ))}\n \n
\n
\n

按隧道筛选

\n {\n const key = Array.from(keys)[0] as string;\n\n setFilterTunnelId(key || \"all\");\n }}\n >\n 全部隧道\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n
\n
\n
\n \n {\n resetFilterUserId();\n resetFilterTunnelId();\n }}\n >\n 重置\n \n \n \n \n )}\n
\n \n
\n );\n}\n", "after": "import type { SpeedLimitApiItem } from \"@/api/types\";\n\nimport { useState, useEffect, useMemo, useRef } from \"react\";\nimport toast from \"react-hot-toast\";\nimport {\n DndContext,\n closestCenter,\n KeyboardSensor,\n MouseSensor,\n TouchSensor,\n useSensor,\n useSensors,\n DragEndEvent,\n} from \"@dnd-kit/core\";\nimport {\n arrayMove,\n SortableContext,\n sortableKeyboardCoordinates,\n rectSortingStrategy,\n verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { useSortable } from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\n\nimport { SearchBar } from \"@/components/search-bar\";\nimport { AnimatedPage } from \"@/components/animated-page\";\nimport { Card, CardBody, CardHeader } from \"@/shadcn-bridge/heroui/card\";\nimport { Button } from \"@/shadcn-bridge/heroui/button\";\nimport { Input } from \"@/shadcn-bridge/heroui/input\";\nimport { Textarea } from \"@/shadcn-bridge/heroui/input\";\nimport { Select, SelectItem } from \"@/shadcn-bridge/heroui/select\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableColumn,\n TableHeader,\n TableRow,\n} from \"@/shadcn-bridge/heroui/table\";\nimport {\n Modal,\n ModalContent,\n ModalHeader,\n ModalBody,\n ModalFooter,\n} from \"@/shadcn-bridge/heroui/modal\";\nimport { Chip } from \"@/shadcn-bridge/heroui/chip\";\nimport { Spinner } from \"@/shadcn-bridge/heroui/spinner\";\nimport { Switch } from \"@/shadcn-bridge/heroui/switch\";\nimport { Alert } from \"@/shadcn-bridge/heroui/alert\";\nimport { Checkbox } from \"@/shadcn-bridge/heroui/checkbox\";\nimport {\n createForward,\n getForwardList,\n getSpeedLimitList,\n getPeerShareList,\n getPeerRemoteUsageList,\n updateForward,\n deleteForward,\n forceDeleteForward,\n userTunnel,\n getTunnelList,\n getNodeList,\n pauseForwardService,\n resumeForwardService,\n diagnoseForward,\n updateForwardOrder,\n getConfigByName,\n updateConfig,\n} from \"@/api\";\nimport {\n type ForwardAddressItem,\n formatInAddress,\n formatRemoteAddress,\n hasMultipleAddresses,\n resolveForwardAddressAction,\n} from \"@/pages/forward/address\";\nimport {\n buildForwardDiagnosisFallbackResult,\n getForwardDiagnosisQualityDisplay,\n type ForwardDiagnosisResult,\n} from \"@/pages/forward/diagnosis\";\nimport { diagnoseForwardStream } from \"@/api/diagnosis-stream\";\nimport {\n executeForwardBatchChangeTunnel,\n executeForwardBatchDelete,\n executeForwardBatchRedeploy,\n executeForwardBatchToggleService,\n} from \"@/pages/forward/batch-actions\";\nimport {\n convertNyItemToForwardInput,\n parseNyFormatData,\n} from \"@/pages/forward/import-format\";\nimport { buildForwardOrder, FORWARD_ORDER_KEY } from \"@/pages/forward/order\";\nimport { PageLoadingState } from \"@/components/page-state\";\nimport { useMobileBreakpoint } from \"@/hooks/useMobileBreakpoint\";\nimport { useLocalStorageState } from \"@/hooks/use-local-storage-state\";\nimport { saveOrder } from \"@/utils/order-storage\";\nimport { JwtUtil } from \"@/utils/jwt\";\n\ninterface Forward {\n id: number;\n name: string;\n tunnelId: number;\n tunnelName: string;\n inIp: string;\n inPort: number;\n remoteAddr: string;\n interfaceName?: string;\n strategy: string;\n status: number;\n inFlow: number;\n outFlow: number;\n serviceRunning: boolean;\n federationShareFlow?: number;\n createdTime: string;\n userName?: string;\n userId?: number;\n inx?: number;\n speedId?: number | null;\n}\n\ninterface Tunnel {\n id: number;\n name: string;\n type?: number;\n inIp?: string;\n inNodeId?: Array<{ nodeId: number }>;\n inNodePortSta?: number;\n inNodePortEnd?: number;\n}\n\ninterface Node {\n id: number;\n name?: string;\n serverIp?: string;\n serverIpV4?: string;\n serverIpV6?: string;\n extraIPs?: string;\n}\n\ninterface ForwardForm {\n id?: number;\n userId?: number;\n name: string;\n tunnelId: number | null;\n inPort: number | null;\n inIp: string;\n remoteAddr: string;\n interfaceName?: string;\n strategy: string;\n speedId: number | null;\n}\n\ninterface ForwardUserGroup {\n userId: number;\n userName: string;\n tunnels: ForwardTunnelGroup[];\n}\n\ninterface ForwardTunnelGroup {\n tunnelKey: string;\n tunnelName: string;\n items: Forward[];\n}\n\ntype ForwardGroupOrderMap = Record;\ntype ForwardGroupCollapsedMap = Record;\n\nconst UNKNOWN_FORWARD_USER_NAME = \"未知用户\";\nconst UNCATEGORIZED_FORWARD_TUNNEL_NAME = \"未分类\";\nconst FORWARD_COMPACT_MODE_CONFIG_KEY = \"forward_compact_mode\";\nconst FORWARD_COMPACT_MODE_EVENT = \"forwardCompactModeChanged\";\nconst FORWARD_GROUP_ORDER_CONFIG_KEY = \"forward_group_order_map\";\nconst FORWARD_GROUP_COLLAPSED_CONFIG_KEY = \"forward_group_collapsed_map\";\nconst FORWARD_GROUP_ORDER_LOCAL_STORAGE_PREFIX = \"forward-group-order\";\nconst FORWARD_GROUP_COLLAPSED_LOCAL_STORAGE_PREFIX = \"forward-group-collapsed\";\nconst FORWARD_TUNNEL_GROUP_SORTABLE_PREFIX = \"forward-tunnel-group\";\nconst FORWARD_GROUPED_TABLE_MIN_WIDTH_CLASS = \"min-w-[1320px]\";\nconst FORWARD_GROUPED_TABLE_COLUMN_CLASS = {\n select: \"w-14\",\n drag: \"w-10 pl-4\",\n name: \"w-[200px]\",\n inbound: \"w-[280px]\",\n target: \"w-[280px]\",\n strategy: \"w-[100px]\",\n totalFlow: \"w-[120px]\",\n status: \"w-[100px]\",\n actions: \"w-[144px] text-right\",\n} as const;\n\nconst normalizeForwardUserName = (userName?: string): string => {\n const normalized = (userName || UNKNOWN_FORWARD_USER_NAME).trim();\n\n return normalized || UNKNOWN_FORWARD_USER_NAME;\n};\n\nconst compareForwardUserNameAsc = (a: string, b: string): number => {\n return a.localeCompare(b, \"en\", {\n sensitivity: \"base\",\n numeric: true,\n });\n};\n\nconst normalizeForwardTunnelName = (tunnelName?: string): string => {\n const normalized = (tunnelName || \"\").trim();\n\n return normalized || UNCATEGORIZED_FORWARD_TUNNEL_NAME;\n};\n\nconst buildForwardTunnelGroupKey = (tunnelName?: string): string => {\n const normalized = normalizeForwardTunnelName(tunnelName);\n\n if (normalized === UNCATEGORIZED_FORWARD_TUNNEL_NAME) {\n return \"__uncategorized__\";\n }\n\n return normalized.toLocaleLowerCase();\n};\n\nconst compareForwardTunnelNameAsc = (a: string, b: string): number => {\n return a.localeCompare(b, \"en\", {\n sensitivity: \"base\",\n numeric: true,\n });\n};\n\nconst compareForwardTunnelGroupKeyAsc = (a: string, b: string): number => {\n const aIsUncategorized = a === \"__uncategorized__\";\n const bIsUncategorized = b === \"__uncategorized__\";\n\n if (aIsUncategorized !== bIsUncategorized) {\n return aIsUncategorized ? 1 : -1;\n }\n\n return compareForwardTunnelNameAsc(a, b);\n};\n\nconst buildForwardGroupOrderLocalKey = (tokenUserId: number): string => {\n return `${FORWARD_GROUP_ORDER_LOCAL_STORAGE_PREFIX}:u:${tokenUserId}`;\n};\n\nconst buildForwardGroupCollapsedLocalKey = (tokenUserId: number): string => {\n return `${FORWARD_GROUP_COLLAPSED_LOCAL_STORAGE_PREFIX}:u:${tokenUserId}`;\n};\n\nconst parsePreferenceMap = (\n raw: string | null,\n): Record | null => {\n if (!raw) {\n return null;\n }\n\n try {\n const parsed = JSON.parse(raw);\n\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return null;\n }\n\n return parsed as Record;\n } catch {\n return null;\n }\n};\n\nconst parseGroupOrderMap = (raw: string | null): ForwardGroupOrderMap => {\n const parsed = parsePreferenceMap(raw);\n\n if (!parsed) {\n return {};\n }\n\n const result: ForwardGroupOrderMap = {};\n\n Object.entries(parsed).forEach(([userId, value]) => {\n if (!Array.isArray(value)) {\n return;\n }\n\n const keys = value\n .map((item) => (typeof item === \"string\" ? item.trim() : \"\"))\n .filter((item) => item !== \"\");\n\n if (keys.length > 0) {\n result[userId] = Array.from(new Set(keys));\n }\n });\n\n return result;\n};\n\nconst parseGroupCollapsedMap = (\n raw: string | null,\n): ForwardGroupCollapsedMap => {\n const parsed = parsePreferenceMap(raw);\n\n if (!parsed) {\n return {};\n }\n\n const result: ForwardGroupCollapsedMap = {};\n\n Object.entries(parsed).forEach(([key, value]) => {\n if (typeof value === \"boolean\") {\n result[key] = value;\n }\n });\n\n return result;\n};\n\nconst sanitizeGroupOrderMap = (\n source: ForwardGroupOrderMap,\n availableTunnelKeysByUser: Map>,\n): ForwardGroupOrderMap => {\n const sanitized: ForwardGroupOrderMap = {};\n\n availableTunnelKeysByUser.forEach((availableKeys, userId) => {\n if (availableKeys.size === 0) {\n return;\n }\n\n const orderFromSource = source[userId.toString()] || [];\n const used = new Set();\n const merged: string[] = [];\n\n orderFromSource.forEach((key) => {\n if (!availableKeys.has(key) || used.has(key)) {\n return;\n }\n\n used.add(key);\n merged.push(key);\n });\n\n Array.from(availableKeys)\n .sort(compareForwardTunnelGroupKeyAsc)\n .forEach((key) => {\n if (!used.has(key)) {\n used.add(key);\n merged.push(key);\n }\n });\n\n if (merged.length > 0) {\n sanitized[userId.toString()] = merged;\n }\n });\n\n return sanitized;\n};\n\nconst sanitizeGroupCollapsedMap = (\n source: ForwardGroupCollapsedMap,\n availableCollapseKeys: Set,\n): ForwardGroupCollapsedMap => {\n const sanitized: ForwardGroupCollapsedMap = {};\n\n availableCollapseKeys.forEach((key) => {\n if (source[key] === true) {\n sanitized[key] = true;\n }\n });\n\n return sanitized;\n};\n\nconst buildTunnelGroupCollapseKey = (\n userId: number,\n tunnelKey: string,\n): string => {\n return `${userId}:${tunnelKey}`;\n};\n\nconst buildTunnelGroupSortableId = (\n userId: number,\n tunnelKey: string,\n): string => {\n return `${FORWARD_TUNNEL_GROUP_SORTABLE_PREFIX}:${userId}:${tunnelKey}`;\n};\n\nconst parseTunnelGroupSortableId = (\n value: unknown,\n): { userId: number; tunnelKey: string } | null => {\n if (typeof value !== \"string\") {\n return null;\n }\n\n if (!value.startsWith(`${FORWARD_TUNNEL_GROUP_SORTABLE_PREFIX}:`)) {\n return null;\n }\n\n const parts = value.split(\":\");\n\n if (parts.length < 3) {\n return null;\n }\n\n const userId = Number(parts[1]);\n const tunnelKey = parts.slice(2).join(\":\").trim();\n\n if (!Number.isFinite(userId) || tunnelKey === \"\") {\n return null;\n }\n\n return { userId, tunnelKey };\n};\n\nconst buildAvailableGroupData = (\n forwards: Forward[],\n): {\n availableTunnelKeysByUser: Map>;\n availableCollapseKeys: Set;\n} => {\n const availableTunnelKeysByUser = new Map>();\n const availableCollapseKeys = new Set();\n\n forwards.forEach((forward) => {\n const userId = forward.userId ?? 0;\n const tunnelKey = buildForwardTunnelGroupKey(forward.tunnelName);\n\n let set = availableTunnelKeysByUser.get(userId);\n\n if (!set) {\n set = new Set();\n availableTunnelKeysByUser.set(userId, set);\n }\n\n set.add(tunnelKey);\n availableCollapseKeys.add(buildTunnelGroupCollapseKey(userId, tunnelKey));\n });\n\n return { availableTunnelKeysByUser, availableCollapseKeys };\n};\n\nconst isSameStringArray = (a: string[], b: string[]): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n\n for (let i = 0; i < a.length; i += 1) {\n if (a[i] !== b[i]) {\n return false;\n }\n }\n\n return true;\n};\n\nconst isSameGroupOrderMap = (\n a: ForwardGroupOrderMap,\n b: ForwardGroupOrderMap,\n): boolean => {\n const aKeys = Object.keys(a).sort(compareForwardTunnelNameAsc);\n const bKeys = Object.keys(b).sort(compareForwardTunnelNameAsc);\n\n if (!isSameStringArray(aKeys, bKeys)) {\n return false;\n }\n\n for (const key of aKeys) {\n if (!isSameStringArray(a[key] || [], b[key] || [])) {\n return false;\n }\n }\n\n return true;\n};\n\nconst isSameGroupCollapsedMap = (\n a: ForwardGroupCollapsedMap,\n b: ForwardGroupCollapsedMap,\n): boolean => {\n const aKeys = Object.keys(a).sort(compareForwardTunnelNameAsc);\n const bKeys = Object.keys(b).sort(compareForwardTunnelNameAsc);\n\n if (!isSameStringArray(aKeys, bKeys)) {\n return false;\n }\n\n for (const key of aKeys) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n};\n\nexport default function ForwardPage() {\n const [loading, setLoading] = useState(true);\n const [forwards, setForwards] = useState([]);\n const [tunnels, setTunnels] = useState([]);\n const [allTunnels, setAllTunnels] = useState([]);\n const [nodes, setNodes] = useState([]);\n const [speedLimits, setSpeedLimits] = useState([]);\n const isMobile = useMobileBreakpoint();\n const [searchKeyword, setSearchKeyword] = useLocalStorageState(\n \"forward-search-keyword\",\n \"\",\n );\n const [isSearchVisible, setIsSearchVisible] = useState(false);\n const [compactMode, setCompactMode] = useState(false);\n\n // 显示模式状态 - 从localStorage读取,默认为平铺显示\n const [viewMode, setViewMode] = useState<\"grouped\" | \"direct\">(() => {\n try {\n const savedMode = localStorage.getItem(\"forward-view-mode\");\n\n return (savedMode as \"grouped\" | \"direct\") || \"direct\";\n } catch {\n return \"direct\";\n }\n });\n\n // 筛选状态\n const [filterUserId, setFilterUserId, resetFilterUserId] =\n useLocalStorageState(\"forward-filter-user-id\", \"all\");\n const [filterTunnelId, setFilterTunnelId, resetFilterTunnelId] =\n useLocalStorageState(\"forward-filter-tunnel-id\", \"all\");\n\n // 拖拽排序相关状态\n const [forwardOrder, setForwardOrder] = useState([]);\n\n // 模态框状态\n const [modalOpen, setModalOpen] = useState(false);\n const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);\n const [deleteModalOpen, setDeleteModalOpen] = useState(false);\n const [addressModalOpen, setAddressModalOpen] = useState(false);\n const [diagnosisModalOpen, setDiagnosisModalOpen] = useState(false);\n const [isEdit, setIsEdit] = useState(false);\n const [submitLoading, setSubmitLoading] = useState(false);\n const [deleteLoading, setDeleteLoading] = useState(false);\n const [diagnosisLoading, setDiagnosisLoading] = useState(false);\n const [forwardToDelete, setForwardToDelete] = useState(null);\n const [currentDiagnosisForward, setCurrentDiagnosisForward] =\n useState(null);\n const [diagnosisResult, setDiagnosisResult] =\n useState(null);\n const [diagnosisProgress, setDiagnosisProgress] = useState({\n total: 0,\n completed: 0,\n success: 0,\n failed: 0,\n timedOut: false,\n });\n const diagnosisAbortRef = useRef(null);\n const [addressModalTitle, setAddressModalTitle] = useState(\"\");\n const [addressList, setAddressList] = useState([]);\n\n // 导出相关状态\n const [exportModalOpen, setExportModalOpen] = useState(false);\n const [exportData, setExportData] = useState(\"\");\n const [exportLoading, setExportLoading] = useState(false);\n const [selectedTunnelForExport, setSelectedTunnelForExport] = useState<\n number | null\n >(null);\n\n // 导入相关状态\n type ImportFormat = \"flvx\" | \"ny\";\n const [importModalOpen, setImportModalOpen] = useState(false);\n const [importData, setImportData] = useState(\"\");\n const [importLoading, setImportLoading] = useState(false);\n const [importFormat, setImportFormat] = useState(\"flvx\");\n const [selectedTunnelForImport, setSelectedTunnelForImport] = useState<\n number | null\n >(null);\n const [importResults, setImportResults] = useState<\n Array<{\n line: string;\n success: boolean;\n message: string;\n forwardName?: string;\n }>\n >([]);\n\n // 表单状态\n const [form, setForm] = useState({\n name: \"\",\n tunnelId: null,\n inPort: null,\n inIp: \"\",\n remoteAddr: \"\",\n interfaceName: \"\",\n strategy: \"fifo\",\n speedId: null,\n });\n\n // 表单验证错误\n const [errors, setErrors] = useState<{ [key: string]: string }>({});\n\n // 批量操作相关状态\n const [selectMode, setSelectMode] = useState(false);\n const [selectedIds, setSelectedIds] = useState>(new Set());\n const [batchDeleteModalOpen, setBatchDeleteModalOpen] = useState(false);\n const [batchChangeTunnelModalOpen, setBatchChangeTunnelModalOpen] =\n useState(false);\n const [batchTargetTunnelId, setBatchTargetTunnelId] = useState(\n null,\n );\n const [batchLoading, setBatchLoading] = useState(false);\n const [groupOrderMap, setGroupOrderMap] = useState({});\n const [collapsedTunnelGroups, setCollapsedTunnelGroups] =\n useState({});\n const [groupPreferenceHydrated, setGroupPreferenceHydrated] = useState(false);\n const tokenUserId = JwtUtil.getUserIdFromToken();\n const tokenRoleId = JwtUtil.getRoleIdFromToken();\n const isAdmin = tokenRoleId === 0;\n\n const parseNodeIPs = (node?: Node): string[] => {\n if (!node) {\n return [];\n }\n\n const ips: string[] = [];\n const add = (value?: string) => {\n const trimmed = (value || \"\").trim();\n\n if (trimmed) {\n ips.push(trimmed);\n }\n };\n\n add(node.serverIpV4);\n add(node.serverIpV6);\n add(node.serverIp);\n\n (node.extraIPs || \"\")\n .split(\",\")\n .map((v) => v.trim())\n .filter((v) => v)\n .forEach((v) => ips.push(v));\n\n return Array.from(new Set(ips));\n };\n\n const tunnelInIpOptionMap = useMemo(() => {\n const map = new Map();\n const nodeMap = new Map(nodes.map((n) => [n.id, n]));\n\n for (const tunnel of allTunnels) {\n const collected: string[] = [];\n const entryNodes = tunnel.inNodeId || [];\n\n for (const entry of entryNodes) {\n collected.push(...parseNodeIPs(nodeMap.get(entry.nodeId)));\n }\n\n if (collected.length === 0) {\n (tunnel.inIp || \"\")\n .split(\",\")\n .map((v) => v.trim())\n .filter((v) => v)\n .forEach((v) => collected.push(v));\n }\n\n map.set(tunnel.id, Array.from(new Set(collected)));\n }\n\n return map;\n }, [allTunnels, nodes]);\n\n const currentTunnelIpOptions = useMemo(() => {\n if (!form.tunnelId) {\n return [];\n }\n\n return tunnelInIpOptionMap.get(form.tunnelId) || [];\n }, [form.tunnelId, tunnelInIpOptionMap]);\n\n useEffect(() => {\n return () => {\n diagnosisAbortRef.current?.abort();\n diagnosisAbortRef.current = null;\n };\n }, []);\n\n const persistGroupOrderToLocal = (nextOrderMap: ForwardGroupOrderMap) => {\n if (tokenUserId === null) {\n return;\n }\n\n try {\n localStorage.setItem(\n buildForwardGroupOrderLocalKey(tokenUserId),\n JSON.stringify(nextOrderMap),\n );\n } catch {}\n };\n\n const persistGroupCollapsedToLocal = (\n nextCollapsedMap: ForwardGroupCollapsedMap,\n ) => {\n if (tokenUserId === null) {\n return;\n }\n\n try {\n localStorage.setItem(\n buildForwardGroupCollapsedLocalKey(tokenUserId),\n JSON.stringify(nextCollapsedMap),\n );\n } catch {}\n };\n\n const persistGroupOrderToGlobal = async (\n nextOrderMap: ForwardGroupOrderMap,\n ): Promise => {\n if (!isAdmin || tokenUserId === null) {\n return;\n }\n\n try {\n const currentRes = await getConfigByName(FORWARD_GROUP_ORDER_CONFIG_KEY);\n const globalMap =\n parsePreferenceMap(\n currentRes.code === 0 && typeof currentRes.data?.value === \"string\"\n ? currentRes.data.value\n : null,\n ) || {};\n\n globalMap[tokenUserId.toString()] = nextOrderMap;\n\n const saveRes = await updateConfig(\n FORWARD_GROUP_ORDER_CONFIG_KEY,\n JSON.stringify(globalMap),\n );\n\n if (saveRes.code !== 0) {\n toast.error(saveRes.msg || \"保存分组排序失败\");\n }\n } catch {\n toast.error(\"保存分组排序失败\");\n }\n };\n\n const persistGroupCollapsedToGlobal = async (\n nextCollapsedMap: ForwardGroupCollapsedMap,\n ): Promise => {\n if (!isAdmin || tokenUserId === null) {\n return;\n }\n\n try {\n const currentRes = await getConfigByName(\n FORWARD_GROUP_COLLAPSED_CONFIG_KEY,\n );\n const globalMap =\n parsePreferenceMap(\n currentRes.code === 0 && typeof currentRes.data?.value === \"string\"\n ? currentRes.data.value\n : null,\n ) || {};\n\n globalMap[tokenUserId.toString()] = nextCollapsedMap;\n\n const saveRes = await updateConfig(\n FORWARD_GROUP_COLLAPSED_CONFIG_KEY,\n JSON.stringify(globalMap),\n );\n\n if (saveRes.code !== 0) {\n toast.error(saveRes.msg || \"保存分组折叠状态失败\");\n }\n } catch {\n toast.error(\"保存分组折叠状态失败\");\n }\n };\n\n useEffect(() => {\n let cancelled = false;\n\n const loadGroupPreferences = async () => {\n if (tokenUserId === null) {\n if (!cancelled) {\n setGroupOrderMap({});\n setCollapsedTunnelGroups({});\n setGroupPreferenceHydrated(true);\n }\n\n return;\n }\n\n let localOrderMap: ForwardGroupOrderMap = {};\n let localCollapsedMap: ForwardGroupCollapsedMap = {};\n\n try {\n localOrderMap = parseGroupOrderMap(\n localStorage.getItem(buildForwardGroupOrderLocalKey(tokenUserId)),\n );\n } catch {\n localOrderMap = {};\n }\n\n try {\n localCollapsedMap = parseGroupCollapsedMap(\n localStorage.getItem(buildForwardGroupCollapsedLocalKey(tokenUserId)),\n );\n } catch {\n localCollapsedMap = {};\n }\n\n if (isAdmin) {\n try {\n const [globalOrderRes, globalCollapsedRes] = await Promise.all([\n getConfigByName(FORWARD_GROUP_ORDER_CONFIG_KEY),\n getConfigByName(FORWARD_GROUP_COLLAPSED_CONFIG_KEY),\n ]);\n\n const globalOrderMap = parsePreferenceMap(\n globalOrderRes.code === 0 &&\n typeof globalOrderRes.data?.value === \"string\"\n ? globalOrderRes.data.value\n : null,\n );\n const globalCollapsedMap =\n parsePreferenceMap(\n globalCollapsedRes.code === 0 &&\n typeof globalCollapsedRes.data?.value === \"string\"\n ? globalCollapsedRes.data.value\n : null,\n );\n\n const globalOrderBucket = globalOrderMap?.[tokenUserId.toString()];\n const globalCollapsedBucket =\n globalCollapsedMap?.[tokenUserId.toString()];\n\n if (\n globalOrderBucket &&\n typeof globalOrderBucket === \"object\" &&\n !Array.isArray(globalOrderBucket)\n ) {\n localOrderMap = parseGroupOrderMap(\n JSON.stringify(globalOrderBucket),\n );\n }\n\n if (\n globalCollapsedBucket &&\n typeof globalCollapsedBucket === \"object\" &&\n !Array.isArray(globalCollapsedBucket)\n ) {\n localCollapsedMap = parseGroupCollapsedMap(\n JSON.stringify(globalCollapsedBucket),\n );\n }\n } catch {}\n }\n\n if (cancelled) {\n return;\n }\n\n setGroupOrderMap(localOrderMap);\n setCollapsedTunnelGroups(localCollapsedMap);\n persistGroupOrderToLocal(localOrderMap);\n persistGroupCollapsedToLocal(localCollapsedMap);\n setGroupPreferenceHydrated(true);\n };\n\n setGroupPreferenceHydrated(false);\n loadGroupPreferences();\n\n return () => {\n cancelled = true;\n };\n }, [tokenUserId, isAdmin]);\n\n useEffect(() => {\n const loadForwardCompactMode = async () => {\n try {\n const response = await getConfigByName(FORWARD_COMPACT_MODE_CONFIG_KEY);\n const enabled =\n response.code === 0 &&\n typeof response.data?.value === \"string\" &&\n response.data.value === \"true\";\n\n setCompactMode(enabled);\n } catch {\n setCompactMode(false);\n }\n };\n\n const handleCompactModeChanged = (event: Event) => {\n const customEvent = event as CustomEvent<{ enabled?: boolean }>;\n\n if (typeof customEvent.detail?.enabled === \"boolean\") {\n setCompactMode(customEvent.detail.enabled);\n }\n };\n\n loadForwardCompactMode();\n window.addEventListener(\n FORWARD_COMPACT_MODE_EVENT,\n handleCompactModeChanged,\n );\n\n return () => {\n window.removeEventListener(\n FORWARD_COMPACT_MODE_EVENT,\n handleCompactModeChanged,\n );\n };\n }, []);\n\n const parseShareIdFromTunnelName = (tunnelName: string): number | null => {\n const normalized = (tunnelName || \"\").trim();\n\n if (!normalized.startsWith(\"Share-\")) {\n return null;\n }\n\n const raw = normalized.slice(\"Share-\".length);\n const idx = raw.indexOf(\"-Port-\");\n\n if (idx <= 0) {\n return null;\n }\n\n const shareId = Number(raw.slice(0, idx).trim());\n\n return Number.isFinite(shareId) && shareId > 0 ? shareId : null;\n };\n\n const mergeFederationShareFlow = async (\n forwardsData: Forward[],\n ): Promise => {\n if (forwardsData.length === 0) {\n return forwardsData;\n }\n\n try {\n const [usageRes, localShareRes] = await Promise.all([\n getPeerRemoteUsageList(),\n getPeerShareList(),\n ]);\n\n const flowByShare = new Map();\n const shareIdsByTunnel = new Map>();\n\n if (usageRes.code === 0 && Array.isArray(usageRes.data)) {\n usageRes.data.forEach((item: Record) => {\n const shareId = Number(item.shareId || 0);\n const currentFlow = Number(item.currentFlow || 0);\n\n if (\n Number.isFinite(shareId) &&\n shareId > 0 &&\n Number.isFinite(currentFlow) &&\n currentFlow > 0\n ) {\n const prev = flowByShare.get(shareId) || 0;\n\n flowByShare.set(shareId, Math.max(prev, currentFlow));\n }\n\n if (Number.isFinite(shareId) && shareId > 0) {\n const bindings = Array.isArray(item.bindings)\n ? (item.bindings as Array>)\n : [];\n\n bindings.forEach((binding) => {\n const tunnelId = Number(binding.tunnelId || 0);\n const chainType = Number(binding.chainType || 0);\n\n if (!Number.isFinite(tunnelId) || tunnelId <= 0) {\n return;\n }\n\n if (Number.isFinite(chainType) && chainType !== 1) {\n return;\n }\n\n let shareSet = shareIdsByTunnel.get(tunnelId);\n\n if (!shareSet) {\n shareSet = new Set();\n shareIdsByTunnel.set(tunnelId, shareSet);\n }\n\n shareSet.add(shareId);\n });\n }\n });\n }\n\n if (localShareRes.code === 0 && Array.isArray(localShareRes.data)) {\n localShareRes.data.forEach((item: Record) => {\n const shareId = Number(item.id || 0);\n const currentFlow = Number(item.currentFlow || 0);\n\n if (\n Number.isFinite(shareId) &&\n shareId > 0 &&\n Number.isFinite(currentFlow) &&\n currentFlow > 0\n ) {\n const prev = flowByShare.get(shareId) || 0;\n\n flowByShare.set(shareId, Math.max(prev, currentFlow));\n }\n });\n }\n\n if (flowByShare.size === 0) {\n return forwardsData;\n }\n\n const resolveShareIdForForward = (forward: Forward): number | null => {\n const candidates = new Set();\n const shareIdFromName = parseShareIdFromTunnelName(\n forward.tunnelName || \"\",\n );\n\n if (shareIdFromName) {\n candidates.add(shareIdFromName);\n }\n\n const tunnelId = Number(forward.tunnelId || 0);\n const shareSetByTunnel = shareIdsByTunnel.get(tunnelId);\n\n if (shareSetByTunnel && shareSetByTunnel.size > 0) {\n shareSetByTunnel.forEach((shareId) => {\n if (Number.isFinite(shareId) && shareId > 0) {\n candidates.add(shareId);\n }\n });\n }\n\n if (candidates.size === 0) {\n return null;\n }\n\n let bestShareId: number | null = null;\n let bestFlow = 0;\n\n candidates.forEach((shareId) => {\n const shareFlow = flowByShare.get(shareId) || 0;\n\n if (shareFlow > bestFlow) {\n bestFlow = shareFlow;\n bestShareId = shareId;\n }\n });\n\n return bestShareId;\n };\n\n const resolvedShareByForwardId = new Map();\n\n forwardsData.forEach((forward) => {\n const shareId = resolveShareIdForForward(forward);\n\n if (shareId) {\n resolvedShareByForwardId.set(forward.id, shareId);\n }\n });\n\n const forwardCountByShare = new Map();\n\n forwardsData.forEach((forward) => {\n const shareId = resolvedShareByForwardId.get(forward.id) || null;\n\n if (!shareId || !flowByShare.has(shareId)) {\n return;\n }\n\n forwardCountByShare.set(\n shareId,\n (forwardCountByShare.get(shareId) || 0) + 1,\n );\n });\n\n return forwardsData.map((forward) => {\n const shareId = resolvedShareByForwardId.get(forward.id) || null;\n\n if (!shareId) {\n return { ...forward, federationShareFlow: undefined };\n }\n\n const shareFlow = flowByShare.get(shareId) || 0;\n\n if (shareFlow <= 0) {\n return { ...forward, federationShareFlow: undefined };\n }\n\n const directFlow = (forward.inFlow || 0) + (forward.outFlow || 0);\n\n if (directFlow > 0) {\n return { ...forward, federationShareFlow: undefined };\n }\n\n const count = forwardCountByShare.get(shareId) || 1;\n const estimated = Math.max(1, Math.floor(shareFlow / count));\n\n return { ...forward, federationShareFlow: estimated };\n });\n } catch {\n return forwardsData;\n }\n };\n\n const getForwardDisplayFlow = (forward: Forward): number => {\n const directFlow = (forward.inFlow || 0) + (forward.outFlow || 0);\n\n if (directFlow > 0) {\n return directFlow;\n }\n\n return forward.federationShareFlow || 0;\n };\n\n useEffect(() => {\n loadData();\n }, []);\n\n // 切换显示模式并保存到localStorage\n const handleViewModeChange = () => {\n const newMode = viewMode === \"grouped\" ? \"direct\" : \"grouped\";\n\n setViewMode(newMode);\n try {\n localStorage.setItem(\"forward-view-mode\", newMode);\n } catch {}\n };\n\n // 加载所有数据\n const loadData = async (lod = true) => {\n setLoading(lod);\n try {\n const [forwardsRes, tunnelsRes, speedLimitsRes] = await Promise.all([\n getForwardList(),\n userTunnel(),\n getSpeedLimitList(),\n ]);\n const [allTunnelsRes, nodesRes] = await Promise.allSettled([\n getTunnelList(),\n getNodeList(),\n ]);\n\n if (forwardsRes.code === 0) {\n const forwardsData =\n forwardsRes.data?.map((forward: any) => ({\n ...forward,\n serviceRunning: forward.status === 1,\n })) || [];\n\n const mergedForwards = await mergeFederationShareFlow(forwardsData);\n\n setForwards(mergedForwards);\n\n // 初始化拖拽排序顺序\n const currentUserId = JwtUtil.getUserIdFromToken();\n const { order, fromDatabase } = buildForwardOrder(\n mergedForwards,\n currentUserId,\n );\n\n setForwardOrder(order);\n\n if (fromDatabase) {\n saveOrder(FORWARD_ORDER_KEY, order);\n }\n } else {\n toast.error(forwardsRes.msg || \"获取转发列表失败\");\n }\n\n if (tunnelsRes.code === 0) {\n setTunnels(tunnelsRes.data || []);\n } else {\n }\n\n if (\n allTunnelsRes.status === \"fulfilled\" &&\n allTunnelsRes.value.code === 0\n ) {\n setAllTunnels((allTunnelsRes.value.data || []) as Tunnel[]);\n }\n\n if (nodesRes.status === \"fulfilled\" && nodesRes.value.code === 0) {\n setNodes((nodesRes.value.data || []) as Node[]);\n }\n\n if (speedLimitsRes.code === 0) {\n setSpeedLimits(speedLimitsRes.data || []);\n }\n } catch {\n toast.error(\"加载数据失败\");\n } finally {\n setLoading(false);\n }\n };\n\n // 表单验证\n const noLimitSpeedLimitIds = useMemo(() => {\n return new Set(\n speedLimits\n .filter((speedLimit) => speedLimit.name.trim() === \"不限速\")\n .map((speedLimit) => speedLimit.id),\n );\n }, [speedLimits]);\n\n const availableSpeedLimits = useMemo(() => {\n return speedLimits.filter(\n (speedLimit) => !noLimitSpeedLimitIds.has(speedLimit.id),\n );\n }, [speedLimits, noLimitSpeedLimitIds]);\n\n const normalizeSpeedId = (speedId?: number | null): number | null => {\n if (speedId === null || speedId === undefined) {\n return null;\n }\n\n return noLimitSpeedLimitIds.has(speedId) ? null : speedId;\n };\n\n const selectedSpeedId = normalizeSpeedId(form.speedId);\n\n const validateForm = (): boolean => {\n const newErrors: { [key: string]: string } = {};\n\n if (!form.name.trim()) {\n newErrors.name = \"请输入转发名称\";\n } else if (form.name.length < 2 || form.name.length > 50) {\n newErrors.name = \"转发名称长度应在2-50个字符之间\";\n }\n\n if (!form.tunnelId) {\n newErrors.tunnelId = \"请选择关联隧道\";\n }\n\n // 验证入口端口(可选,如果填写则验证)\n if (form.inPort !== null && form.inPort !== undefined) {\n const port = Number(form.inPort);\n\n if (isNaN(port) || port < 1 || port > 65535) {\n newErrors.inPort = \"端口必须在 1-65535 之间\";\n }\n }\n\n if (!form.remoteAddr.trim()) {\n newErrors.remoteAddr = \"请输入远程地址\";\n } else {\n // 验证地址格式\n const addresses = form.remoteAddr\n .split(\"\\n\")\n .map((addr) => addr.trim())\n .filter((addr) => addr);\n const ipv4Pattern =\n /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):\\d+$/;\n const ipv6FullPattern =\n /^\\[((([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:))|(([0-9a-fA-F]{1,4}:){6}(:[0-9a-fA-F]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9a-fA-F]{1,4}:){5}(((:[0-9a-fA-F]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9a-fA-F]{1,4}:){4}(((:[0-9a-fA-F]{1,4}){1,3})|((:[0-9a-fA-F]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){3}(((:[0-9a-fA-F]{1,4}){1,4})|((:[0-9a-fA-F]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){2}(((:[0-9a-fA-F]{1,4}){1,5})|((:[0-9a-fA-F]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){1}(((:[0-9a-fA-F]{1,4}){1,6})|((:[0-9a-fA-F]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9a-fA-F]{1,4}){1,7})|((:[0-9a-fA-F]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))\\]:\\d+$/;\n const domainPattern =\n /^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*:\\d+$/;\n\n for (let i = 0; i < addresses.length; i++) {\n const addr = addresses[i];\n\n if (\n !ipv4Pattern.test(addr) &&\n !ipv6FullPattern.test(addr) &&\n !domainPattern.test(addr)\n ) {\n newErrors.remoteAddr = `第${i + 1}行地址格式错误`;\n break;\n }\n }\n }\n\n setErrors(newErrors);\n\n return Object.keys(newErrors).length === 0;\n };\n\n // 新增转发\n const handleAdd = () => {\n setIsEdit(false);\n setForm({\n name: \"\",\n tunnelId: null,\n inPort: null,\n inIp: \"\",\n remoteAddr: \"\",\n interfaceName: \"\",\n strategy: \"fifo\",\n speedId: null,\n });\n setErrors({});\n setModalOpen(true);\n };\n\n // 编辑转发\n const handleEdit = (forward: Forward) => {\n setIsEdit(true);\n setForm({\n id: forward.id,\n userId: forward.userId,\n name: forward.name,\n tunnelId: forward.tunnelId,\n inPort: forward.inPort,\n inIp: forward.inIp || \"\",\n remoteAddr: forward.remoteAddr.split(\",\").join(\"\\n\"),\n interfaceName: forward.interfaceName || \"\",\n strategy: forward.strategy || \"fifo\",\n speedId: normalizeSpeedId(forward.speedId),\n });\n setErrors({});\n setModalOpen(true);\n };\n\n // 显示删除确认\n const handleDelete = (forward: Forward) => {\n setForwardToDelete(forward);\n setDeleteModalOpen(true);\n };\n\n // 确认删除转发\n const confirmDelete = async () => {\n if (!forwardToDelete) return;\n\n setDeleteLoading(true);\n try {\n const res = await deleteForward(forwardToDelete.id);\n\n if (res.code === 0) {\n toast.success(\"删除成功\");\n setDeleteModalOpen(false);\n loadData();\n } else {\n // 删除失败,询问是否强制删除\n const confirmed = window.confirm(\n `常规删除失败:${res.msg || \"删除失败\"}\\n\\n是否需要强制删除?\\n\\n⚠️ 注意:强制删除不会去验证节点端是否已经删除对应的转发服务。`,\n );\n\n if (confirmed) {\n const forceRes = await forceDeleteForward(forwardToDelete.id);\n\n if (forceRes.code === 0) {\n toast.success(\"强制删除成功\");\n setDeleteModalOpen(false);\n loadData();\n } else {\n toast.error(forceRes.msg || \"强制删除失败\");\n }\n }\n }\n } catch {\n toast.error(\"删除失败\");\n } finally {\n setDeleteLoading(false);\n }\n };\n\n // 处理隧道选择变化\n const handleTunnelChange = (tunnelId: string) => {\n const nextTunnelId = parseInt(tunnelId);\n const options = tunnelInIpOptionMap.get(nextTunnelId) || [];\n\n setForm((prev) => ({\n ...prev,\n tunnelId: nextTunnelId,\n inIp: options.includes(prev.inIp) ? prev.inIp : \"\",\n }));\n };\n\n // 提交表单\n const handleSubmit = async () => {\n if (!validateForm()) return;\n\n setSubmitLoading(true);\n try {\n const processedRemoteAddr = form.remoteAddr\n .split(\"\\n\")\n .map((addr) => addr.trim())\n .filter((addr) => addr)\n .join(\",\");\n\n const addressCount = processedRemoteAddr.split(\",\").length;\n\n let res: { code: number; msg: string };\n\n if (isEdit) {\n // 更新时确保包含必要字段\n const updateData = {\n id: form.id,\n userId: form.userId,\n name: form.name,\n tunnelId: form.tunnelId,\n inPort: form.inPort,\n inIp: form.inIp || undefined,\n remoteAddr: processedRemoteAddr,\n strategy: addressCount > 1 ? form.strategy : \"fifo\",\n speedId: normalizeSpeedId(form.speedId),\n };\n\n res = await updateForward(updateData);\n } else {\n const createData = {\n name: form.name,\n tunnelId: form.tunnelId,\n inPort: form.inPort,\n inIp: form.inIp || undefined,\n remoteAddr: processedRemoteAddr,\n strategy: addressCount > 1 ? form.strategy : \"fifo\",\n speedId: normalizeSpeedId(form.speedId),\n };\n\n res = await createForward(createData);\n }\n\n if (res.code === 0) {\n toast.success(isEdit ? \"修改成功\" : \"创建成功\");\n setModalOpen(false);\n loadData();\n } else {\n toast.error(res.msg || \"操作失败\");\n }\n } catch {\n toast.error(\"操作失败\");\n } finally {\n setSubmitLoading(false);\n }\n };\n\n // 处理服务开关\n const handleServiceToggle = async (forward: Forward) => {\n if (forward.status !== 1 && forward.status !== 0) {\n toast.error(\"转发状态异常,无法操作\");\n\n return;\n }\n\n const targetState = !forward.serviceRunning;\n\n try {\n // 乐观更新UI\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, serviceRunning: targetState } : f,\n ),\n );\n\n let res: { code: number; msg: string };\n\n if (targetState) {\n res = await resumeForwardService(forward.id);\n } else {\n res = await pauseForwardService(forward.id);\n }\n\n if (res.code === 0) {\n toast.success(targetState ? \"服务已启动\" : \"服务已暂停\");\n // 更新转发状态\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, status: targetState ? 1 : 0 } : f,\n ),\n );\n } else {\n // 操作失败,恢复UI状态\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, serviceRunning: !targetState } : f,\n ),\n );\n toast.error(res.msg || \"操作失败\");\n }\n } catch {\n // 操作失败,恢复UI状态\n setForwards((prev) =>\n prev.map((f) =>\n f.id === forward.id ? { ...f, serviceRunning: !targetState } : f,\n ),\n );\n toast.error(\"网络错误,操作失败\");\n }\n };\n\n // 诊断转发\n const handleDiagnose = async (forward: Forward) => {\n diagnosisAbortRef.current?.abort();\n const abortController = new AbortController();\n\n diagnosisAbortRef.current = abortController;\n\n setCurrentDiagnosisForward(forward);\n setDiagnosisModalOpen(true);\n setDiagnosisLoading(true);\n setDiagnosisProgress({\n total: 0,\n completed: 0,\n success: 0,\n failed: 0,\n timedOut: false,\n });\n setDiagnosisResult({\n forwardName: forward.name,\n timestamp: Date.now(),\n results: [],\n });\n\n try {\n let streamErrorMessage = \"\";\n const streamResult = await diagnoseForwardStream(\n forward.id,\n {\n onStart: (payload) => {\n const startForwardName =\n typeof payload.forwardName === \"string\" &&\n payload.forwardName.trim() !== \"\"\n ? payload.forwardName\n : forward.name;\n const startTotal = Number(payload.total);\n const startItems = Array.isArray(payload.items)\n ? (payload.items as ForwardDiagnosisResult[\"results\"])\n : [];\n\n setDiagnosisResult((prev) => ({\n forwardName: startForwardName,\n timestamp: Date.now(),\n results: startItems.length > 0 ? startItems : prev?.results || [],\n }));\n if (Number.isFinite(startTotal) && startTotal >= 0) {\n setDiagnosisProgress((prev) => ({\n ...prev,\n total: startTotal,\n }));\n }\n },\n onItem: ({ result, progress }) => {\n setDiagnosisResult((prev) => {\n const base: ForwardDiagnosisResult = prev || {\n forwardName: forward.name,\n timestamp: Date.now(),\n results: [],\n };\n const nextResults = [...base.results];\n const existingIndex = nextResults.findIndex(\n (item) =>\n item.description === result.description &&\n item.nodeId === result.nodeId &&\n item.targetIp === result.targetIp &&\n item.targetPort === result.targetPort,\n );\n\n if (existingIndex >= 0) {\n nextResults[existingIndex] = {\n ...result,\n diagnosing: false,\n };\n } else {\n nextResults.push({\n ...result,\n diagnosing: false,\n });\n }\n\n return {\n ...base,\n timestamp: Date.now(),\n results: nextResults,\n };\n });\n setDiagnosisProgress({\n total: progress.total,\n completed: progress.completed,\n success: progress.success,\n failed: progress.failed,\n timedOut: Boolean(progress.timedOut),\n });\n },\n onDone: (progress) => {\n setDiagnosisProgress({\n total: progress.total,\n completed: progress.completed,\n success: progress.success,\n failed: progress.failed,\n timedOut: Boolean(progress.timedOut),\n });\n },\n onError: (message) => {\n streamErrorMessage = message;\n },\n },\n abortController.signal,\n );\n\n if (streamResult.fallback) {\n const response = await diagnoseForward(forward.id);\n\n if (response.code === 0) {\n const resultData = response.data as ForwardDiagnosisResult;\n const successCount = resultData.results.filter(\n (r) => r.success,\n ).length;\n const failedCount = resultData.results.length - successCount;\n\n setDiagnosisResult(resultData);\n setDiagnosisProgress({\n total: resultData.results.length,\n completed: resultData.results.length,\n success: successCount,\n failed: failedCount,\n timedOut: false,\n });\n } else {\n toast.error(response.msg || \"诊断失败\");\n setDiagnosisResult(\n buildForwardDiagnosisFallbackResult({\n forwardName: forward.name,\n remoteAddr: forward.remoteAddr,\n description: \"诊断失败\",\n message: response.msg || \"诊断过程中发生错误\",\n }),\n );\n setDiagnosisProgress({\n total: 1,\n completed: 1,\n success: 0,\n failed: 1,\n timedOut: false,\n });\n }\n\n return;\n }\n\n if (streamErrorMessage) {\n toast.error(streamErrorMessage);\n }\n if (streamResult.timedOut) {\n toast.error(\"诊断超时(单条30秒 / 整体2分钟),已返回当前结果\");\n }\n } catch {\n if (abortController.signal.aborted) {\n return;\n }\n toast.error(\"网络错误,请重试\");\n setDiagnosisResult(\n buildForwardDiagnosisFallbackResult({\n forwardName: forward.name,\n remoteAddr: forward.remoteAddr,\n description: \"网络错误\",\n message: \"无法连接到服务器\",\n }),\n );\n setDiagnosisProgress({\n total: 1,\n completed: 1,\n success: 0,\n failed: 1,\n timedOut: false,\n });\n } finally {\n if (diagnosisAbortRef.current === abortController) {\n diagnosisAbortRef.current = null;\n }\n setDiagnosisLoading(false);\n }\n };\n\n // 格式化流量\n const formatFlow = (value: number): string => {\n if (value === 0) return \"0 B\";\n if (value < 1024) return value + \" B\";\n if (value < 1024 * 1024) return (value / 1024).toFixed(2) + \" KB\";\n if (value < 1024 * 1024 * 1024)\n return (value / (1024 * 1024)).toFixed(2) + \" MB\";\n\n return (value / (1024 * 1024 * 1024)).toFixed(2) + \" GB\";\n };\n\n // 显示地址列表弹窗\n const showAddressModal = (\n addressString: string,\n port: number | null,\n title: string,\n ) => {\n const action = resolveForwardAddressAction(addressString, port, title);\n\n if (action.type === \"none\") {\n return;\n }\n\n if (action.type === \"copy\") {\n copyToClipboard(action.text, action.label);\n\n return;\n }\n\n setAddressList(action.items);\n setAddressModalTitle(action.title);\n setAddressModalOpen(true);\n };\n\n // 复制到剪贴板\n const copyToClipboard = async (text: string, label: string = \"内容\") => {\n try {\n await navigator.clipboard.writeText(text);\n toast.success(`已复制${label}`);\n } catch {\n toast.error(\"复制失败\");\n }\n };\n\n // 复制地址\n const copyAddress = async (addressItem: ForwardAddressItem) => {\n try {\n setAddressList((prev) =>\n prev.map((item) =>\n item.id === addressItem.id ? { ...item, copying: true } : item,\n ),\n );\n await copyToClipboard(addressItem.address, \"地址\");\n } catch {\n toast.error(\"复制失败\");\n } finally {\n setAddressList((prev) =>\n prev.map((item) =>\n item.id === addressItem.id ? { ...item, copying: false } : item,\n ),\n );\n }\n };\n\n // 复制所有地址\n const copyAllAddresses = async () => {\n if (addressList.length === 0) return;\n const allAddresses = addressList.map((item) => item.address).join(\"\\n\");\n\n await copyToClipboard(allAddresses, \"所有地址\");\n };\n\n // 导出转发数据\n const handleExport = () => {\n setSelectedTunnelForExport(null);\n setExportData(\"\");\n setExportModalOpen(true);\n };\n\n // 执行导出\n const executeExport = () => {\n if (!selectedTunnelForExport) {\n toast.error(\"请选择要导出的隧道\");\n\n return;\n }\n\n setExportLoading(true);\n\n try {\n // 获取要导出的转发列表\n const forwardsToExport = sortedForwards.filter(\n (forward) => forward.tunnelId === selectedTunnelForExport,\n );\n\n if (forwardsToExport.length === 0) {\n toast.error(\"所选隧道没有转发数据\");\n setExportLoading(false);\n\n return;\n }\n\n // 格式化导出数据:remoteAddr|name|inPort\n const exportLines = forwardsToExport.map((forward) => {\n return `${forward.remoteAddr}|${forward.name}|${forward.inPort}`;\n });\n\n const exportText = exportLines.join(\"\\n\");\n\n setExportData(exportText);\n } catch {\n toast.error(\"导出失败\");\n } finally {\n setExportLoading(false);\n }\n };\n\n // 复制导出数据\n const copyExportData = async () => {\n await copyToClipboard(exportData, \"转发数据\");\n };\n\n // 导入转发数据\n const handleImport = () => {\n setImportData(\"\");\n setImportResults([]);\n setSelectedTunnelForImport(null);\n setImportModalOpen(true);\n };\n\n // 执行导入\n const executeImport = async () => {\n if (!importData.trim()) {\n toast.error(\"请输入要导入的数据\");\n\n return;\n }\n\n if (!selectedTunnelForImport) {\n toast.error(\"请选择要导入的隧道\");\n\n return;\n }\n\n setImportLoading(true);\n setImportResults([]);\n\n try {\n if (importFormat === \"ny\") {\n const parsedItems = parseNyFormatData(importData);\n\n if (parsedItems.length === 0) {\n toast.error(\"未解析到有效的ny格式数据\");\n\n setImportLoading(false);\n\n return;\n }\n\n for (const item of parsedItems) {\n if (item.error) {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: item.error || \"解析失败\",\n },\n ...prev,\n ]);\n\n continue;\n }\n\n if (!item.parsed) {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: \"解析失败\",\n },\n ...prev,\n ]);\n\n continue;\n }\n\n const parsedNyItem = item.parsed;\n const nyForwardInput = convertNyItemToForwardInput(parsedNyItem);\n\n try {\n const response = await createForward({\n name: nyForwardInput.name,\n tunnelId: selectedTunnelForImport,\n inPort: nyForwardInput.inPort,\n remoteAddr: nyForwardInput.remoteAddr,\n strategy: nyForwardInput.strategy,\n });\n\n if (response.code === 0) {\n setImportResults((prev) => [\n {\n line: item.line,\n success: true,\n message: `创建成功 (${parsedNyItem.dest.length}个目标)`,\n forwardName: nyForwardInput.name,\n },\n ...prev,\n ]);\n } else {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: response.msg || \"创建失败\",\n },\n ...prev,\n ]);\n }\n } catch {\n setImportResults((prev) => [\n {\n line: item.line,\n success: false,\n message: \"网络错误,创建失败\",\n },\n ...prev,\n ]);\n }\n }\n } else {\n const lines = importData\n .trim()\n .split(\"\\n\")\n .filter((line) => line.trim());\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n const parts = line.split(\"|\");\n\n if (parts.length < 2) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"格式错误:需要至少包含目标地址和转发名称\",\n },\n ...prev,\n ]);\n continue;\n }\n\n const [remoteAddr, name, inPort] = parts;\n\n if (!remoteAddr.trim() || !name.trim()) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"目标地址和转发名称不能为空\",\n },\n ...prev,\n ]);\n continue;\n }\n\n const addresses = remoteAddr.trim().split(\",\");\n const addressPattern = /^[^:]+:\\d+$/;\n const isValidFormat = addresses.every((addr) =>\n addressPattern.test(addr.trim()),\n );\n\n if (!isValidFormat) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message:\n \"目标地址格式错误,应为 地址:端口 格式,多个地址用逗号分隔\",\n },\n ...prev,\n ]);\n continue;\n }\n\n try {\n let portNumber: number | null = null;\n\n if (inPort && inPort.trim()) {\n const port = parseInt(inPort.trim());\n\n if (isNaN(port) || port < 1 || port > 65535) {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"入口端口格式错误,应为1-65535之间的数字\",\n },\n ...prev,\n ]);\n continue;\n }\n portNumber = port;\n }\n\n const response = await createForward({\n name: name.trim(),\n tunnelId: selectedTunnelForImport,\n inPort: portNumber,\n remoteAddr: remoteAddr.trim(),\n strategy: \"fifo\",\n });\n\n if (response.code === 0) {\n setImportResults((prev) => [\n {\n line,\n success: true,\n message: \"创建成功\",\n forwardName: name.trim(),\n },\n ...prev,\n ]);\n } else {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: response.msg || \"创建失败\",\n },\n ...prev,\n ]);\n }\n } catch {\n setImportResults((prev) => [\n {\n line,\n success: false,\n message: \"网络错误,创建失败\",\n },\n ...prev,\n ]);\n }\n }\n }\n\n toast.success(\"导入执行完成\");\n\n await loadData(false);\n } catch {\n toast.error(\"导入过程中发生错误\");\n } finally {\n setImportLoading(false);\n }\n };\n\n // 获取状态显示\n const getStatusDisplay = (status: number) => {\n switch (status) {\n case 1:\n return { color: \"success\", text: \"正常\" };\n case 0:\n return { color: \"warning\", text: \"暂停\" };\n case -1:\n return { color: \"danger\", text: \"异常\" };\n default:\n return { color: \"default\", text: \"未知\" };\n }\n };\n\n // 获取策略显示\n const getStrategyDisplay = (strategy: string) => {\n switch (strategy) {\n case \"fifo\":\n return { color: \"primary\", text: \"主备\" };\n case \"round\":\n return { color: \"success\", text: \"轮询\" };\n case \"rand\":\n return { color: \"warning\", text: \"随机\" };\n default:\n return { color: \"default\", text: \"未知\" };\n }\n };\n\n // 获取地址数量\n const getAddressCount = (addressString: string): number => {\n if (!addressString) return 0;\n const addresses = addressString\n .split(\"\\n\")\n .map((addr) => addr.trim())\n .filter((addr) => addr);\n\n return addresses.length;\n };\n\n // 处理拖拽结束\n const handleDragEnd = async (event: DragEndEvent) => {\n const { active, over } = event;\n\n if (!active || !over || active.id === over.id) return;\n\n const activeGroup = parseTunnelGroupSortableId(active.id);\n const overGroup = parseTunnelGroupSortableId(over.id);\n\n if (activeGroup && overGroup) {\n if (compactMode || !groupPreferenceHydrated) {\n return;\n }\n\n if (activeGroup.userId !== overGroup.userId) {\n return;\n }\n\n const userIdKey = activeGroup.userId.toString();\n const currentOrder = groupOrderMap[userIdKey] || [];\n const oldIndex = currentOrder.indexOf(activeGroup.tunnelKey);\n const newIndex = currentOrder.indexOf(overGroup.tunnelKey);\n\n if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) {\n return;\n }\n\n const moved = arrayMove(currentOrder, oldIndex, newIndex);\n const nextOrderMap: ForwardGroupOrderMap = {\n ...groupOrderMap,\n [userIdKey]: moved,\n };\n\n setGroupOrderMap(nextOrderMap);\n persistGroupOrderToLocal(nextOrderMap);\n void persistGroupOrderToGlobal(nextOrderMap);\n\n return;\n }\n\n // 确保 forwardOrder 存在且有效\n if (!forwardOrder || forwardOrder.length === 0) return;\n\n const activeId = Number(active.id);\n const overId = Number(over.id);\n\n // 检查 ID 是否有效\n if (isNaN(activeId) || isNaN(overId)) return;\n\n const activeForward = forwards.find((forward) => forward.id === activeId);\n const overForward = forwards.find((forward) => forward.id === overId);\n const activeUserId = activeForward?.userId ?? 0;\n const overUserId = overForward?.userId ?? 0;\n const activeTunnelGroupKey = buildForwardTunnelGroupKey(\n activeForward?.tunnelName,\n );\n const overTunnelGroupKey = buildForwardTunnelGroupKey(\n overForward?.tunnelName,\n );\n\n // 非精简模式仅允许在同一用户+隧道分组内拖拽,避免混排\n if (!compactMode) {\n if (\n activeUserId !== overUserId ||\n activeTunnelGroupKey !== overTunnelGroupKey\n ) {\n return;\n }\n }\n\n const oldIndex = forwardOrder.indexOf(activeId);\n const newIndex = forwardOrder.indexOf(overId);\n\n if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {\n const newOrder = arrayMove(forwardOrder, oldIndex, newIndex);\n\n setForwardOrder(newOrder);\n\n saveOrder(FORWARD_ORDER_KEY, newOrder);\n\n // 持久化到数据库\n try {\n const forwardsToUpdate = newOrder.map((id, index) => ({\n id,\n inx: index,\n }));\n\n const response = await updateForwardOrder({\n forwards: forwardsToUpdate,\n });\n\n if (response.code === 0) {\n // 更新本地数据中的 inx 字段\n setForwards((prev) =>\n prev.map((forward) => {\n const updatedForward = forwardsToUpdate.find(\n (f) => f.id === forward.id,\n );\n\n if (updatedForward) {\n return { ...forward, inx: updatedForward.inx };\n }\n\n return forward;\n }),\n );\n } else {\n toast.error(\"保存排序失败:\" + (response.msg || \"未知错误\"));\n }\n } catch {\n toast.error(\"保存排序失败,请重试\");\n }\n }\n };\n\n const toggleSelectMode = () => {\n setSelectMode(!selectMode);\n if (selectMode) {\n setSelectedIds(new Set());\n }\n };\n\n const toggleSelect = (id: number) => {\n const newSet = new Set(selectedIds);\n\n if (newSet.has(id)) {\n newSet.delete(id);\n } else {\n newSet.add(id);\n }\n setSelectedIds(newSet);\n };\n\n const selectAll = () => {\n const allIds = sortedForwards.map((f) => f.id);\n\n setSelectedIds(new Set(allIds));\n };\n\n const deselectAll = () => {\n setSelectedIds(new Set());\n };\n\n const handleBatchDelete = async () => {\n if (selectedIds.size === 0) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchDelete(Array.from(selectedIds));\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n if (outcome.closeDeleteModal) {\n setBatchDeleteModalOpen(false);\n }\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n const handleBatchToggleService = async (enable: boolean) => {\n if (selectedIds.size === 0) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchToggleService(\n Array.from(selectedIds),\n enable,\n );\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n const handleBatchRedeploy = async () => {\n if (selectedIds.size === 0) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchRedeploy(\n Array.from(selectedIds),\n );\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n const handleBatchChangeTunnel = async () => {\n if (selectedIds.size === 0 || !batchTargetTunnelId) return;\n setBatchLoading(true);\n try {\n const outcome = await executeForwardBatchChangeTunnel(\n Array.from(selectedIds),\n batchTargetTunnelId,\n );\n\n if (outcome.toastVariant === \"success\") {\n toast.success(outcome.toastMessage);\n } else {\n toast.error(outcome.toastMessage);\n }\n\n if (outcome.shouldRefresh) {\n setSelectedIds(new Set());\n setSelectMode(false);\n if (outcome.closeChangeTunnelModal) {\n setBatchChangeTunnelModalOpen(false);\n }\n if (outcome.resetTargetTunnel) {\n setBatchTargetTunnelId(null);\n }\n loadData(false);\n }\n } finally {\n setBatchLoading(false);\n }\n };\n\n // 传感器配置 - 使用默认配置避免错误\n const sensors = useSensors(\n useSensor(MouseSensor, {\n activationConstraint: {\n distance: 8,\n },\n }),\n useSensor(TouchSensor, {\n activationConstraint: {\n delay: 250,\n tolerance: 8,\n },\n }),\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n }),\n );\n\n // 根据排序顺序获取转发列表\n const orderedForwards = useMemo((): Forward[] => {\n // 确保 forwards 数组存在且有效\n if (!forwards || forwards.length === 0) {\n return [];\n }\n\n let filteredForwards = forwards;\n\n if (filterUserId !== \"all\") {\n const targetUserId = parseInt(filterUserId);\n\n filteredForwards = filteredForwards.filter(\n (f) => f.userId === targetUserId || (targetUserId === 0 && !f.userId),\n );\n }\n if (filterTunnelId !== \"all\") {\n const targetTunnelId = parseInt(filterTunnelId);\n\n filteredForwards = filteredForwards.filter(\n (f) => f.tunnelId === targetTunnelId,\n );\n }\n\n if (searchKeyword.trim()) {\n const lowerKeyword = searchKeyword.toLowerCase();\n\n filteredForwards = filteredForwards.filter(\n (f) =>\n (f.name && f.name.toLowerCase().includes(lowerKeyword)) ||\n (f.remoteAddr && f.remoteAddr.toLowerCase().includes(lowerKeyword)) ||\n (f.userName && f.userName.toLowerCase().includes(lowerKeyword)),\n );\n }\n\n // 确保过滤后的转发列表有效\n if (!filteredForwards || filteredForwards.length === 0) {\n return [];\n }\n\n // 优先使用数据库中的 inx 字段进行排序\n const sortedByDb = [...filteredForwards].sort((a, b) => {\n const aInx = a.inx ?? 0;\n const bInx = b.inx ?? 0;\n\n if (aInx !== bInx) {\n return aInx - bInx;\n }\n\n return (a.id ?? 0) - (b.id ?? 0);\n });\n\n // 如果数据库中没有排序信息,则使用本地存储的顺序\n if (\n forwardOrder &&\n forwardOrder.length > 0 &&\n sortedByDb.every((f) => f.inx === undefined || f.inx === 0)\n ) {\n const forwardMap = new Map(filteredForwards.map((f) => [f.id, f]));\n const localSortedForwards: Forward[] = [];\n\n forwardOrder.forEach((id) => {\n const forward = forwardMap.get(id);\n\n if (forward) {\n localSortedForwards.push(forward);\n }\n });\n\n // 添加不在排序列表中的转发(新添加的)\n filteredForwards.forEach((forward) => {\n if (!forwardOrder.includes(forward.id)) {\n localSortedForwards.push(forward);\n }\n });\n\n return localSortedForwards;\n }\n\n return sortedByDb;\n }, [forwards, forwardOrder, filterUserId, filterTunnelId, searchKeyword]);\n\n const availableGroupData = useMemo(\n () => buildAvailableGroupData(forwards),\n [forwards],\n );\n\n const sanitizedGroupOrderMap = useMemo(\n () =>\n sanitizeGroupOrderMap(\n groupOrderMap,\n availableGroupData.availableTunnelKeysByUser,\n ),\n [groupOrderMap, availableGroupData],\n );\n\n const sanitizedCollapsedTunnelGroups = useMemo(\n () =>\n sanitizeGroupCollapsedMap(\n collapsedTunnelGroups,\n availableGroupData.availableCollapseKeys,\n ),\n [collapsedTunnelGroups, availableGroupData],\n );\n\n useEffect(() => {\n if (!groupPreferenceHydrated || tokenUserId === null) {\n return;\n }\n\n if (!isSameGroupOrderMap(groupOrderMap, sanitizedGroupOrderMap)) {\n setGroupOrderMap(sanitizedGroupOrderMap);\n persistGroupOrderToLocal(sanitizedGroupOrderMap);\n void persistGroupOrderToGlobal(sanitizedGroupOrderMap);\n }\n\n if (\n !isSameGroupCollapsedMap(\n collapsedTunnelGroups,\n sanitizedCollapsedTunnelGroups,\n )\n ) {\n setCollapsedTunnelGroups(sanitizedCollapsedTunnelGroups);\n persistGroupCollapsedToLocal(sanitizedCollapsedTunnelGroups);\n void persistGroupCollapsedToGlobal(sanitizedCollapsedTunnelGroups);\n }\n }, [\n groupPreferenceHydrated,\n tokenUserId,\n groupOrderMap,\n sanitizedGroupOrderMap,\n collapsedTunnelGroups,\n sanitizedCollapsedTunnelGroups,\n ]);\n\n const groupedForwards = useMemo((): ForwardUserGroup[] => {\n if (orderedForwards.length === 0) {\n return [];\n }\n\n type MutableForwardUserGroup = {\n userId: number;\n userName: string;\n tunnelMap: Map;\n };\n\n const userGroupMap = new Map();\n\n orderedForwards.forEach((forward) => {\n const userId = forward.userId ?? 0;\n const userName = normalizeForwardUserName(forward.userName);\n const tunnelName = normalizeForwardTunnelName(forward.tunnelName);\n const tunnelKey = buildForwardTunnelGroupKey(forward.tunnelName);\n\n let existingGroup = userGroupMap.get(userId);\n\n if (!existingGroup) {\n existingGroup = {\n userId,\n userName,\n tunnelMap: new Map(),\n };\n userGroupMap.set(userId, existingGroup);\n } else if (\n existingGroup.userName === UNKNOWN_FORWARD_USER_NAME &&\n userName !== UNKNOWN_FORWARD_USER_NAME\n ) {\n existingGroup.userName = userName;\n }\n\n const existingTunnelGroup = existingGroup.tunnelMap.get(tunnelKey);\n\n if (!existingTunnelGroup) {\n existingGroup.tunnelMap.set(tunnelKey, {\n tunnelKey,\n tunnelName,\n items: [forward],\n });\n\n return;\n }\n\n existingTunnelGroup.items.push(forward);\n\n if (\n existingTunnelGroup.tunnelName === UNCATEGORIZED_FORWARD_TUNNEL_NAME &&\n tunnelName !== UNCATEGORIZED_FORWARD_TUNNEL_NAME\n ) {\n existingTunnelGroup.tunnelName = tunnelName;\n }\n });\n\n const groups = Array.from(userGroupMap.values()).map((group) => {\n const tunnels = Array.from(group.tunnelMap.values());\n const tunnelOrder = sanitizedGroupOrderMap[group.userId.toString()] || [];\n const tunnelOrderIndex = new Map();\n\n tunnelOrder.forEach((key, index) => {\n tunnelOrderIndex.set(key, index);\n });\n\n tunnels.sort((a, b) => {\n const aIndex = tunnelOrderIndex.get(a.tunnelKey);\n const bIndex = tunnelOrderIndex.get(b.tunnelKey);\n\n if (aIndex !== undefined || bIndex !== undefined) {\n if (aIndex === undefined) {\n return 1;\n }\n\n if (bIndex === undefined) {\n return -1;\n }\n\n return aIndex - bIndex;\n }\n\n const nameCompare = compareForwardTunnelNameAsc(\n a.tunnelName,\n b.tunnelName,\n );\n\n if (nameCompare !== 0) {\n return nameCompare;\n }\n\n return compareForwardTunnelNameAsc(a.tunnelKey, b.tunnelKey);\n });\n\n return {\n userId: group.userId,\n userName: group.userName,\n tunnels,\n };\n });\n\n groups.sort((a, b) => {\n if (isAdmin && tokenUserId !== null) {\n const aIsSelf = a.userId === tokenUserId;\n const bIsSelf = b.userId === tokenUserId;\n\n if (aIsSelf !== bIsSelf) {\n return aIsSelf ? -1 : 1;\n }\n }\n\n const nameCompare = compareForwardUserNameAsc(a.userName, b.userName);\n\n if (nameCompare !== 0) {\n return nameCompare;\n }\n\n return a.userId - b.userId;\n });\n\n return groups;\n }, [orderedForwards, isAdmin, tokenUserId, sanitizedGroupOrderMap]);\n\n const sortedForwards = useMemo(() => {\n if (compactMode) {\n return orderedForwards;\n }\n\n return groupedForwards.flatMap((group) =>\n group.tunnels.flatMap((tunnel) => tunnel.items),\n );\n }, [compactMode, orderedForwards, groupedForwards]);\n\n const sortableForwardIds = useMemo(\n () => sortedForwards.map((f) => f.id).filter((id) => id > 0),\n [sortedForwards],\n );\n\n const toggleTunnelGroupCollapsed = (userId: number, tunnelKey: string) => {\n const collapseKey = buildTunnelGroupCollapseKey(userId, tunnelKey);\n const nextCollapsedMap: ForwardGroupCollapsedMap = {\n ...sanitizedCollapsedTunnelGroups,\n };\n\n if (nextCollapsedMap[collapseKey] === true) {\n delete nextCollapsedMap[collapseKey];\n } else {\n nextCollapsedMap[collapseKey] = true;\n }\n\n setCollapsedTunnelGroups(nextCollapsedMap);\n persistGroupCollapsedToLocal(nextCollapsedMap);\n void persistGroupCollapsedToGlobal(nextCollapsedMap);\n };\n\n const SortableTunnelGroupContainer = ({\n groupUserId,\n tunnel,\n collapsed,\n onToggleCollapsed,\n wrapperClassName,\n headerClassName,\n titleClassName,\n countClassName,\n bodyClassName,\n children,\n }: {\n groupUserId: number;\n tunnel: ForwardTunnelGroup;\n collapsed: boolean;\n onToggleCollapsed: () => void;\n wrapperClassName: string;\n headerClassName: string;\n titleClassName: string;\n countClassName: string;\n bodyClassName: string;\n children: React.ReactNode;\n }) => {\n const sortableId = buildTunnelGroupSortableId(\n groupUserId,\n tunnel.tunnelKey,\n );\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: sortableId });\n\n const style: React.CSSProperties = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.55 : 1,\n willChange: isDragging ? \"transform\" : undefined,\n zIndex: isDragging ? 1 : undefined,\n };\n\n return (\n
\n
\n
\n \n \n \n \n \n {tunnel.tunnelName}\n
\n
\n {tunnel.items.length} 条转发\n \n \n \n \n
\n
\n
\n {!collapsed &&
{children}
}\n \n );\n };\n\n // 可拖拽的转发卡片组件\n const SortableForwardCard = ({ forward }: { forward: Forward }) => {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: forward.id });\n\n const style: React.CSSProperties = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.5 : 1,\n willChange: isDragging ? \"transform\" : undefined,\n };\n\n return (\n
\n {renderForwardCard(forward, listeners)}\n
\n );\n };\n\n // 生成用作筛选项的用户和隧道列表\n const uniqueUsers = useMemo(() => {\n const userMap = new Map();\n\n forwards.forEach((f) => {\n const uId = f.userId ?? 0;\n const userName = normalizeForwardUserName(f.userName);\n const existingUser = userMap.get(uId);\n\n if (!existingUser) {\n userMap.set(uId, { id: uId, name: userName });\n\n return;\n }\n\n if (\n existingUser.name === UNKNOWN_FORWARD_USER_NAME &&\n userName !== UNKNOWN_FORWARD_USER_NAME\n ) {\n existingUser.name = userName;\n }\n });\n\n const users = Array.from(userMap.values());\n\n users.sort((a, b) => {\n if (isAdmin && tokenUserId !== null) {\n const aIsSelf = a.id === tokenUserId;\n const bIsSelf = b.id === tokenUserId;\n\n if (aIsSelf !== bIsSelf) {\n return aIsSelf ? -1 : 1;\n }\n }\n\n const nameCompare = compareForwardUserNameAsc(a.name, b.name);\n\n if (nameCompare !== 0) {\n return nameCompare;\n }\n\n return a.id - b.id;\n });\n\n return users;\n }, [forwards, isAdmin, tokenUserId]);\n\n // 可拖拽的表格行组件\n const SortableTableRow = ({\n forward,\n selectMode,\n selectedIds,\n toggleSelect,\n getStrategyDisplay,\n formatInAddress,\n formatRemoteAddress,\n handleServiceToggle,\n handleEdit,\n handleDelete,\n handleDiagnose,\n showAddressModal,\n hasMultipleAddresses,\n formatFlow,\n }: any) => {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: forward.id });\n\n const style = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.5 : 1,\n backgroundColor: isDragging ? \"var(--nextui-default-100)\" : undefined,\n };\n\n const strategyDisplay = getStrategyDisplay(forward.strategy);\n\n return (\n \n {selectMode && (\n \n toggleSelect(forward.id)}\n />\n \n )}\n \n \n \n \n \n \n \n \n {forward.name}\n \n \n \n showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n }\n >\n {formatInAddress(forward.inIp, forward.inPort)}\n \n \n \n \n showAddressModal(forward.remoteAddr, null, \"目标地址\")\n }\n >\n {formatRemoteAddress(forward.remoteAddr)}\n \n \n \n \n {strategyDisplay.text}\n \n \n \n \n {formatFlow(getForwardDisplayFlow(forward))}\n \n \n \n
\n handleServiceToggle(forward)}\n />\n
\n
\n \n
\n handleEdit(forward)}\n >\n \n \n \n \n handleDiagnose(forward)}\n >\n \n \n \n \n handleDelete(forward)}\n >\n \n \n \n \n
\n
\n
\n );\n };\n\n const SortableCompactTableRow = ({\n forward,\n selectMode,\n selectedIds,\n toggleSelect,\n getStrategyDisplay,\n formatInAddress,\n formatRemoteAddress,\n handleServiceToggle,\n handleEdit,\n handleDelete,\n handleDiagnose,\n showAddressModal,\n hasMultipleAddresses,\n formatFlow,\n }: any) => {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: forward.id });\n\n const style = {\n transform: transform\n ? CSS.Transform.toString({\n ...transform,\n x: Math.round(transform.x),\n y: Math.round(transform.y),\n })\n : undefined,\n transition: isDragging ? undefined : transition || undefined,\n opacity: isDragging ? 0.5 : 1,\n backgroundColor: isDragging ? \"var(--nextui-default-100)\" : undefined,\n };\n\n const strategyDisplay = getStrategyDisplay(forward.strategy);\n\n return (\n \n {selectMode && (\n \n toggleSelect(forward.id)}\n />\n \n )}\n \n \n \n \n \n \n \n \n \n {forward.userName || \"未知用户\"}\n \n \n \n {forward.name}\n \n \n \n \n {forward.tunnelName}\n \n \n \n \n \n showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n }\n >\n {formatInAddress(forward.inIp, forward.inPort)}\n \n \n \n \n showAddressModal(forward.remoteAddr, null, \"目标地址\")\n }\n >\n {formatRemoteAddress(forward.remoteAddr)}\n \n \n \n \n {strategyDisplay.text}\n \n \n \n \n {formatFlow(getForwardDisplayFlow(forward))}\n \n \n \n
\n handleServiceToggle(forward)}\n />\n
\n
\n \n
\n handleEdit(forward)}\n >\n \n \n \n \n handleDiagnose(forward)}\n >\n \n \n \n \n handleDelete(forward)}\n >\n \n \n \n \n
\n
\n
\n );\n };\n\n // 渲染转发卡片\n const renderForwardCard = (forward: Forward, listeners?: any) => {\n const statusDisplay = getStatusDisplay(forward.status);\n const strategyDisplay = getStrategyDisplay(forward.strategy);\n\n return (\n \n \n
\n {selectMode && (\n toggleSelect(forward.id)}\n />\n )}\n
\n

\n {forward.name}\n

\n

\n {normalizeForwardTunnelName(forward.tunnelName)}\n

\n
\n
\n {viewMode === \"direct\" && (\n \n \n \n \n
\n )}\n handleServiceToggle(forward)}\n />\n \n {statusDisplay.text}\n \n
\n \n
\n\n \n
\n {/* 地址信息 */}\n
\n \n showAddressModal(forward.inIp, forward.inPort, \"入口端口\")\n }\n >\n
\n
\n \n 入口:\n \n \n {formatInAddress(forward.inIp, forward.inPort)}\n \n
\n {hasMultipleAddresses(forward.inIp) && (\n \n \n \n )}\n
\n \n\n \n showAddressModal(forward.remoteAddr, null, \"目标地址\")\n }\n >\n
\n
\n \n 目标:\n \n \n {formatRemoteAddress(forward.remoteAddr)}\n \n
\n {hasMultipleAddresses(forward.remoteAddr) && (\n \n \n \n )}\n
\n \n
\n\n {/* 统计信息 */}\n
\n \n {strategyDisplay.text}\n \n {(forward.inFlow || 0) + (forward.outFlow || 0) > 0 ? (\n <>\n
\n \n ↑{formatFlow(forward.inFlow || 0)}\n \n
\n \n ↓{formatFlow(forward.outFlow || 0)}\n \n \n ) : (forward.federationShareFlow || 0) > 0 ? (\n \n 共享 {formatFlow(forward.federationShareFlow || 0)}\n \n ) : (\n \n 总流量 {formatFlow(0)}\n \n )}\n
\n
\n\n
\n \n \n \n }\n variant=\"flat\"\n onPress={() => handleEdit(forward)}\n >\n 编辑\n \n \n \n \n }\n variant=\"flat\"\n onPress={() => handleDiagnose(forward)}\n >\n 诊断\n \n \n \n \n \n }\n variant=\"flat\"\n onPress={() => handleDelete(forward)}\n >\n 删除\n \n
\n
\n \n );\n };\n\n if (loading || !groupPreferenceHydrated) {\n return ;\n }\n\n return (\n \n {/* 页面头部 */}\n
\n
\n setIsSearchVisible(false)}\n onOpen={() => setIsSearchVisible(true)}\n />\n
\n
\n
*]:shrink-0\">\n {selectMode ? (\n <>\n \n 已选择 {selectedIds.size} 项\n \n \n 全选\n \n \n 清空\n \n setBatchDeleteModalOpen(true)}\n >\n 删除\n \n handleBatchToggleService(false)}\n >\n 停用\n \n handleBatchToggleService(true)}\n >\n 启用\n \n \n 下发\n \n setBatchChangeTunnelModalOpen(true)}\n >\n 隧道\n \n \n 退出\n \n \n ) : (\n <>\n {/* 筛选按钮 */}\n setIsFilterModalOpen(true)}\n >\n \n \n \n {(filterUserId !== \"all\" || filterTunnelId !== \"all\") && (\n \n )}\n \n {/* 显示模式切换按钮 */}\n \n {viewMode === \"grouped\" ? (\n \n \n \n ) : (\n \n \n \n )}\n \n\n {/* 导入按钮 */}\n \n 导入\n \n\n {/* 导出按钮 */}\n \n 导出\n \n\n \n 批量\n \n\n \n 新增\n \n \n )}\n
\n
\n
\n\n {/* 根据显示模式渲染不同内容 */}\n {compactMode ? (\n viewMode === \"grouped\" ? (\n sortedForwards.length > 0 ? (\n
\n \n \n \n {selectMode && (\n 选择\n )}\n \n 用户\n 名称\n 隧道\n 入口\n 目标\n 策略\n 总流量\n 状态\n 操作\n \n \n {(forward) => (\n \n \n \n )}\n \n \n \n
\n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )\n ) : sortedForwards.length > 0 ? (\n {}}\n >\n \n
\n {sortedForwards.map((forward) =>\n forward && forward.id ? (\n \n ) : null,\n )}\n
\n \n \n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )\n ) : viewMode === \"grouped\" ? (\n sortedForwards.length > 0 ? (\n
\n {groupedForwards.map((group) => {\n const isSelfGroup =\n isAdmin && tokenUserId !== null && group.userId === tokenUserId;\n const groupForwardCount = group.tunnels.reduce(\n (total, tunnel) => total + tunnel.items.length,\n 0,\n );\n\n return (\n \n
\n
\n \n {group.userName}\n \n {isSelfGroup && (\n \n 管理员本人\n \n )}\n
\n \n {groupForwardCount} 条转发\n \n
\n\n
\n \n \n buildTunnelGroupSortableId(\n group.userId,\n tunnel.tunnelKey,\n ),\n )}\n strategy={verticalListSortingStrategy}\n >\n {group.tunnels.map((tunnel) => {\n const tunnelSortableForwardIds = tunnel.items\n .map((item) => item.id)\n .filter((id) => id > 0);\n const collapsed =\n sanitizedCollapsedTunnelGroups[\n buildTunnelGroupCollapseKey(\n group.userId,\n tunnel.tunnelKey,\n )\n ] === true;\n\n return (\n \n toggleTunnelGroupCollapsed(\n group.userId,\n tunnel.tunnelKey,\n )\n }\n >\n \n \n \n {selectMode && (\n \n 选择\n \n )}\n \n \n 名称\n \n \n 入口\n \n \n 目标\n \n \n 策略\n \n \n 总流量\n \n \n 状态\n \n \n 操作\n \n \n \n {(forward) => (\n \n \n \n )}\n \n \n \n \n );\n })}\n \n \n
\n
\n );\n })}\n \n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )\n ) : sortedForwards.length > 0 ? (\n
\n {groupedForwards.map((group) => {\n const isSelfGroup =\n isAdmin && tokenUserId !== null && group.userId === tokenUserId;\n const groupForwardCount = group.tunnels.reduce(\n (total, tunnel) => total + tunnel.items.length,\n 0,\n );\n\n return (\n \n
\n
\n \n {group.userName}\n \n {isSelfGroup && (\n \n 管理员本人\n \n )}\n
\n \n {groupForwardCount} 条转发\n \n
\n\n
\n \n \n buildTunnelGroupSortableId(\n group.userId,\n tunnel.tunnelKey,\n ),\n )}\n strategy={verticalListSortingStrategy}\n >\n {group.tunnels.map((tunnel) => {\n const tunnelSortableForwardIds = tunnel.items\n .map((item) => item.id)\n .filter((id) => id > 0);\n const collapsed =\n sanitizedCollapsedTunnelGroups[\n buildTunnelGroupCollapseKey(\n group.userId,\n tunnel.tunnelKey,\n )\n ] === true;\n\n return (\n \n toggleTunnelGroupCollapsed(\n group.userId,\n tunnel.tunnelKey,\n )\n }\n >\n {}}\n >\n \n
\n {tunnel.items.map((forward) =>\n forward && forward.id ? (\n \n ) : null,\n )}\n
\n \n \n \n );\n })}\n \n \n
\n
\n );\n })}\n \n ) : (\n \n \n

\n 暂无转发配置\n

\n

\n 还没有创建任何转发配置,点击上方按钮开始创建\n

\n
\n
\n )}\n\n {/* 新增/编辑模态框 */}\n \n \n {(onClose) => (\n <>\n \n

\n {isEdit ? \"编辑转发\" : \"新增转发\"}\n

\n

\n {isEdit ? \"修改现有转发配置的信息\" : \"创建新的转发配置\"}\n

\n
\n \n
\n \n setForm((prev) => ({ ...prev, name: e.target.value }))\n }\n />\n\n {\n const selectedKey = Array.from(keys)[0] as\n | string\n | undefined;\n\n setForm((prev) => ({\n ...prev,\n speedId: selectedKey ? Number(selectedKey) : null,\n }));\n }}\n >\n {availableSpeedLimits.map((speedLimit) => (\n \n {speedLimit.name}\n \n ))}\n \n\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n if (selectedKey) {\n handleTunnelChange(selectedKey);\n }\n }}\n >\n {tunnels.map((tunnel) => (\n {tunnel.name}\n ))}\n \n\n {\n const value = e.target.value;\n\n setForm((prev) => ({\n ...prev,\n inPort: value ? parseInt(value) : null,\n }));\n }}\n />\n\n 0\n ? \"选择入口监听IP\"\n : \"当前隧道入口节点暂无可选IP\"\n : \"请先选择隧道\"\n }\n selectedKeys={[form.inIp || \"__default__\"]}\n variant=\"bordered\"\n onSelectionChange={(keys) => {\n const selectedKey = Array.from(keys)[0] as string;\n\n setForm((prev) => ({\n ...prev,\n inIp: selectedKey === \"__default__\" ? \"\" : selectedKey,\n }));\n }}\n >\n 默认入口IP\n {currentTunnelIpOptions.map((ip) => (\n {ip}\n ))}\n \n\n \n setForm((prev) => ({\n ...prev,\n remoteAddr: e.target.value,\n }))\n }\n />\n\n {getAddressCount(form.remoteAddr) > 1 && (\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n setForm((prev) => ({ ...prev, strategy: selectedKey }));\n }}\n >\n 主备模式 - 自上而下\n 轮询模式 - 依次轮换\n 随机模式 - 随机选择\n 哈希模式 - IP哈希\n \n )}\n
\n
\n \n \n \n {isEdit ? \"保存修改\" : \"创建转发\"}\n \n \n \n )}\n
\n \n\n {/* 删除确认模态框 */}\n \n \n {(onClose) => (\n <>\n \n

确认删除

\n
\n \n

\n 确定要删除转发{\" \"}\n \n "{forwardToDelete?.name}"\n {\" \"}\n 吗?\n

\n

\n 此操作无法撤销,删除后该转发将永久消失。\n

\n
\n \n \n \n 确认删除\n \n \n \n )}\n
\n \n\n {/* 地址列表弹窗 */}\n setAddressModalOpen(false)}\n >\n \n {addressModalTitle}\n \n
\n \n
\n\n
\n {addressList.map((item) => (\n \n \n {item.address}\n \n copyAddress(item)}\n >\n 复制\n \n
\n ))}\n \n
\n
\n \n\n {/* 导出数据模态框 */}\n {\n setExportModalOpen(false);\n setSelectedTunnelForExport(null);\n setExportData(\"\");\n }}\n >\n \n \n

导出转发数据

\n

\n 格式:目标地址|转发名称|入口端口\n

\n
\n \n
\n {/* 隧道选择 */}\n
\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n setSelectedTunnelForExport(\n selectedKey ? parseInt(selectedKey) : null,\n );\n }}\n >\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n
\n\n {/* 导出按钮和数据 */}\n {exportData && (\n
\n \n \n \n }\n onPress={executeExport}\n >\n 重新生成\n \n \n \n \n \n }\n onPress={copyExportData}\n >\n 复制\n \n
\n )}\n\n {/* 初始导出按钮 */}\n {!exportData && (\n
\n \n \n \n }\n onPress={executeExport}\n >\n 生成导出数据\n \n
\n )}\n\n {/* 导出数据显示 */}\n {exportData && (\n
\n \n
\n )}\n
\n
\n \n \n \n
\n \n\n {/* 导入数据模态框 */}\n setImportModalOpen(false)}\n >\n \n \n

导入转发数据

\n {importFormat === \"flvx\" ? (\n <>\n

\n 格式:目标地址|转发名称|入口端口,每行一个,入口端口留空将自动分配可用端口\n

\n

\n 目标地址支持单个地址(如:example.com:8080)或多个地址用逗号分隔(如:3.3.3.3:3,4.4.4.4:4)\n

\n \n ) : (\n <>\n

\n ny格式:JSON对象,支持多个目标地址(负载均衡),按所选隧道导入\n

\n

\n 格式:{"dest":["地址:端口"],"listen_port":端口,"name":"名称"}(listen_port可省略,自动分配端口)\n

\n \n )}\n
\n \n
\n {/* 格式选择 */}\n {\n const selectedKey = Array.from(keys)[0] as ImportFormat;\n\n if (selectedKey) {\n setImportFormat(selectedKey);\n setSelectedTunnelForImport(null);\n setImportData(\"\");\n setImportResults([]);\n }\n }}\n >\n \n flvx格式(管道分隔)\n \n \n ny格式(JSON)\n \n \n\n {/* 隧道选择 - 两种格式都需要 */}\n {\n const selectedKey = Array.from(keys)[0] as string;\n\n setSelectedTunnelForImport(\n selectedKey ? parseInt(selectedKey) : null,\n );\n }}\n >\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n\n {/* 输入区域 */}\n setImportData(e.target.value)}\n />\n\n {/* 导入结果 */}\n {importResults.length > 0 && (\n
\n
\n

导入结果

\n \n 成功:{importResults.filter((r) => r.success).length} /\n 总计:{importResults.length}\n \n
\n\n \n {importResults.map((result, index) => (\n \n
\n {result.success ? (\n \n \n \n ) : (\n \n \n \n )}\n
\n
\n \n {result.success ? \"成功\" : \"失败\"}\n \n \n |\n \n \n {result.line}\n \n
\n \n {result.message}\n
\n
\n
\n
\n ))}\n \n \n )}\n \n
\n \n \n \n 开始导入\n \n \n
\n \n\n {/* 诊断结果模态框 */}\n {\n setDiagnosisModalOpen(open);\n if (!open) {\n diagnosisAbortRef.current?.abort();\n diagnosisAbortRef.current = null;\n setDiagnosisLoading(false);\n }\n }}\n >\n \n {(onClose) => (\n <>\n \n

转发诊断结果

\n {currentDiagnosisForward && (\n
\n \n {currentDiagnosisForward.name}\n \n \n 转发服务\n \n
\n )}\n
\n \n {diagnosisResult ? (\n
\n {diagnosisLoading && (\n
\n
\n \n \n 正在诊断 {diagnosisProgress.completed}/\n {diagnosisProgress.total > 0\n ? diagnosisProgress.total\n : \"?\"}\n \n
\n \n 流式更新中\n \n
\n )}\n\n {diagnosisProgress.timedOut && (\n \n )}\n\n {/* 统计摘要 */}\n
\n
\n
\n {diagnosisProgress.total > 0\n ? diagnosisProgress.total\n : diagnosisResult.results.length}\n
\n
\n 总测试数\n
\n
\n
\n
\n {diagnosisProgress.completed > 0 ||\n diagnosisProgress.total > 0\n ? diagnosisProgress.success\n : diagnosisResult.results.filter((r) => r.success)\n .length}\n
\n
\n 成功\n
\n
\n
\n
\n {diagnosisProgress.completed > 0 ||\n diagnosisProgress.total > 0\n ? diagnosisProgress.failed\n : diagnosisResult.results.filter((r) => !r.success)\n .length}\n
\n
\n 失败\n
\n
\n
\n\n {/* 桌面端表格展示 */}\n
\n {(() => {\n // 使用后端返回的 chainType 和 inx 字段进行分组\n const groupedResults = {\n entry: diagnosisResult.results.filter(\n (r) => r.fromChainType === 1,\n ),\n chains: {} as Record<\n number,\n typeof diagnosisResult.results\n >,\n exit: diagnosisResult.results.filter(\n (r) => r.fromChainType === 3,\n ),\n };\n\n // 按 inx 分组链路测试\n diagnosisResult.results.forEach((r) => {\n if (r.fromChainType === 2 && r.fromInx != null) {\n if (!groupedResults.chains[r.fromInx]) {\n groupedResults.chains[r.fromInx] = [];\n }\n groupedResults.chains[r.fromInx].push(r);\n }\n });\n\n const renderTableSection = (\n title: string,\n results: typeof diagnosisResult.results,\n ) => {\n if (results.length === 0) return null;\n\n return (\n \n
\n

\n {title}\n

\n
\n \n \n \n \n \n \n \n \n \n \n \n {results.map((result, index) => {\n const isDiagnosing = Boolean(\n result.diagnosing,\n );\n const isSuccess = result.success === true;\n const quality =\n getForwardDiagnosisQualityDisplay(\n result.averageTime,\n result.packetLoss,\n );\n\n return (\n \n \n \n \n \n \n \n );\n })}\n \n
\n 路径\n \n 状态\n \n 延迟(ms)\n \n 丢包率\n \n 质量\n
\n
\n {isDiagnosing ? (\n \n ) : (\n \n {isSuccess ? \"✓\" : \"✗\"}\n \n )}\n
\n
\n {result.description}\n
\n
\n {result.targetIp}:\n {result.targetPort}\n
\n
\n
\n
\n \n {isDiagnosing\n ? \"诊断中\"\n : isSuccess\n ? \"成功\"\n : \"失败\"}\n \n \n {isSuccess ? (\n \n {result.averageTime?.toFixed(0)}\n \n ) : (\n \n -\n \n )}\n \n {isSuccess ? (\n 0\n ? \"text-warning\"\n : \"text-success\"\n }`}\n >\n {result.packetLoss?.toFixed(1)}%\n \n ) : (\n \n -\n \n )}\n \n {isSuccess && quality ? (\n \n {quality.text}\n \n ) : (\n \n -\n \n )}\n
\n
\n );\n };\n\n return (\n <>\n {/* 入口测试 */}\n {renderTableSection(\n \"🚪 入口测试\",\n groupedResults.entry,\n )}\n\n {/* 链路测试(按跳数排序) */}\n {Object.keys(groupedResults.chains)\n .map(Number)\n .sort((a, b) => a - b)\n .map((hop) =>\n renderTableSection(\n `🔗 转发链 - 第${hop}跳`,\n groupedResults.chains[hop],\n ),\n )}\n\n {/* 出口测试 */}\n {renderTableSection(\n \"🚀 出口测试\",\n groupedResults.exit,\n )}\n \n );\n })()}\n
\n\n {/* 移动端卡片展示 */}\n
\n {(() => {\n // 使用后端返回的 chainType 和 inx 字段进行分组\n const groupedResults = {\n entry: diagnosisResult.results.filter(\n (r) => r.fromChainType === 1,\n ),\n chains: {} as Record<\n number,\n typeof diagnosisResult.results\n >,\n exit: diagnosisResult.results.filter(\n (r) => r.fromChainType === 3,\n ),\n };\n\n // 按 inx 分组链路测试\n diagnosisResult.results.forEach((r) => {\n if (r.fromChainType === 2 && r.fromInx != null) {\n if (!groupedResults.chains[r.fromInx]) {\n groupedResults.chains[r.fromInx] = [];\n }\n groupedResults.chains[r.fromInx].push(r);\n }\n });\n\n const renderCardSection = (\n title: string,\n results: typeof diagnosisResult.results,\n ) => {\n if (results.length === 0) return null;\n\n return (\n
\n
\n

\n {title}\n

\n
\n {results.map((result, index) => {\n const isDiagnosing = Boolean(result.diagnosing);\n const isSuccess = result.success === true;\n const quality =\n getForwardDiagnosisQualityDisplay(\n result.averageTime,\n result.packetLoss,\n );\n\n return (\n \n
\n {isDiagnosing ? (\n \n ) : (\n \n {isSuccess ? \"✓\" : \"✗\"}\n \n )}\n
\n
\n {result.description}\n
\n
\n {result.targetIp}:{result.targetPort}\n
\n
\n \n {isDiagnosing\n ? \"诊断中\"\n : isSuccess\n ? \"成功\"\n : \"失败\"}\n \n
\n\n {isSuccess ? (\n
\n
\n
\n {result.averageTime?.toFixed(0)}\n
\n
\n 延迟(ms)\n
\n
\n
\n 0\n ? \"text-warning\"\n : \"text-success\"\n }`}\n >\n {result.packetLoss?.toFixed(1)}%\n
\n
\n 丢包率\n
\n
\n
\n {quality && (\n <>\n \n {quality.text}\n \n
\n 质量\n
\n \n )}\n
\n
\n ) : (\n
\n \n {isDiagnosing\n ? result.message || \"诊断中...\"\n : result.message || \"连接失败\"}\n
\n
\n )}\n \n );\n })}\n \n );\n };\n\n return (\n <>\n {/* 入口测试 */}\n {renderCardSection(\n \"🚪 入口测试\",\n groupedResults.entry,\n )}\n\n {/* 链路测试(按跳数排序) */}\n {Object.keys(groupedResults.chains)\n .map(Number)\n .sort((a, b) => a - b)\n .map((hop) =>\n renderCardSection(\n `🔗 转发链 - 第${hop}跳`,\n groupedResults.chains[hop],\n ),\n )}\n\n {/* 出口测试 */}\n {renderCardSection(\n \"🚀 出口测试\",\n groupedResults.exit,\n )}\n \n );\n })()}\n \n\n {/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */}\n {diagnosisResult.results.some(\n (r) => r.success === false && !r.diagnosing,\n ) && (\n \n )}\n \n ) : (\n
\n
\n \n \n \n
\n

\n 暂无诊断数据\n

\n
\n )}\n
\n \n \n {currentDiagnosisForward && (\n handleDiagnose(currentDiagnosisForward)}\n >\n 重新诊断\n \n )}\n \n \n )}\n
\n \n\n {/* 批量删除确认模态框 */}\n \n \n {(onClose) => (\n <>\n 确认删除\n \n

\n 确定要删除选中的 {selectedIds.size} 项转发吗?此操作不可撤销。\n

\n
\n \n \n \n 确认删除\n \n \n \n )}\n
\n \n\n {/* 批量换隧道模态框 */}\n \n \n {(onClose) => (\n <>\n 隧道\n \n

\n 将选中的 {selectedIds.size} 项转发迁移到新隧道:\n

\n {\n const selected = Array.from(keys)[0];\n\n setBatchTargetTunnelId(selected ? Number(selected) : null);\n }}\n >\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n
\n \n \n \n 确认换隧道\n \n \n \n )}\n
\n \n\n {/* 筛选模态框 */}\n \n \n {(onClose) => (\n <>\n \n 筛选条件\n \n \n
\n
\n

按用户筛选

\n {\n const key = Array.from(keys)[0] as string;\n\n setFilterUserId(key || \"all\");\n }}\n >\n 全部用户\n {uniqueUsers.map((user) => (\n \n {user.name}\n \n ))}\n \n
\n
\n

按隧道筛选

\n {\n const key = Array.from(keys)[0] as string;\n\n setFilterTunnelId(key || \"all\");\n }}\n >\n 全部隧道\n {tunnels.map((tunnel) => (\n \n {tunnel.name}\n \n ))}\n \n
\n
\n
\n \n {\n resetFilterUserId();\n resetFilterTunnelId();\n }}\n >\n 重置\n \n \n \n \n )}\n
\n \n
\n );\n}\n", "additions": 3, "deletions": 3 } ], "diagnostics": { "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/control_plane.go": [ { "range": { "start": { "line": 39, "character": 25 }, "end": { "line": 39, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 58, "character": 58 }, "end": { "line": 58, "character": 69 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 60, "character": 93 }, "end": { "line": 60, "character": 104 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 62, "character": 22 }, "end": { "line": 62, "character": 33 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 66, "character": 28 }, "end": { "line": 66, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 90, "character": 21 }, "end": { "line": 90, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 101, "character": 3 }, "end": { "line": 101, "character": 12 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop" }, "source": "mapsloop", "message": "Replace m[k]=v loop with maps.Copy" }, { "range": { "start": { "line": 340, "character": 26 }, "end": { "line": 340, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 375, "character": 63 }, "end": { "line": 375, "character": 74 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 383, "character": 73 }, "end": { "line": 383, "character": 84 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 387, "character": 84 }, "end": { "line": 387, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 419, "character": 83 }, "end": { "line": 419, "character": 94 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 423, "character": 94 }, "end": { "line": 423, "character": 105 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 464, "character": 98 }, "end": { "line": 464, "character": 109 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 475, "character": 23 }, "end": { "line": 475, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 520, "character": 26 }, "end": { "line": 520, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 537, "character": 27 }, "end": { "line": 537, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 553, "character": 27 }, "end": { "line": 553, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 573, "character": 28 }, "end": { "line": 573, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 590, "character": 28 }, "end": { "line": 590, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 609, "character": 26 }, "end": { "line": 609, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 624, "character": 26 }, "end": { "line": 624, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 635, "character": 89 }, "end": { "line": 635, "character": 100 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 646, "character": 23 }, "end": { "line": 646, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 690, "character": 25 }, "end": { "line": 690, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 706, "character": 27 }, "end": { "line": 706, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 722, "character": 27 }, "end": { "line": 722, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 742, "character": 28 }, "end": { "line": 742, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 759, "character": 28 }, "end": { "line": 759, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 777, "character": 25 }, "end": { "line": 777, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 790, "character": 25 }, "end": { "line": 790, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 821, "character": 1 }, "end": { "line": 821, "character": 11 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicessort" }, "source": "slicessort", "message": "sort.Slice can be modernized using slices.Sort" }, { "range": { "start": { "line": 891, "character": 84 }, "end": { "line": 891, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 905, "character": 112 }, "end": { "line": 905, "character": 123 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 906, "character": 29 }, "end": { "line": 906, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 920, "character": 135 }, "end": { "line": 920, "character": 146 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 924, "character": 30 }, "end": { "line": 924, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 930, "character": 4 }, "end": { "line": 930, "character": 19 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#minmax" }, "source": "minmax", "message": "if statement can be modernized using max" }, { "range": { "start": { "line": 939, "character": 19 }, "end": { "line": 939, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 964, "character": 5 }, "end": { "line": 964, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#rangeint" }, "source": "rangeint", "message": "for loop can be modernized using range over int" }, { "range": { "start": { "line": 1015, "character": 119 }, "end": { "line": 1015, "character": 130 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1015, "character": 143 }, "end": { "line": 1015, "character": 154 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1016, "character": 20 }, "end": { "line": 1016, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1026, "character": 2 }, "end": { "line": 1026, "character": 9 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop" }, "source": "mapsloop", "message": "Replace m[k]=v loop with maps.Copy" }, { "range": { "start": { "line": 1031, "character": 62 }, "end": { "line": 1031, "character": 73 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1031, "character": 199 }, "end": { "line": 1031, "character": 210 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1044, "character": 60 }, "end": { "line": 1044, "character": 71 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1044, "character": 197 }, "end": { "line": 1044, "character": 208 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1057, "character": 22 }, "end": { "line": 1057, "character": 33 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1094, "character": 64 }, "end": { "line": 1094, "character": 75 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1094, "character": 192 }, "end": { "line": 1094, "character": 203 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1159, "character": 110 }, "end": { "line": 1159, "character": 121 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1166, "character": 72 }, "end": { "line": 1166, "character": 83 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1181, "character": 120 }, "end": { "line": 1181, "character": 131 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1305, "character": 193 }, "end": { "line": 1305, "character": 204 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1307, "character": 31 }, "end": { "line": 1307, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1329, "character": 24 }, "end": { "line": 1329, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1332, "character": 25 }, "end": { "line": 1332, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1335, "character": 26 }, "end": { "line": 1335, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1338, "character": 27 }, "end": { "line": 1338, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1340, "character": 27 }, "end": { "line": 1340, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1348, "character": 34 }, "end": { "line": 1348, "character": 45 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1352, "character": 35 }, "end": { "line": 1352, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1355, "character": 34 }, "end": { "line": 1355, "character": 45 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1358, "character": 36 }, "end": { "line": 1358, "character": 47 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1369, "character": 56 }, "end": { "line": 1369, "character": 67 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1370, "character": 28 }, "end": { "line": 1370, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1372, "character": 35 }, "end": { "line": 1372, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1410, "character": 14 }, "end": { "line": 1410, "character": 25 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1433, "character": 76 }, "end": { "line": 1433, "character": 87 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1438, "character": 25 }, "end": { "line": 1438, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1444, "character": 60 }, "end": { "line": 1444, "character": 71 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1444, "character": 84 }, "end": { "line": 1444, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1445, "character": 19 }, "end": { "line": 1445, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1457, "character": 24 }, "end": { "line": 1457, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/model/model.go": [ { "range": { "start": { "line": 230, "character": 27 }, "end": { "line": 230, "character": 85 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero" }, "source": "omitzero", "message": "Omitempty has no effect on nested struct fields" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/repo/repository_control.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/mutations.go": [ { "range": { "start": { "line": 32, "character": 20 }, "end": { "line": 32, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 86, "character": 20 }, "end": { "line": 86, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 213, "character": 20 }, "end": { "line": 213, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 238, "character": 20 }, "end": { "line": 238, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 286, "character": 20 }, "end": { "line": 286, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 453, "character": 20 }, "end": { "line": 453, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 621, "character": 2 }, "end": { "line": 621, "character": 23 } }, "severity": 3, "code": "default", "codeDescription": { "href": "https://staticcheck.dev/docs/checks/#QF1003" }, "source": "QF1003", "message": "could use tagged switch on row.ChainType" }, { "range": { "start": { "line": 622, "character": 67 }, "end": { "line": 622, "character": 78 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 624, "character": 67 }, "end": { "line": 624, "character": 78 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 625, "character": 68 }, "end": { "line": 625, "character": 79 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 627, "character": 68 }, "end": { "line": 627, "character": 79 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 660, "character": 20 }, "end": { "line": 660, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 843, "character": 46 }, "end": { "line": 843, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 993, "character": 46 }, "end": { "line": 993, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 997, "character": 20 }, "end": { "line": 997, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1022, "character": 18 }, "end": { "line": 1022, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1047, "character": 20 }, "end": { "line": 1047, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1112, "character": 20 }, "end": { "line": 1112, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1202, "character": 20 }, "end": { "line": 1202, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1469, "character": 46 }, "end": { "line": 1469, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1500, "character": 46 }, "end": { "line": 1500, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1531, "character": 46 }, "end": { "line": 1531, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1558, "character": 46 }, "end": { "line": 1558, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1654, "character": 46 }, "end": { "line": 1654, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1658, "character": 20 }, "end": { "line": 1658, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1683, "character": 20 }, "end": { "line": 1683, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1866, "character": 20 }, "end": { "line": 1866, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1885, "character": 20 }, "end": { "line": 1885, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1976, "character": 71 }, "end": { "line": 1976, "character": 82 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2162, "character": 46 }, "end": { "line": 2162, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2210, "character": 20 }, "end": { "line": 2210, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2543, "character": 63 }, "end": { "line": 2543, "character": 74 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2554, "character": 62 }, "end": { "line": 2554, "character": 73 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2575, "character": 153 }, "end": { "line": 2575, "character": 164 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2583, "character": 32 }, "end": { "line": 2583, "character": 43 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2598, "character": 26 }, "end": { "line": 2598, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2602, "character": 38 }, "end": { "line": 2602, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2604, "character": 43 }, "end": { "line": 2604, "character": 54 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2608, "character": 24 }, "end": { "line": 2608, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2615, "character": 19 }, "end": { "line": 2615, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2617, "character": 25 }, "end": { "line": 2617, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2628, "character": 19 }, "end": { "line": 2628, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2630, "character": 23 }, "end": { "line": 2630, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2634, "character": 111 }, "end": { "line": 2634, "character": 122 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2639, "character": 26 }, "end": { "line": 2639, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2643, "character": 38 }, "end": { "line": 2643, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2645, "character": 23 }, "end": { "line": 2645, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2649, "character": 25 }, "end": { "line": 2649, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2654, "character": 33 }, "end": { "line": 2654, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2657, "character": 35 }, "end": { "line": 2657, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2659, "character": 21 }, "end": { "line": 2659, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2786, "character": 84 }, "end": { "line": 2786, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2988, "character": 10 }, "end": { "line": 2988, "character": 23 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq" }, "source": "stringsseq", "message": "Ranging over SplitSeq is more efficient" }, { "range": { "start": { "line": 3073, "character": 50 }, "end": { "line": 3073, "character": 61 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3240, "character": 18 }, "end": { "line": 3240, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3240, "character": 33 }, "end": { "line": 3240, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3244, "character": 20 }, "end": { "line": 3244, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3250, "character": 18 }, "end": { "line": 3250, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3250, "character": 44 }, "end": { "line": 3250, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3255, "character": 26 }, "end": { "line": 3255, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3257, "character": 29 }, "end": { "line": 3257, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3264, "character": 16 }, "end": { "line": 3264, "character": 27 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3283, "character": 13 }, "end": { "line": 3283, "character": 24 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3295, "character": 15 }, "end": { "line": 3295, "character": 26 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3307, "character": 15 }, "end": { "line": 3307, "character": 26 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3319, "character": 23 }, "end": { "line": 3319, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3340, "character": 20 }, "end": { "line": 3340, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3358, "character": 20 }, "end": { "line": 3358, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3375, "character": 1 }, "end": { "line": 3375, "character": 11 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicessort" }, "source": "slicessort", "message": "sort.Slice can be modernized using slices.Sort" }, { "range": { "start": { "line": 3379, "character": 28 }, "end": { "line": 3379, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3386, "character": 27 }, "end": { "line": 3386, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3408, "character": 20 }, "end": { "line": 3408, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/diagnosis_stream.go": [ { "range": { "start": { "line": 15, "character": 6 }, "end": { "line": 15, "character": 17 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 31, "character": 99 }, "end": { "line": 31, "character": 110 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 43, "character": 53 }, "end": { "line": 43, "character": 64 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 73, "character": 136 }, "end": { "line": 73, "character": 147 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 80, "character": 23 }, "end": { "line": 80, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 86, "character": 2 }, "end": { "line": 86, "character": 14 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop" }, "source": "mapsloop", "message": "Replace m[k]=v loop with maps.Copy" }, { "range": { "start": { "line": 93, "character": 44 }, "end": { "line": 93, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 97, "character": 28 }, "end": { "line": 97, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 116, "character": 27 }, "end": { "line": 116, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 146, "character": 28 }, "end": { "line": 146, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 195, "character": 28 }, "end": { "line": 195, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/dual_stack_test.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/repo/repository.go": [ { "range": { "start": { "line": 161, "character": 13 }, "end": { "line": 161, "character": 24 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 356, "character": 77 }, "end": { "line": 356, "character": 88 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 436, "character": 74 }, "end": { "line": 436, "character": 85 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 573, "character": 77 }, "end": { "line": 573, "character": 88 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 583, "character": 77 }, "end": { "line": 583, "character": 88 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 596, "character": 28 }, "end": { "line": 596, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 603, "character": 28 }, "end": { "line": 603, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 611, "character": 29 }, "end": { "line": 611, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 624, "character": 47 }, "end": { "line": 624, "character": 58 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 632, "character": 28 }, "end": { "line": 632, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 634, "character": 35 }, "end": { "line": 634, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 654, "character": 47 }, "end": { "line": 654, "character": 58 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 662, "character": 28 }, "end": { "line": 662, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 664, "character": 35 }, "end": { "line": 664, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 676, "character": 53 }, "end": { "line": 676, "character": 64 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 684, "character": 28 }, "end": { "line": 684, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 686, "character": 21 }, "end": { "line": 686, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 696, "character": 50 }, "end": { "line": 696, "character": 61 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 728, "character": 28 }, "end": { "line": 728, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 734, "character": 21 }, "end": { "line": 734, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 750, "character": 75 }, "end": { "line": 750, "character": 86 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 769, "character": 28 }, "end": { "line": 769, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 771, "character": 35 }, "end": { "line": 771, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 776, "character": 64 }, "end": { "line": 776, "character": 75 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 790, "character": 28 }, "end": { "line": 790, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 792, "character": 35 }, "end": { "line": 792, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 797, "character": 49 }, "end": { "line": 797, "character": 60 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 807, "character": 40 }, "end": { "line": 807, "character": 51 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 811, "character": 31 }, "end": { "line": 811, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 817, "character": 37 }, "end": { "line": 817, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 818, "character": 37 }, "end": { "line": 818, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 819, "character": 39 }, "end": { "line": 819, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 839, "character": 47 }, "end": { "line": 839, "character": 58 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 856, "character": 24 }, "end": { "line": 856, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 873, "character": 54 }, "end": { "line": 873, "character": 65 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 879, "character": 51 }, "end": { "line": 879, "character": 62 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 883, "character": 56 }, "end": { "line": 883, "character": 67 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 897, "character": 33 }, "end": { "line": 897, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 910, "character": 29 }, "end": { "line": 910, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 921, "character": 54 }, "end": { "line": 921, "character": 65 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 929, "character": 29 }, "end": { "line": 929, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 935, "character": 37 }, "end": { "line": 935, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 944, "character": 52 }, "end": { "line": 944, "character": 63 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 952, "character": 29 }, "end": { "line": 952, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 958, "character": 37 }, "end": { "line": 958, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 967, "character": 58 }, "end": { "line": 967, "character": 69 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 990, "character": 29 }, "end": { "line": 990, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 992, "character": 37 }, "end": { "line": 992, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1064, "character": 84 }, "end": { "line": 1064, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1130, "character": 27 }, "end": { "line": 1130, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1146, "character": 83 }, "end": { "line": 1146, "character": 94 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1170, "character": 90 }, "end": { "line": 1170, "character": 101 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1184, "character": 85 }, "end": { "line": 1184, "character": 96 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1404, "character": 85 }, "end": { "line": 1404, "character": 96 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1421, "character": 130 }, "end": { "line": 1421, "character": 141 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1442, "character": 21 }, "end": { "line": 1442, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2382, "character": 23 }, "end": { "line": 2382, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2397, "character": 23 }, "end": { "line": 2397, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2503, "character": 36 }, "end": { "line": 2503, "character": 47 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2591, "character": 27 }, "end": { "line": 2591, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2713, "character": 16 }, "end": { "line": 2713, "character": 29 } }, "severity": 4, "code": "stringscut", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#stringscut" }, "source": "stringscut", "message": "strings.Index can be simplified using strings.Cut" }, { "range": { "start": { "line": 2757, "character": 15 }, "end": { "line": 2757, "character": 28 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq" }, "source": "stringsseq", "message": "Ranging over SplitSeq is more efficient" }, { "range": { "start": { "line": 2803, "character": 38 }, "end": { "line": 2803, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2810, "character": 38 }, "end": { "line": 2810, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2818, "character": 36 }, "end": { "line": 2818, "character": 47 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/repo/repository_mutations.go": [ { "range": { "start": { "line": 78, "character": 21 }, "end": { "line": 78, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 96, "character": 21 }, "end": { "line": 96, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 113, "character": 21 }, "end": { "line": 113, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 156, "character": 21 }, "end": { "line": 156, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 163, "character": 21 }, "end": { "line": 163, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 172, "character": 21 }, "end": { "line": 172, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 198, "character": 116 }, "end": { "line": 198, "character": 127 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 198, "character": 277 }, "end": { "line": 198, "character": 288 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 241, "character": 119 }, "end": { "line": 241, "character": 130 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 247, "character": 21 }, "end": { "line": 247, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 294, "character": 21 }, "end": { "line": 294, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 371, "character": 21 }, "end": { "line": 371, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 380, "character": 21 }, "end": { "line": 380, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 559, "character": 107 }, "end": { "line": 559, "character": 118 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 565, "character": 21 }, "end": { "line": 565, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 601, "character": 70 }, "end": { "line": 601, "character": 81 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 620, "character": 62 }, "end": { "line": 620, "character": 73 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 626, "character": 21 }, "end": { "line": 626, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 648, "character": 122 }, "end": { "line": 648, "character": 133 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 654, "character": 21 }, "end": { "line": 654, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 670, "character": 21 }, "end": { "line": 670, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 679, "character": 21 }, "end": { "line": 679, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 722, "character": 149 }, "end": { "line": 722, "character": 160 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 728, "character": 21 }, "end": { "line": 728, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 786, "character": 23 }, "end": { "line": 786, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 837, "character": 23 }, "end": { "line": 837, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1179, "character": 192 }, "end": { "line": 1179, "character": 203 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1228, "character": 80 }, "end": { "line": 1228, "character": 91 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1237, "character": 137 }, "end": { "line": 1237, "character": 148 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1265, "character": 31 }, "end": { "line": 1265, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1291, "character": 30 }, "end": { "line": 1291, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1334, "character": 27 }, "end": { "line": 1334, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1363, "character": 10 }, "end": { "line": 1363, "character": 23 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq" }, "source": "stringsseq", "message": "Ranging over SplitSeq is more efficient" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/client/federation.go": [ { "range": { "start": { "line": 81, "character": 13 }, "end": { "line": 81, "character": 24 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 88, "character": 20 }, "end": { "line": 88, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 147, "character": 23 }, "end": { "line": 147, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 305, "character": 112 }, "end": { "line": 305, "character": 123 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 332, "character": 18 }, "end": { "line": 332, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/tests/contract/diagnosis_contract_test.go": [ { "range": { "start": { "line": 103, "character": 38 }, "end": { "line": 103, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 107, "character": 39 }, "end": { "line": 107, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 116, "character": 31 }, "end": { "line": 116, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 156, "character": 38 }, "end": { "line": 156, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 160, "character": 39 }, "end": { "line": 160, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 169, "character": 31 }, "end": { "line": 169, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 272, "character": 37 }, "end": { "line": 272, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 276, "character": 38 }, "end": { "line": 276, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 284, "character": 30 }, "end": { "line": 284, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 326, "character": 44 }, "end": { "line": 326, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 330, "character": 21 }, "end": { "line": 330, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 333, "character": 44 }, "end": { "line": 333, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 338, "character": 44 }, "end": { "line": 338, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 343, "character": 44 }, "end": { "line": 343, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 348, "character": 43 }, "end": { "line": 348, "character": 54 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 351, "character": 22 }, "end": { "line": 351, "character": 33 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 431, "character": 37 }, "end": { "line": 431, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 435, "character": 38 }, "end": { "line": 435, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 442, "character": 30 }, "end": { "line": 442, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 531, "character": 38 }, "end": { "line": 531, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 535, "character": 39 }, "end": { "line": 535, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 543, "character": 31 }, "end": { "line": 543, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 584, "character": 24 }, "end": { "line": 584, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 592, "character": 41 }, "end": { "line": 592, "character": 52 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 596, "character": 33 }, "end": { "line": 596, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 601, "character": 32 }, "end": { "line": 601, "character": 43 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/control_plane_test.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/jobs_test.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/reconstruct_tunnel_state_test.go": [] }, "truncated": false }, "time": { "start": 1772527872397, "end": 1772527872437 } }, "id": "prt_cb2e4d7ef001zOP4wDKC7Os5hX", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e4b7ec002JuDND0Cz41ar1x" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "efb3d3e844a61777bd618fab10e58a841c932b7c", "cost": 0, "tokens": { "total": 36836, "input": 22423, "output": 717, "reasoning": 271, "cache": { "read": 13696, "write": 0 } }, "id": "prt_cb2e4e9eb001iI2mq63KoHga11", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e4b7ec002JuDND0Cz41ar1x" }, { "type": "patch", "hash": "bef151cc89fd456e4195183345800f4cf9181ff7", "files": [ "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward.tsx" ], "id": "prt_cb2e4ea3b0017i7qaesDNMhfqg", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e4b7ec002JuDND0Cz41ar1x" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527872577, "completed": 1772527891941 }, "parentID": "msg_cb2e428a7001Ejj8LNAqWjo6fE", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "build", "agent": "build", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 37422, "input": 36564, "output": 858, "reasoning": 49, "cache": { "read": 0, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e4ea4100261mRk4AWiBuIa1", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "efb3d3e844a61777bd618fab10e58a841c932b7c", "id": "prt_cb2e5074e001LpGkX2Vb4caJyI", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e4ea4100261mRk4AWiBuIa1" }, { "type": "reasoning", "text": "**Planning test updates**", "time": { "start": 1772527880015, "end": 1772527891839 }, "id": "prt_cb2e5074f001DoGeyF5nTQ96s1", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e4ea4100261mRk4AWiBuIa1" }, { "type": "tool", "callID": "call_uctaZQAOwIGFdVTpyocKUTyX", "tool": "apply_patch", "state": { "status": "completed", "input": { "patchText": "*** Begin Patch\n*** Update File: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts\n@@\n test(\"parseNyFormatData returns validation errors for invalid fields\", () => {\n const input =\n '{\"dest\":[],\"listen_port\":0,\"name\":\"\"}{\"dest\":[\"bad-address\"],\"listen_port\":80,\"name\":\"ok\"}';\n@@\n assert.match(result[1].error || \"\", /目标地址格式错误/);\n });\n+\n+test(\"parseNyFormatData allows missing listen_port for auto assignment\", () => {\n+ const input = '{\"dest\":[\"1.1.1.1:1000\"],\"name\":\"No Port\"}';\n+\n+ const result = parseNyFormatData(input);\n+\n+ assert.equal(result.length, 1);\n+ assert.equal(result[0].error, undefined);\n+ assert.equal(result[0].parsed?.listen_port, null);\n+});\n+\n+test(\"parseNyFormatData supports ny alias fields\", () => {\n+ const input =\n+ '{\"dst\":[\"2.2.2.2:2000\"],\"listenPort\":\"3000\",\"forward_name\":\"Alias A\"}\\n{\"target\":\"3.3.3.3:4000,4.4.4.4:5000\",\"port\":6000,\"forwardName\":\"Alias B\"}';\n+\n+ const result = parseNyFormatData(input);\n+\n+ assert.equal(result.length, 2);\n+ assert.equal(result[0].error, undefined);\n+ assert.equal(result[1].error, undefined);\n+ assert.deepEqual(result[0].parsed?.dest, [\"2.2.2.2:2000\"]);\n+ assert.equal(result[0].parsed?.listen_port, 3000);\n+ assert.equal(result[0].parsed?.name, \"Alias A\");\n+ assert.deepEqual(result[1].parsed?.dest, [\"3.3.3.3:4000\", \"4.4.4.4:5000\"]);\n+ assert.equal(result[1].parsed?.listen_port, 6000);\n+ assert.equal(result[1].parsed?.name, \"Alias B\");\n+});\n@@\n test(\"convertNyItemToForwardInput maps ny fields correctly\", () => {\n const mapped = convertNyItemToForwardInput({\n dest: [\"1.1.1.1:1111\", \"2.2.2.2:2222\"],\n@@\n strategy: \"fifo\",\n });\n });\n+\n+test(\"convertNyItemToForwardInput keeps null inPort for auto assignment\", () => {\n+ const mapped = convertNyItemToForwardInput({\n+ dest: [\"1.1.1.1:1111\"],\n+ listen_port: null,\n+ name: \"No Port\",\n+ });\n+\n+ assert.deepEqual(mapped, {\n+ name: \"No Port\",\n+ inPort: null,\n+ remoteAddr: \"1.1.1.1:1111\",\n+ strategy: \"fifo\",\n+ });\n+});\n*** End Patch" }, "output": "Success. Updated the following files:\nM vite-frontend/src/pages/forward/import-format.test.ts", "title": "Success. Updated the following files:\nM vite-frontend/src/pages/forward/import-format.test.ts", "metadata": { "diff": "Index: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts\n===================================================================\n--- /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts\n+++ /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts\n@@ -45,8 +45,35 @@\n assert.match(result[0].error || \"\", /dest数组为空|listen_port|name/);\n assert.match(result[1].error || \"\", /目标地址格式错误/);\n });\n \n+test(\"parseNyFormatData allows missing listen_port for auto assignment\", () => {\n+ const input = '{\"dest\":[\"1.1.1.1:1000\"],\"name\":\"No Port\"}';\n+\n+ const result = parseNyFormatData(input);\n+\n+ assert.equal(result.length, 1);\n+ assert.equal(result[0].error, undefined);\n+ assert.equal(result[0].parsed?.listen_port, null);\n+});\n+\n+test(\"parseNyFormatData supports ny alias fields\", () => {\n+ const input =\n+ '{\"dst\":[\"2.2.2.2:2000\"],\"listenPort\":\"3000\",\"forward_name\":\"Alias A\"}\\n{\"target\":\"3.3.3.3:4000,4.4.4.4:5000\",\"port\":6000,\"forwardName\":\"Alias B\"}';\n+\n+ const result = parseNyFormatData(input);\n+\n+ assert.equal(result.length, 2);\n+ assert.equal(result[0].error, undefined);\n+ assert.equal(result[1].error, undefined);\n+ assert.deepEqual(result[0].parsed?.dest, [\"2.2.2.2:2000\"]);\n+ assert.equal(result[0].parsed?.listen_port, 3000);\n+ assert.equal(result[0].parsed?.name, \"Alias A\");\n+ assert.deepEqual(result[1].parsed?.dest, [\"3.3.3.3:4000\", \"4.4.4.4:5000\"]);\n+ assert.equal(result[1].parsed?.listen_port, 6000);\n+ assert.equal(result[1].parsed?.name, \"Alias B\");\n+});\n+\n test(\"convertNyItemToForwardInput maps ny fields correctly\", () => {\n const mapped = convertNyItemToForwardInput({\n dest: [\"1.1.1.1:1111\", \"2.2.2.2:2222\"],\n listen_port: 3333,\n@@ -59,4 +86,19 @@\n remoteAddr: \"1.1.1.1:1111,2.2.2.2:2222\",\n strategy: \"fifo\",\n });\n });\n+\n+test(\"convertNyItemToForwardInput keeps null inPort for auto assignment\", () => {\n+ const mapped = convertNyItemToForwardInput({\n+ dest: [\"1.1.1.1:1111\"],\n+ listen_port: null,\n+ name: \"No Port\",\n+ });\n+\n+ assert.deepEqual(mapped, {\n+ name: \"No Port\",\n+ inPort: null,\n+ remoteAddr: \"1.1.1.1:1111\",\n+ strategy: \"fifo\",\n+ });\n+});\n\n", "files": [ { "filePath": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts", "relativePath": "vite-frontend/src/pages/forward/import-format.test.ts", "type": "update", "diff": "Index: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts\n===================================================================\n--- /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts\n+++ /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts\n@@ -45,8 +45,35 @@\n assert.match(result[0].error || \"\", /dest数组为空|listen_port|name/);\n assert.match(result[1].error || \"\", /目标地址格式错误/);\n });\n \n+test(\"parseNyFormatData allows missing listen_port for auto assignment\", () => {\n+ const input = '{\"dest\":[\"1.1.1.1:1000\"],\"name\":\"No Port\"}';\n+\n+ const result = parseNyFormatData(input);\n+\n+ assert.equal(result.length, 1);\n+ assert.equal(result[0].error, undefined);\n+ assert.equal(result[0].parsed?.listen_port, null);\n+});\n+\n+test(\"parseNyFormatData supports ny alias fields\", () => {\n+ const input =\n+ '{\"dst\":[\"2.2.2.2:2000\"],\"listenPort\":\"3000\",\"forward_name\":\"Alias A\"}\\n{\"target\":\"3.3.3.3:4000,4.4.4.4:5000\",\"port\":6000,\"forwardName\":\"Alias B\"}';\n+\n+ const result = parseNyFormatData(input);\n+\n+ assert.equal(result.length, 2);\n+ assert.equal(result[0].error, undefined);\n+ assert.equal(result[1].error, undefined);\n+ assert.deepEqual(result[0].parsed?.dest, [\"2.2.2.2:2000\"]);\n+ assert.equal(result[0].parsed?.listen_port, 3000);\n+ assert.equal(result[0].parsed?.name, \"Alias A\");\n+ assert.deepEqual(result[1].parsed?.dest, [\"3.3.3.3:4000\", \"4.4.4.4:5000\"]);\n+ assert.equal(result[1].parsed?.listen_port, 6000);\n+ assert.equal(result[1].parsed?.name, \"Alias B\");\n+});\n+\n test(\"convertNyItemToForwardInput maps ny fields correctly\", () => {\n const mapped = convertNyItemToForwardInput({\n dest: [\"1.1.1.1:1111\", \"2.2.2.2:2222\"],\n listen_port: 3333,\n@@ -59,4 +86,19 @@\n remoteAddr: \"1.1.1.1:1111,2.2.2.2:2222\",\n strategy: \"fifo\",\n });\n });\n+\n+test(\"convertNyItemToForwardInput keeps null inPort for auto assignment\", () => {\n+ const mapped = convertNyItemToForwardInput({\n+ dest: [\"1.1.1.1:1111\"],\n+ listen_port: null,\n+ name: \"No Port\",\n+ });\n+\n+ assert.deepEqual(mapped, {\n+ name: \"No Port\",\n+ inPort: null,\n+ remoteAddr: \"1.1.1.1:1111\",\n+ strategy: \"fifo\",\n+ });\n+});\n", "before": "import test from \"node:test\";\nimport assert from \"node:assert/strict\";\n\nimport {\n convertNyItemToForwardInput,\n parseNyFormatData,\n} from \"./import-format.ts\";\n\ntest(\"parseNyFormatData parses concatenated ny JSON objects\", () => {\n const input =\n '{\"dest\":[\"151.241.129.52:23609\"],\"listen_port\":20224,\"name\":\"灵玥-JP-Lpt【三网通用】\"}{\"dest\":[\"64.81.33.2:24577\"],\"listen_port\":41034,\"name\":\"Yolo-US-Lpt【三网通用】\"}';\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.equal(result[0].error, undefined);\n assert.equal(result[1].error, undefined);\n assert.deepEqual(result[0].parsed?.dest, [\"151.241.129.52:23609\"]);\n assert.equal(result[0].parsed?.listen_port, 20224);\n assert.equal(result[1].parsed?.name, \"Yolo-US-Lpt【三网通用】\");\n});\n\ntest(\"parseNyFormatData parses newline-separated ny JSON objects\", () => {\n const input = [\n '{\"dest\":[\"1.1.1.1:1000\",\"2.2.2.2:2000\"],\"listen_port\":3000,\"name\":\"A\"}',\n '{\"dest\":[\"3.3.3.3:4000\"],\"listen_port\":5000,\"name\":\"B\"}',\n ].join(\"\\n\");\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.equal(result[0].error, undefined);\n assert.equal(result[1].error, undefined);\n assert.deepEqual(result[0].parsed?.dest, [\"1.1.1.1:1000\", \"2.2.2.2:2000\"]);\n assert.equal(result[0].parsed?.listen_port, 3000);\n});\n\ntest(\"parseNyFormatData returns validation errors for invalid fields\", () => {\n const input =\n '{\"dest\":[],\"listen_port\":0,\"name\":\"\"}{\"dest\":[\"bad-address\"],\"listen_port\":80,\"name\":\"ok\"}';\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.match(result[0].error || \"\", /dest数组为空|listen_port|name/);\n assert.match(result[1].error || \"\", /目标地址格式错误/);\n});\n\ntest(\"convertNyItemToForwardInput maps ny fields correctly\", () => {\n const mapped = convertNyItemToForwardInput({\n dest: [\"1.1.1.1:1111\", \"2.2.2.2:2222\"],\n listen_port: 3333,\n name: \" Forward Name \",\n });\n\n assert.deepEqual(mapped, {\n name: \"Forward Name\",\n inPort: 3333,\n remoteAddr: \"1.1.1.1:1111,2.2.2.2:2222\",\n strategy: \"fifo\",\n });\n});\n", "after": "import test from \"node:test\";\nimport assert from \"node:assert/strict\";\n\nimport {\n convertNyItemToForwardInput,\n parseNyFormatData,\n} from \"./import-format.ts\";\n\ntest(\"parseNyFormatData parses concatenated ny JSON objects\", () => {\n const input =\n '{\"dest\":[\"151.241.129.52:23609\"],\"listen_port\":20224,\"name\":\"灵玥-JP-Lpt【三网通用】\"}{\"dest\":[\"64.81.33.2:24577\"],\"listen_port\":41034,\"name\":\"Yolo-US-Lpt【三网通用】\"}';\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.equal(result[0].error, undefined);\n assert.equal(result[1].error, undefined);\n assert.deepEqual(result[0].parsed?.dest, [\"151.241.129.52:23609\"]);\n assert.equal(result[0].parsed?.listen_port, 20224);\n assert.equal(result[1].parsed?.name, \"Yolo-US-Lpt【三网通用】\");\n});\n\ntest(\"parseNyFormatData parses newline-separated ny JSON objects\", () => {\n const input = [\n '{\"dest\":[\"1.1.1.1:1000\",\"2.2.2.2:2000\"],\"listen_port\":3000,\"name\":\"A\"}',\n '{\"dest\":[\"3.3.3.3:4000\"],\"listen_port\":5000,\"name\":\"B\"}',\n ].join(\"\\n\");\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.equal(result[0].error, undefined);\n assert.equal(result[1].error, undefined);\n assert.deepEqual(result[0].parsed?.dest, [\"1.1.1.1:1000\", \"2.2.2.2:2000\"]);\n assert.equal(result[0].parsed?.listen_port, 3000);\n});\n\ntest(\"parseNyFormatData returns validation errors for invalid fields\", () => {\n const input =\n '{\"dest\":[],\"listen_port\":0,\"name\":\"\"}{\"dest\":[\"bad-address\"],\"listen_port\":80,\"name\":\"ok\"}';\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.match(result[0].error || \"\", /dest数组为空|listen_port|name/);\n assert.match(result[1].error || \"\", /目标地址格式错误/);\n});\n\ntest(\"parseNyFormatData allows missing listen_port for auto assignment\", () => {\n const input = '{\"dest\":[\"1.1.1.1:1000\"],\"name\":\"No Port\"}';\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 1);\n assert.equal(result[0].error, undefined);\n assert.equal(result[0].parsed?.listen_port, null);\n});\n\ntest(\"parseNyFormatData supports ny alias fields\", () => {\n const input =\n '{\"dst\":[\"2.2.2.2:2000\"],\"listenPort\":\"3000\",\"forward_name\":\"Alias A\"}\\n{\"target\":\"3.3.3.3:4000,4.4.4.4:5000\",\"port\":6000,\"forwardName\":\"Alias B\"}';\n\n const result = parseNyFormatData(input);\n\n assert.equal(result.length, 2);\n assert.equal(result[0].error, undefined);\n assert.equal(result[1].error, undefined);\n assert.deepEqual(result[0].parsed?.dest, [\"2.2.2.2:2000\"]);\n assert.equal(result[0].parsed?.listen_port, 3000);\n assert.equal(result[0].parsed?.name, \"Alias A\");\n assert.deepEqual(result[1].parsed?.dest, [\"3.3.3.3:4000\", \"4.4.4.4:5000\"]);\n assert.equal(result[1].parsed?.listen_port, 6000);\n assert.equal(result[1].parsed?.name, \"Alias B\");\n});\n\ntest(\"convertNyItemToForwardInput maps ny fields correctly\", () => {\n const mapped = convertNyItemToForwardInput({\n dest: [\"1.1.1.1:1111\", \"2.2.2.2:2222\"],\n listen_port: 3333,\n name: \" Forward Name \",\n });\n\n assert.deepEqual(mapped, {\n name: \"Forward Name\",\n inPort: 3333,\n remoteAddr: \"1.1.1.1:1111,2.2.2.2:2222\",\n strategy: \"fifo\",\n });\n});\n\ntest(\"convertNyItemToForwardInput keeps null inPort for auto assignment\", () => {\n const mapped = convertNyItemToForwardInput({\n dest: [\"1.1.1.1:1111\"],\n listen_port: null,\n name: \"No Port\",\n });\n\n assert.deepEqual(mapped, {\n name: \"No Port\",\n inPort: null,\n remoteAddr: \"1.1.1.1:1111\",\n strategy: \"fifo\",\n });\n});\n", "additions": 42, "deletions": 0 } ], "diagnostics": { "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/control_plane.go": [ { "range": { "start": { "line": 39, "character": 25 }, "end": { "line": 39, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 58, "character": 58 }, "end": { "line": 58, "character": 69 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 60, "character": 93 }, "end": { "line": 60, "character": 104 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 62, "character": 22 }, "end": { "line": 62, "character": 33 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 66, "character": 28 }, "end": { "line": 66, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 90, "character": 21 }, "end": { "line": 90, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 101, "character": 3 }, "end": { "line": 101, "character": 12 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop" }, "source": "mapsloop", "message": "Replace m[k]=v loop with maps.Copy" }, { "range": { "start": { "line": 340, "character": 26 }, "end": { "line": 340, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 375, "character": 63 }, "end": { "line": 375, "character": 74 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 383, "character": 73 }, "end": { "line": 383, "character": 84 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 387, "character": 84 }, "end": { "line": 387, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 419, "character": 83 }, "end": { "line": 419, "character": 94 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 423, "character": 94 }, "end": { "line": 423, "character": 105 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 464, "character": 98 }, "end": { "line": 464, "character": 109 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 475, "character": 23 }, "end": { "line": 475, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 520, "character": 26 }, "end": { "line": 520, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 537, "character": 27 }, "end": { "line": 537, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 553, "character": 27 }, "end": { "line": 553, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 573, "character": 28 }, "end": { "line": 573, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 590, "character": 28 }, "end": { "line": 590, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 609, "character": 26 }, "end": { "line": 609, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 624, "character": 26 }, "end": { "line": 624, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 635, "character": 89 }, "end": { "line": 635, "character": 100 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 646, "character": 23 }, "end": { "line": 646, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 690, "character": 25 }, "end": { "line": 690, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 706, "character": 27 }, "end": { "line": 706, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 722, "character": 27 }, "end": { "line": 722, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 742, "character": 28 }, "end": { "line": 742, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 759, "character": 28 }, "end": { "line": 759, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 777, "character": 25 }, "end": { "line": 777, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 790, "character": 25 }, "end": { "line": 790, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 821, "character": 1 }, "end": { "line": 821, "character": 11 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicessort" }, "source": "slicessort", "message": "sort.Slice can be modernized using slices.Sort" }, { "range": { "start": { "line": 891, "character": 84 }, "end": { "line": 891, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 905, "character": 112 }, "end": { "line": 905, "character": 123 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 906, "character": 29 }, "end": { "line": 906, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 920, "character": 135 }, "end": { "line": 920, "character": 146 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 924, "character": 30 }, "end": { "line": 924, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 930, "character": 4 }, "end": { "line": 930, "character": 19 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#minmax" }, "source": "minmax", "message": "if statement can be modernized using max" }, { "range": { "start": { "line": 939, "character": 19 }, "end": { "line": 939, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 964, "character": 5 }, "end": { "line": 964, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#rangeint" }, "source": "rangeint", "message": "for loop can be modernized using range over int" }, { "range": { "start": { "line": 1015, "character": 119 }, "end": { "line": 1015, "character": 130 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1015, "character": 143 }, "end": { "line": 1015, "character": 154 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1016, "character": 20 }, "end": { "line": 1016, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1026, "character": 2 }, "end": { "line": 1026, "character": 9 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop" }, "source": "mapsloop", "message": "Replace m[k]=v loop with maps.Copy" }, { "range": { "start": { "line": 1031, "character": 62 }, "end": { "line": 1031, "character": 73 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1031, "character": 199 }, "end": { "line": 1031, "character": 210 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1044, "character": 60 }, "end": { "line": 1044, "character": 71 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1044, "character": 197 }, "end": { "line": 1044, "character": 208 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1057, "character": 22 }, "end": { "line": 1057, "character": 33 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1094, "character": 64 }, "end": { "line": 1094, "character": 75 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1094, "character": 192 }, "end": { "line": 1094, "character": 203 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1159, "character": 110 }, "end": { "line": 1159, "character": 121 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1166, "character": 72 }, "end": { "line": 1166, "character": 83 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1181, "character": 120 }, "end": { "line": 1181, "character": 131 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1305, "character": 193 }, "end": { "line": 1305, "character": 204 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1307, "character": 31 }, "end": { "line": 1307, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1329, "character": 24 }, "end": { "line": 1329, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1332, "character": 25 }, "end": { "line": 1332, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1335, "character": 26 }, "end": { "line": 1335, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1338, "character": 27 }, "end": { "line": 1338, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1340, "character": 27 }, "end": { "line": 1340, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1348, "character": 34 }, "end": { "line": 1348, "character": 45 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1352, "character": 35 }, "end": { "line": 1352, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1355, "character": 34 }, "end": { "line": 1355, "character": 45 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1358, "character": 36 }, "end": { "line": 1358, "character": 47 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1369, "character": 56 }, "end": { "line": 1369, "character": 67 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1370, "character": 28 }, "end": { "line": 1370, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1372, "character": 35 }, "end": { "line": 1372, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1410, "character": 14 }, "end": { "line": 1410, "character": 25 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1433, "character": 76 }, "end": { "line": 1433, "character": 87 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1438, "character": 25 }, "end": { "line": 1438, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1444, "character": 60 }, "end": { "line": 1444, "character": 71 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1444, "character": 84 }, "end": { "line": 1444, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1445, "character": 19 }, "end": { "line": 1445, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1457, "character": 24 }, "end": { "line": 1457, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/model/model.go": [ { "range": { "start": { "line": 230, "character": 27 }, "end": { "line": 230, "character": 85 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero" }, "source": "omitzero", "message": "Omitempty has no effect on nested struct fields" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/repo/repository_control.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/mutations.go": [ { "range": { "start": { "line": 32, "character": 20 }, "end": { "line": 32, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 86, "character": 20 }, "end": { "line": 86, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 213, "character": 20 }, "end": { "line": 213, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 238, "character": 20 }, "end": { "line": 238, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 286, "character": 20 }, "end": { "line": 286, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 453, "character": 20 }, "end": { "line": 453, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 621, "character": 2 }, "end": { "line": 621, "character": 23 } }, "severity": 3, "code": "default", "codeDescription": { "href": "https://staticcheck.dev/docs/checks/#QF1003" }, "source": "QF1003", "message": "could use tagged switch on row.ChainType" }, { "range": { "start": { "line": 622, "character": 67 }, "end": { "line": 622, "character": 78 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 624, "character": 67 }, "end": { "line": 624, "character": 78 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 625, "character": 68 }, "end": { "line": 625, "character": 79 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 627, "character": 68 }, "end": { "line": 627, "character": 79 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 660, "character": 20 }, "end": { "line": 660, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 843, "character": 46 }, "end": { "line": 843, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 993, "character": 46 }, "end": { "line": 993, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 997, "character": 20 }, "end": { "line": 997, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1022, "character": 18 }, "end": { "line": 1022, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1047, "character": 20 }, "end": { "line": 1047, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1112, "character": 20 }, "end": { "line": 1112, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1202, "character": 20 }, "end": { "line": 1202, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1469, "character": 46 }, "end": { "line": 1469, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1500, "character": 46 }, "end": { "line": 1500, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1531, "character": 46 }, "end": { "line": 1531, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1558, "character": 46 }, "end": { "line": 1558, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1654, "character": 46 }, "end": { "line": 1654, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1658, "character": 20 }, "end": { "line": 1658, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1683, "character": 20 }, "end": { "line": 1683, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1866, "character": 20 }, "end": { "line": 1866, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1885, "character": 20 }, "end": { "line": 1885, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1976, "character": 71 }, "end": { "line": 1976, "character": 82 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2162, "character": 46 }, "end": { "line": 2162, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2210, "character": 20 }, "end": { "line": 2210, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2543, "character": 63 }, "end": { "line": 2543, "character": 74 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2554, "character": 62 }, "end": { "line": 2554, "character": 73 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2575, "character": 153 }, "end": { "line": 2575, "character": 164 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2583, "character": 32 }, "end": { "line": 2583, "character": 43 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2598, "character": 26 }, "end": { "line": 2598, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2602, "character": 38 }, "end": { "line": 2602, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2604, "character": 43 }, "end": { "line": 2604, "character": 54 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2608, "character": 24 }, "end": { "line": 2608, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2615, "character": 19 }, "end": { "line": 2615, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2617, "character": 25 }, "end": { "line": 2617, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2628, "character": 19 }, "end": { "line": 2628, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2630, "character": 23 }, "end": { "line": 2630, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2634, "character": 111 }, "end": { "line": 2634, "character": 122 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2639, "character": 26 }, "end": { "line": 2639, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2643, "character": 38 }, "end": { "line": 2643, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2645, "character": 23 }, "end": { "line": 2645, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2649, "character": 25 }, "end": { "line": 2649, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2654, "character": 33 }, "end": { "line": 2654, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2657, "character": 35 }, "end": { "line": 2657, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2659, "character": 21 }, "end": { "line": 2659, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2786, "character": 84 }, "end": { "line": 2786, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2988, "character": 10 }, "end": { "line": 2988, "character": 23 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq" }, "source": "stringsseq", "message": "Ranging over SplitSeq is more efficient" }, { "range": { "start": { "line": 3073, "character": 50 }, "end": { "line": 3073, "character": 61 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3240, "character": 18 }, "end": { "line": 3240, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3240, "character": 33 }, "end": { "line": 3240, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3244, "character": 20 }, "end": { "line": 3244, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3250, "character": 18 }, "end": { "line": 3250, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3250, "character": 44 }, "end": { "line": 3250, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3255, "character": 26 }, "end": { "line": 3255, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3257, "character": 29 }, "end": { "line": 3257, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3264, "character": 16 }, "end": { "line": 3264, "character": 27 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3283, "character": 13 }, "end": { "line": 3283, "character": 24 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3295, "character": 15 }, "end": { "line": 3295, "character": 26 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3307, "character": 15 }, "end": { "line": 3307, "character": 26 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3319, "character": 23 }, "end": { "line": 3319, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3340, "character": 20 }, "end": { "line": 3340, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3358, "character": 20 }, "end": { "line": 3358, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3375, "character": 1 }, "end": { "line": 3375, "character": 11 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicessort" }, "source": "slicessort", "message": "sort.Slice can be modernized using slices.Sort" }, { "range": { "start": { "line": 3379, "character": 28 }, "end": { "line": 3379, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3386, "character": 27 }, "end": { "line": 3386, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3408, "character": 20 }, "end": { "line": 3408, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/diagnosis_stream.go": [ { "range": { "start": { "line": 15, "character": 6 }, "end": { "line": 15, "character": 17 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 31, "character": 99 }, "end": { "line": 31, "character": 110 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 43, "character": 53 }, "end": { "line": 43, "character": 64 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 73, "character": 136 }, "end": { "line": 73, "character": 147 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 80, "character": 23 }, "end": { "line": 80, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 86, "character": 2 }, "end": { "line": 86, "character": 14 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop" }, "source": "mapsloop", "message": "Replace m[k]=v loop with maps.Copy" }, { "range": { "start": { "line": 93, "character": 44 }, "end": { "line": 93, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 97, "character": 28 }, "end": { "line": 97, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 116, "character": 27 }, "end": { "line": 116, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 146, "character": 28 }, "end": { "line": 146, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 195, "character": 28 }, "end": { "line": 195, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/dual_stack_test.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/repo/repository.go": [ { "range": { "start": { "line": 161, "character": 13 }, "end": { "line": 161, "character": 24 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 356, "character": 77 }, "end": { "line": 356, "character": 88 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 436, "character": 74 }, "end": { "line": 436, "character": 85 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 573, "character": 77 }, "end": { "line": 573, "character": 88 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 583, "character": 77 }, "end": { "line": 583, "character": 88 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 596, "character": 28 }, "end": { "line": 596, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 603, "character": 28 }, "end": { "line": 603, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 611, "character": 29 }, "end": { "line": 611, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 624, "character": 47 }, "end": { "line": 624, "character": 58 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 632, "character": 28 }, "end": { "line": 632, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 634, "character": 35 }, "end": { "line": 634, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 654, "character": 47 }, "end": { "line": 654, "character": 58 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 662, "character": 28 }, "end": { "line": 662, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 664, "character": 35 }, "end": { "line": 664, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 676, "character": 53 }, "end": { "line": 676, "character": 64 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 684, "character": 28 }, "end": { "line": 684, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 686, "character": 21 }, "end": { "line": 686, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 696, "character": 50 }, "end": { "line": 696, "character": 61 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 728, "character": 28 }, "end": { "line": 728, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 734, "character": 21 }, "end": { "line": 734, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 750, "character": 75 }, "end": { "line": 750, "character": 86 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 769, "character": 28 }, "end": { "line": 769, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 771, "character": 35 }, "end": { "line": 771, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 776, "character": 64 }, "end": { "line": 776, "character": 75 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 790, "character": 28 }, "end": { "line": 790, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 792, "character": 35 }, "end": { "line": 792, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 797, "character": 49 }, "end": { "line": 797, "character": 60 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 807, "character": 40 }, "end": { "line": 807, "character": 51 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 811, "character": 31 }, "end": { "line": 811, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 817, "character": 37 }, "end": { "line": 817, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 818, "character": 37 }, "end": { "line": 818, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 819, "character": 39 }, "end": { "line": 819, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 839, "character": 47 }, "end": { "line": 839, "character": 58 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 856, "character": 24 }, "end": { "line": 856, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 873, "character": 54 }, "end": { "line": 873, "character": 65 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 879, "character": 51 }, "end": { "line": 879, "character": 62 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 883, "character": 56 }, "end": { "line": 883, "character": 67 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 897, "character": 33 }, "end": { "line": 897, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 910, "character": 29 }, "end": { "line": 910, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 921, "character": 54 }, "end": { "line": 921, "character": 65 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 929, "character": 29 }, "end": { "line": 929, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 935, "character": 37 }, "end": { "line": 935, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 944, "character": 52 }, "end": { "line": 944, "character": 63 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 952, "character": 29 }, "end": { "line": 952, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 958, "character": 37 }, "end": { "line": 958, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 967, "character": 58 }, "end": { "line": 967, "character": 69 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 990, "character": 29 }, "end": { "line": 990, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 992, "character": 37 }, "end": { "line": 992, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1064, "character": 84 }, "end": { "line": 1064, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1130, "character": 27 }, "end": { "line": 1130, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1146, "character": 83 }, "end": { "line": 1146, "character": 94 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1170, "character": 90 }, "end": { "line": 1170, "character": 101 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1184, "character": 85 }, "end": { "line": 1184, "character": 96 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1404, "character": 85 }, "end": { "line": 1404, "character": 96 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1421, "character": 130 }, "end": { "line": 1421, "character": 141 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1442, "character": 21 }, "end": { "line": 1442, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2382, "character": 23 }, "end": { "line": 2382, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2397, "character": 23 }, "end": { "line": 2397, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2503, "character": 36 }, "end": { "line": 2503, "character": 47 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2591, "character": 27 }, "end": { "line": 2591, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2713, "character": 16 }, "end": { "line": 2713, "character": 29 } }, "severity": 4, "code": "stringscut", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#stringscut" }, "source": "stringscut", "message": "strings.Index can be simplified using strings.Cut" }, { "range": { "start": { "line": 2757, "character": 15 }, "end": { "line": 2757, "character": 28 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq" }, "source": "stringsseq", "message": "Ranging over SplitSeq is more efficient" }, { "range": { "start": { "line": 2803, "character": 38 }, "end": { "line": 2803, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2810, "character": 38 }, "end": { "line": 2810, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2818, "character": 36 }, "end": { "line": 2818, "character": 47 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/repo/repository_mutations.go": [ { "range": { "start": { "line": 78, "character": 21 }, "end": { "line": 78, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 96, "character": 21 }, "end": { "line": 96, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 113, "character": 21 }, "end": { "line": 113, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 156, "character": 21 }, "end": { "line": 156, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 163, "character": 21 }, "end": { "line": 163, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 172, "character": 21 }, "end": { "line": 172, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 198, "character": 116 }, "end": { "line": 198, "character": 127 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 198, "character": 277 }, "end": { "line": 198, "character": 288 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 241, "character": 119 }, "end": { "line": 241, "character": 130 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 247, "character": 21 }, "end": { "line": 247, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 294, "character": 21 }, "end": { "line": 294, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 371, "character": 21 }, "end": { "line": 371, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 380, "character": 21 }, "end": { "line": 380, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 559, "character": 107 }, "end": { "line": 559, "character": 118 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 565, "character": 21 }, "end": { "line": 565, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 601, "character": 70 }, "end": { "line": 601, "character": 81 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 620, "character": 62 }, "end": { "line": 620, "character": 73 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 626, "character": 21 }, "end": { "line": 626, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 648, "character": 122 }, "end": { "line": 648, "character": 133 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 654, "character": 21 }, "end": { "line": 654, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 670, "character": 21 }, "end": { "line": 670, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 679, "character": 21 }, "end": { "line": 679, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 722, "character": 149 }, "end": { "line": 722, "character": 160 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 728, "character": 21 }, "end": { "line": 728, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 786, "character": 23 }, "end": { "line": 786, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 837, "character": 23 }, "end": { "line": 837, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1179, "character": 192 }, "end": { "line": 1179, "character": 203 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1228, "character": 80 }, "end": { "line": 1228, "character": 91 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1237, "character": 137 }, "end": { "line": 1237, "character": 148 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1265, "character": 31 }, "end": { "line": 1265, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1291, "character": 30 }, "end": { "line": 1291, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1334, "character": 27 }, "end": { "line": 1334, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1363, "character": 10 }, "end": { "line": 1363, "character": 23 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq" }, "source": "stringsseq", "message": "Ranging over SplitSeq is more efficient" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/client/federation.go": [ { "range": { "start": { "line": 81, "character": 13 }, "end": { "line": 81, "character": 24 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 88, "character": 20 }, "end": { "line": 88, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 147, "character": 23 }, "end": { "line": 147, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 305, "character": 112 }, "end": { "line": 305, "character": 123 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 332, "character": 18 }, "end": { "line": 332, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/tests/contract/diagnosis_contract_test.go": [ { "range": { "start": { "line": 103, "character": 38 }, "end": { "line": 103, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 107, "character": 39 }, "end": { "line": 107, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 116, "character": 31 }, "end": { "line": 116, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 156, "character": 38 }, "end": { "line": 156, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 160, "character": 39 }, "end": { "line": 160, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 169, "character": 31 }, "end": { "line": 169, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 272, "character": 37 }, "end": { "line": 272, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 276, "character": 38 }, "end": { "line": 276, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 284, "character": 30 }, "end": { "line": 284, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 326, "character": 44 }, "end": { "line": 326, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 330, "character": 21 }, "end": { "line": 330, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 333, "character": 44 }, "end": { "line": 333, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 338, "character": 44 }, "end": { "line": 338, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 343, "character": 44 }, "end": { "line": 343, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 348, "character": 43 }, "end": { "line": 348, "character": 54 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 351, "character": 22 }, "end": { "line": 351, "character": 33 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 431, "character": 37 }, "end": { "line": 431, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 435, "character": 38 }, "end": { "line": 435, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 442, "character": 30 }, "end": { "line": 442, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 531, "character": 38 }, "end": { "line": 531, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 535, "character": 39 }, "end": { "line": 535, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 543, "character": 31 }, "end": { "line": 543, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 584, "character": 24 }, "end": { "line": 584, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 592, "character": 41 }, "end": { "line": 592, "character": 52 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 596, "character": 33 }, "end": { "line": 596, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 601, "character": 32 }, "end": { "line": 601, "character": 43 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/control_plane_test.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/jobs_test.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/reconstruct_tunnel_state_test.go": [] }, "truncated": false }, "time": { "start": 1772527891668, "end": 1772527891699 } }, "id": "prt_cb2e507740019Udyx7KE3eZ55o", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e4ea4100261mRk4AWiBuIa1" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "b1ab0fccac05804f1b184843da11ca22f9237952", "cost": 0, "tokens": { "total": 37422, "input": 36564, "output": 858, "reasoning": 49, "cache": { "read": 0, "write": 0 } }, "id": "prt_cb2e53582001aVUbfY31nJPseR", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e4ea4100261mRk4AWiBuIa1" }, { "type": "patch", "hash": "efb3d3e844a61777bd618fab10e58a841c932b7c", "files": [ "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/src/pages/forward/import-format.test.ts" ], "id": "prt_cb2e535e10012s27UihvfHP83O", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e4ea4100261mRk4AWiBuIa1" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527891944, "completed": 1772527915958 }, "parentID": "msg_cb2e428a7001Ejj8LNAqWjo6fE", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "build", "agent": "build", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 37719, "input": 23705, "output": 318, "reasoning": 117, "cache": { "read": 13696, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e535e80022wtpG2B40xE1y9", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "b1ab0fccac05804f1b184843da11ca22f9237952", "id": "prt_cb2e55223001e6ywWR3qRXm2MC", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e535e80022wtpG2B40xE1y9" }, { "type": "reasoning", "text": "**Creating completed plan doc and running tests**", "time": { "start": 1772527899171, "end": 1772527915882 }, "id": "prt_cb2e55223002YWR7I6miHqj0zV", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e535e80022wtpG2B40xE1y9" }, { "type": "tool", "callID": "call_Q8AS0GN5qvLTqRwyJOlNtxTh", "tool": "apply_patch", "state": { "status": "completed", "input": { "patchText": "*** Begin Patch\n*** Add File: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/plans/002-ny-import-tunnel-selection-compat.md\n+- [x] Review current forward import flow and confirm ny import uses tunnel selection\n+- [x] Define ny compatibility update with tunnel-first behavior and auto port assignment fallback\n+- [x] Update ny parser to accept alias fields and optional `listen_port`\n+- [x] Keep import execution bound to selected tunnel and remove entry-selection dependency from ux copy\n+- [x] Update ny import help text to document optional port auto assignment\n+- [x] Add parser tests for alias-field compatibility and missing-port auto assignment\n+- [x] Validate updated import parser tests locally\n*** End Patch" }, "output": "Success. Updated the following files:\nA plans/002-ny-import-tunnel-selection-compat.md", "title": "Success. Updated the following files:\nA plans/002-ny-import-tunnel-selection-compat.md", "metadata": { "diff": "Index: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/plans/002-ny-import-tunnel-selection-compat.md\n===================================================================\n--- /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/plans/002-ny-import-tunnel-selection-compat.md\n+++ /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/plans/002-ny-import-tunnel-selection-compat.md\n@@ -0,0 +1,7 @@\n+- [x] Review current forward import flow and confirm ny import uses tunnel selection\n+- [x] Define ny compatibility update with tunnel-first behavior and auto port assignment fallback\n+- [x] Update ny parser to accept alias fields and optional `listen_port`\n+- [x] Keep import execution bound to selected tunnel and remove entry-selection dependency from ux copy\n+- [x] Update ny import help text to document optional port auto assignment\n+- [x] Add parser tests for alias-field compatibility and missing-port auto assignment\n+- [x] Validate updated import parser tests locally\n\n", "files": [ { "filePath": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/plans/002-ny-import-tunnel-selection-compat.md", "relativePath": "plans/002-ny-import-tunnel-selection-compat.md", "type": "add", "diff": "Index: /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/plans/002-ny-import-tunnel-selection-compat.md\n===================================================================\n--- /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/plans/002-ny-import-tunnel-selection-compat.md\n+++ /Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/plans/002-ny-import-tunnel-selection-compat.md\n@@ -0,0 +1,7 @@\n+- [x] Review current forward import flow and confirm ny import uses tunnel selection\n+- [x] Define ny compatibility update with tunnel-first behavior and auto port assignment fallback\n+- [x] Update ny parser to accept alias fields and optional `listen_port`\n+- [x] Keep import execution bound to selected tunnel and remove entry-selection dependency from ux copy\n+- [x] Update ny import help text to document optional port auto assignment\n+- [x] Add parser tests for alias-field compatibility and missing-port auto assignment\n+- [x] Validate updated import parser tests locally\n", "before": "", "after": "- [x] Review current forward import flow and confirm ny import uses tunnel selection\n- [x] Define ny compatibility update with tunnel-first behavior and auto port assignment fallback\n- [x] Update ny parser to accept alias fields and optional `listen_port`\n- [x] Keep import execution bound to selected tunnel and remove entry-selection dependency from ux copy\n- [x] Update ny import help text to document optional port auto assignment\n- [x] Add parser tests for alias-field compatibility and missing-port auto assignment\n- [x] Validate updated import parser tests locally\n", "additions": 7, "deletions": 0 } ], "diagnostics": { "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/control_plane.go": [ { "range": { "start": { "line": 39, "character": 25 }, "end": { "line": 39, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 58, "character": 58 }, "end": { "line": 58, "character": 69 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 60, "character": 93 }, "end": { "line": 60, "character": 104 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 62, "character": 22 }, "end": { "line": 62, "character": 33 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 66, "character": 28 }, "end": { "line": 66, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 90, "character": 21 }, "end": { "line": 90, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 101, "character": 3 }, "end": { "line": 101, "character": 12 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop" }, "source": "mapsloop", "message": "Replace m[k]=v loop with maps.Copy" }, { "range": { "start": { "line": 340, "character": 26 }, "end": { "line": 340, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 375, "character": 63 }, "end": { "line": 375, "character": 74 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 383, "character": 73 }, "end": { "line": 383, "character": 84 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 387, "character": 84 }, "end": { "line": 387, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 419, "character": 83 }, "end": { "line": 419, "character": 94 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 423, "character": 94 }, "end": { "line": 423, "character": 105 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 464, "character": 98 }, "end": { "line": 464, "character": 109 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 475, "character": 23 }, "end": { "line": 475, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 520, "character": 26 }, "end": { "line": 520, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 537, "character": 27 }, "end": { "line": 537, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 553, "character": 27 }, "end": { "line": 553, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 573, "character": 28 }, "end": { "line": 573, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 590, "character": 28 }, "end": { "line": 590, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 609, "character": 26 }, "end": { "line": 609, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 624, "character": 26 }, "end": { "line": 624, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 635, "character": 89 }, "end": { "line": 635, "character": 100 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 646, "character": 23 }, "end": { "line": 646, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 690, "character": 25 }, "end": { "line": 690, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 706, "character": 27 }, "end": { "line": 706, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 722, "character": 27 }, "end": { "line": 722, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 742, "character": 28 }, "end": { "line": 742, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 759, "character": 28 }, "end": { "line": 759, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 777, "character": 25 }, "end": { "line": 777, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 790, "character": 25 }, "end": { "line": 790, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 821, "character": 1 }, "end": { "line": 821, "character": 11 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicessort" }, "source": "slicessort", "message": "sort.Slice can be modernized using slices.Sort" }, { "range": { "start": { "line": 891, "character": 84 }, "end": { "line": 891, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 905, "character": 112 }, "end": { "line": 905, "character": 123 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 906, "character": 29 }, "end": { "line": 906, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 920, "character": 135 }, "end": { "line": 920, "character": 146 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 924, "character": 30 }, "end": { "line": 924, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 930, "character": 4 }, "end": { "line": 930, "character": 19 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#minmax" }, "source": "minmax", "message": "if statement can be modernized using max" }, { "range": { "start": { "line": 939, "character": 19 }, "end": { "line": 939, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 964, "character": 5 }, "end": { "line": 964, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#rangeint" }, "source": "rangeint", "message": "for loop can be modernized using range over int" }, { "range": { "start": { "line": 1015, "character": 119 }, "end": { "line": 1015, "character": 130 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1015, "character": 143 }, "end": { "line": 1015, "character": 154 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1016, "character": 20 }, "end": { "line": 1016, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1026, "character": 2 }, "end": { "line": 1026, "character": 9 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop" }, "source": "mapsloop", "message": "Replace m[k]=v loop with maps.Copy" }, { "range": { "start": { "line": 1031, "character": 62 }, "end": { "line": 1031, "character": 73 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1031, "character": 199 }, "end": { "line": 1031, "character": 210 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1044, "character": 60 }, "end": { "line": 1044, "character": 71 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1044, "character": 197 }, "end": { "line": 1044, "character": 208 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1057, "character": 22 }, "end": { "line": 1057, "character": 33 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1094, "character": 64 }, "end": { "line": 1094, "character": 75 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1094, "character": 192 }, "end": { "line": 1094, "character": 203 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1159, "character": 110 }, "end": { "line": 1159, "character": 121 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1166, "character": 72 }, "end": { "line": 1166, "character": 83 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1181, "character": 120 }, "end": { "line": 1181, "character": 131 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1305, "character": 193 }, "end": { "line": 1305, "character": 204 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1307, "character": 31 }, "end": { "line": 1307, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1329, "character": 24 }, "end": { "line": 1329, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1332, "character": 25 }, "end": { "line": 1332, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1335, "character": 26 }, "end": { "line": 1335, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1338, "character": 27 }, "end": { "line": 1338, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1340, "character": 27 }, "end": { "line": 1340, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1348, "character": 34 }, "end": { "line": 1348, "character": 45 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1352, "character": 35 }, "end": { "line": 1352, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1355, "character": 34 }, "end": { "line": 1355, "character": 45 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1358, "character": 36 }, "end": { "line": 1358, "character": 47 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1369, "character": 56 }, "end": { "line": 1369, "character": 67 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1370, "character": 28 }, "end": { "line": 1370, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1372, "character": 35 }, "end": { "line": 1372, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1410, "character": 14 }, "end": { "line": 1410, "character": 25 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1433, "character": 76 }, "end": { "line": 1433, "character": 87 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1438, "character": 25 }, "end": { "line": 1438, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1444, "character": 60 }, "end": { "line": 1444, "character": 71 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1444, "character": 84 }, "end": { "line": 1444, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1445, "character": 19 }, "end": { "line": 1445, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1457, "character": 24 }, "end": { "line": 1457, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/model/model.go": [ { "range": { "start": { "line": 230, "character": 27 }, "end": { "line": 230, "character": 85 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero" }, "source": "omitzero", "message": "Omitempty has no effect on nested struct fields" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/repo/repository_control.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/mutations.go": [ { "range": { "start": { "line": 32, "character": 20 }, "end": { "line": 32, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 86, "character": 20 }, "end": { "line": 86, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 213, "character": 20 }, "end": { "line": 213, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 238, "character": 20 }, "end": { "line": 238, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 286, "character": 20 }, "end": { "line": 286, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 453, "character": 20 }, "end": { "line": 453, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 621, "character": 2 }, "end": { "line": 621, "character": 23 } }, "severity": 3, "code": "default", "codeDescription": { "href": "https://staticcheck.dev/docs/checks/#QF1003" }, "source": "QF1003", "message": "could use tagged switch on row.ChainType" }, { "range": { "start": { "line": 622, "character": 67 }, "end": { "line": 622, "character": 78 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 624, "character": 67 }, "end": { "line": 624, "character": 78 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 625, "character": 68 }, "end": { "line": 625, "character": 79 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 627, "character": 68 }, "end": { "line": 627, "character": 79 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 660, "character": 20 }, "end": { "line": 660, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 843, "character": 46 }, "end": { "line": 843, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 993, "character": 46 }, "end": { "line": 993, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 997, "character": 20 }, "end": { "line": 997, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1022, "character": 18 }, "end": { "line": 1022, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1047, "character": 20 }, "end": { "line": 1047, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1112, "character": 20 }, "end": { "line": 1112, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1202, "character": 20 }, "end": { "line": 1202, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1469, "character": 46 }, "end": { "line": 1469, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1500, "character": 46 }, "end": { "line": 1500, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1531, "character": 46 }, "end": { "line": 1531, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1558, "character": 46 }, "end": { "line": 1558, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1654, "character": 46 }, "end": { "line": 1654, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1658, "character": 20 }, "end": { "line": 1658, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1683, "character": 20 }, "end": { "line": 1683, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1866, "character": 20 }, "end": { "line": 1866, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1885, "character": 20 }, "end": { "line": 1885, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1976, "character": 71 }, "end": { "line": 1976, "character": 82 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2162, "character": 46 }, "end": { "line": 2162, "character": 57 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2210, "character": 20 }, "end": { "line": 2210, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2543, "character": 63 }, "end": { "line": 2543, "character": 74 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2554, "character": 62 }, "end": { "line": 2554, "character": 73 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2575, "character": 153 }, "end": { "line": 2575, "character": 164 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2583, "character": 32 }, "end": { "line": 2583, "character": 43 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2598, "character": 26 }, "end": { "line": 2598, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2602, "character": 38 }, "end": { "line": 2602, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2604, "character": 43 }, "end": { "line": 2604, "character": 54 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2608, "character": 24 }, "end": { "line": 2608, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2615, "character": 19 }, "end": { "line": 2615, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2617, "character": 25 }, "end": { "line": 2617, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2628, "character": 19 }, "end": { "line": 2628, "character": 30 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2630, "character": 23 }, "end": { "line": 2630, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2634, "character": 111 }, "end": { "line": 2634, "character": 122 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2639, "character": 26 }, "end": { "line": 2639, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2643, "character": 38 }, "end": { "line": 2643, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2645, "character": 23 }, "end": { "line": 2645, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2649, "character": 25 }, "end": { "line": 2649, "character": 36 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2654, "character": 33 }, "end": { "line": 2654, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2657, "character": 35 }, "end": { "line": 2657, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2659, "character": 21 }, "end": { "line": 2659, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2786, "character": 84 }, "end": { "line": 2786, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2988, "character": 10 }, "end": { "line": 2988, "character": 23 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq" }, "source": "stringsseq", "message": "Ranging over SplitSeq is more efficient" }, { "range": { "start": { "line": 3073, "character": 50 }, "end": { "line": 3073, "character": 61 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3240, "character": 18 }, "end": { "line": 3240, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3240, "character": 33 }, "end": { "line": 3240, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3244, "character": 20 }, "end": { "line": 3244, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3250, "character": 18 }, "end": { "line": 3250, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3250, "character": 44 }, "end": { "line": 3250, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3255, "character": 26 }, "end": { "line": 3255, "character": 37 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3257, "character": 29 }, "end": { "line": 3257, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3264, "character": 16 }, "end": { "line": 3264, "character": 27 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3283, "character": 13 }, "end": { "line": 3283, "character": 24 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3295, "character": 15 }, "end": { "line": 3295, "character": 26 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3307, "character": 15 }, "end": { "line": 3307, "character": 26 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3319, "character": 23 }, "end": { "line": 3319, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3340, "character": 20 }, "end": { "line": 3340, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3358, "character": 20 }, "end": { "line": 3358, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3375, "character": 1 }, "end": { "line": 3375, "character": 11 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicessort" }, "source": "slicessort", "message": "sort.Slice can be modernized using slices.Sort" }, { "range": { "start": { "line": 3379, "character": 28 }, "end": { "line": 3379, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3386, "character": 27 }, "end": { "line": 3386, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 3408, "character": 20 }, "end": { "line": 3408, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/diagnosis_stream.go": [ { "range": { "start": { "line": 15, "character": 6 }, "end": { "line": 15, "character": 17 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 31, "character": 99 }, "end": { "line": 31, "character": 110 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 43, "character": 53 }, "end": { "line": 43, "character": 64 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 73, "character": 136 }, "end": { "line": 73, "character": 147 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 80, "character": 23 }, "end": { "line": 80, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 86, "character": 2 }, "end": { "line": 86, "character": 14 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop" }, "source": "mapsloop", "message": "Replace m[k]=v loop with maps.Copy" }, { "range": { "start": { "line": 93, "character": 44 }, "end": { "line": 93, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 97, "character": 28 }, "end": { "line": 97, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 116, "character": 27 }, "end": { "line": 116, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 146, "character": 28 }, "end": { "line": 146, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 195, "character": 28 }, "end": { "line": 195, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/dual_stack_test.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/repo/repository.go": [ { "range": { "start": { "line": 161, "character": 13 }, "end": { "line": 161, "character": 24 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 356, "character": 77 }, "end": { "line": 356, "character": 88 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 436, "character": 74 }, "end": { "line": 436, "character": 85 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 573, "character": 77 }, "end": { "line": 573, "character": 88 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 583, "character": 77 }, "end": { "line": 583, "character": 88 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 596, "character": 28 }, "end": { "line": 596, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 603, "character": 28 }, "end": { "line": 603, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 611, "character": 29 }, "end": { "line": 611, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 624, "character": 47 }, "end": { "line": 624, "character": 58 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 632, "character": 28 }, "end": { "line": 632, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 634, "character": 35 }, "end": { "line": 634, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 654, "character": 47 }, "end": { "line": 654, "character": 58 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 662, "character": 28 }, "end": { "line": 662, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 664, "character": 35 }, "end": { "line": 664, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 676, "character": 53 }, "end": { "line": 676, "character": 64 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 684, "character": 28 }, "end": { "line": 684, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 686, "character": 21 }, "end": { "line": 686, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 696, "character": 50 }, "end": { "line": 696, "character": 61 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 728, "character": 28 }, "end": { "line": 728, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 734, "character": 21 }, "end": { "line": 734, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 750, "character": 75 }, "end": { "line": 750, "character": 86 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 769, "character": 28 }, "end": { "line": 769, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 771, "character": 35 }, "end": { "line": 771, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 776, "character": 64 }, "end": { "line": 776, "character": 75 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 790, "character": 28 }, "end": { "line": 790, "character": 39 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 792, "character": 35 }, "end": { "line": 792, "character": 46 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 797, "character": 49 }, "end": { "line": 797, "character": 60 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 807, "character": 40 }, "end": { "line": 807, "character": 51 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 811, "character": 31 }, "end": { "line": 811, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 817, "character": 37 }, "end": { "line": 817, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 818, "character": 37 }, "end": { "line": 818, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 819, "character": 39 }, "end": { "line": 819, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 839, "character": 47 }, "end": { "line": 839, "character": 58 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 856, "character": 24 }, "end": { "line": 856, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 873, "character": 54 }, "end": { "line": 873, "character": 65 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 879, "character": 51 }, "end": { "line": 879, "character": 62 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 883, "character": 56 }, "end": { "line": 883, "character": 67 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 897, "character": 33 }, "end": { "line": 897, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 910, "character": 29 }, "end": { "line": 910, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 921, "character": 54 }, "end": { "line": 921, "character": 65 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 929, "character": 29 }, "end": { "line": 929, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 935, "character": 37 }, "end": { "line": 935, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 944, "character": 52 }, "end": { "line": 944, "character": 63 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 952, "character": 29 }, "end": { "line": 952, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 958, "character": 37 }, "end": { "line": 958, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 967, "character": 58 }, "end": { "line": 967, "character": 69 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 990, "character": 29 }, "end": { "line": 990, "character": 40 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 992, "character": 37 }, "end": { "line": 992, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1064, "character": 84 }, "end": { "line": 1064, "character": 95 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1130, "character": 27 }, "end": { "line": 1130, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1146, "character": 83 }, "end": { "line": 1146, "character": 94 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1170, "character": 90 }, "end": { "line": 1170, "character": 101 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1184, "character": 85 }, "end": { "line": 1184, "character": 96 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1404, "character": 85 }, "end": { "line": 1404, "character": 96 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1421, "character": 130 }, "end": { "line": 1421, "character": 141 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1442, "character": 21 }, "end": { "line": 1442, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2382, "character": 23 }, "end": { "line": 2382, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2397, "character": 23 }, "end": { "line": 2397, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2503, "character": 36 }, "end": { "line": 2503, "character": 47 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2591, "character": 27 }, "end": { "line": 2591, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2713, "character": 16 }, "end": { "line": 2713, "character": 29 } }, "severity": 4, "code": "stringscut", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#stringscut" }, "source": "stringscut", "message": "strings.Index can be simplified using strings.Cut" }, { "range": { "start": { "line": 2757, "character": 15 }, "end": { "line": 2757, "character": 28 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq" }, "source": "stringsseq", "message": "Ranging over SplitSeq is more efficient" }, { "range": { "start": { "line": 2803, "character": 38 }, "end": { "line": 2803, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2810, "character": 38 }, "end": { "line": 2810, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 2818, "character": 36 }, "end": { "line": 2818, "character": 47 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/store/repo/repository_mutations.go": [ { "range": { "start": { "line": 78, "character": 21 }, "end": { "line": 78, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 96, "character": 21 }, "end": { "line": 96, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 113, "character": 21 }, "end": { "line": 113, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 156, "character": 21 }, "end": { "line": 156, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 163, "character": 21 }, "end": { "line": 163, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 172, "character": 21 }, "end": { "line": 172, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 198, "character": 116 }, "end": { "line": 198, "character": 127 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 198, "character": 277 }, "end": { "line": 198, "character": 288 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 241, "character": 119 }, "end": { "line": 241, "character": 130 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 247, "character": 21 }, "end": { "line": 247, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 294, "character": 21 }, "end": { "line": 294, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 371, "character": 21 }, "end": { "line": 371, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 380, "character": 21 }, "end": { "line": 380, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 559, "character": 107 }, "end": { "line": 559, "character": 118 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 565, "character": 21 }, "end": { "line": 565, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 601, "character": 70 }, "end": { "line": 601, "character": 81 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 620, "character": 62 }, "end": { "line": 620, "character": 73 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 626, "character": 21 }, "end": { "line": 626, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 648, "character": 122 }, "end": { "line": 648, "character": 133 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 654, "character": 21 }, "end": { "line": 654, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 670, "character": 21 }, "end": { "line": 670, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 679, "character": 21 }, "end": { "line": 679, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 722, "character": 149 }, "end": { "line": 722, "character": 160 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 728, "character": 21 }, "end": { "line": 728, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 786, "character": 23 }, "end": { "line": 786, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 837, "character": 23 }, "end": { "line": 837, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1179, "character": 192 }, "end": { "line": 1179, "character": 203 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1228, "character": 80 }, "end": { "line": 1228, "character": 91 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1237, "character": 137 }, "end": { "line": 1237, "character": 148 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1265, "character": 31 }, "end": { "line": 1265, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1291, "character": 30 }, "end": { "line": 1291, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1334, "character": 27 }, "end": { "line": 1334, "character": 38 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 1363, "character": 10 }, "end": { "line": 1363, "character": 23 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq" }, "source": "stringsseq", "message": "Ranging over SplitSeq is more efficient" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/client/federation.go": [ { "range": { "start": { "line": 81, "character": 13 }, "end": { "line": 81, "character": 24 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 88, "character": 20 }, "end": { "line": 88, "character": 31 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 147, "character": 23 }, "end": { "line": 147, "character": 34 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 305, "character": 112 }, "end": { "line": 305, "character": 123 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 332, "character": 18 }, "end": { "line": 332, "character": 29 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/tests/contract/diagnosis_contract_test.go": [ { "range": { "start": { "line": 103, "character": 38 }, "end": { "line": 103, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 107, "character": 39 }, "end": { "line": 107, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 116, "character": 31 }, "end": { "line": 116, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 156, "character": 38 }, "end": { "line": 156, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 160, "character": 39 }, "end": { "line": 160, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 169, "character": 31 }, "end": { "line": 169, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 272, "character": 37 }, "end": { "line": 272, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 276, "character": 38 }, "end": { "line": 276, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 284, "character": 30 }, "end": { "line": 284, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 326, "character": 44 }, "end": { "line": 326, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 330, "character": 21 }, "end": { "line": 330, "character": 32 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 333, "character": 44 }, "end": { "line": 333, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 338, "character": 44 }, "end": { "line": 338, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 343, "character": 44 }, "end": { "line": 343, "character": 55 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 348, "character": 43 }, "end": { "line": 348, "character": 54 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 351, "character": 22 }, "end": { "line": 351, "character": 33 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 431, "character": 37 }, "end": { "line": 431, "character": 48 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 435, "character": 38 }, "end": { "line": 435, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 442, "character": 30 }, "end": { "line": 442, "character": 41 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 531, "character": 38 }, "end": { "line": 531, "character": 49 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 535, "character": 39 }, "end": { "line": 535, "character": 50 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 543, "character": 31 }, "end": { "line": 543, "character": 42 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 584, "character": 24 }, "end": { "line": 584, "character": 35 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 592, "character": 41 }, "end": { "line": 592, "character": 52 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 596, "character": 33 }, "end": { "line": 596, "character": 44 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" }, { "range": { "start": { "line": 601, "character": 32 }, "end": { "line": 601, "character": 43 } }, "severity": 4, "code": "default", "codeDescription": { "href": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any" }, "source": "any", "message": "interface{} can be replaced by any" } ], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/control_plane_test.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/jobs_test.go": [], "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/go-backend/internal/http/handler/reconstruct_tunnel_state_test.go": [] }, "truncated": false }, "time": { "start": 1772527915719, "end": 1772527915757 } }, "id": "prt_cb2e552e5001spNr3rJFH5rtK6", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e535e80022wtpG2B40xE1y9" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "a065b6a10de4bd0df5c764079bb7633783a676df", "cost": 0, "tokens": { "total": 37719, "input": 23705, "output": 318, "reasoning": 117, "cache": { "read": 13696, "write": 0 } }, "id": "prt_cb2e5936b001v21N6fA91vWpGj", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e535e80022wtpG2B40xE1y9" }, { "type": "patch", "hash": "b1ab0fccac05804f1b184843da11ca22f9237952", "files": [ "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/plans/002-ny-import-tunnel-selection-compat.md" ], "id": "prt_cb2e593b2001fQIavkRakszoIf", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e535e80022wtpG2B40xE1y9" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527915962, "completed": 1772527925311 }, "parentID": "msg_cb2e428a7001Ejj8LNAqWjo6fE", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "build", "agent": "build", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 37770, "input": 23935, "output": 139, "reasoning": 67, "cache": { "read": 13696, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e593ba0028hetOWz0GsZMSk", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "a065b6a10de4bd0df5c764079bb7633783a676df", "id": "prt_cb2e5b68a001iheUliXELGKcoG", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e593ba0028hetOWz0GsZMSk" }, { "type": "reasoning", "text": "**Checking test run setup**", "time": { "start": 1772527924875, "end": 1772527925240 }, "id": "prt_cb2e5b68b001nz1zvtMFk09ZEk", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e593ba0028hetOWz0GsZMSk" }, { "type": "tool", "callID": "call_7Gud9iB7MuxOWMcWV3t7velo", "tool": "read", "state": { "status": "completed", "input": { "filePath": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/package.json", "offset": 1, "limit": 220 }, "output": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend/package.json\nfile\n1: {\n2: \"name\": \"flvx\",\n3: \"private\": true,\n4: \"version\": \"0.0.0\",\n5: \"type\": \"module\",\n6: \"scripts\": {\n7: \"dev\": \"vite\",\n8: \"build\": \"tsc && vite build\",\n9: \"lint\": \"eslint --fix\",\n10: \"preview\": \"vite preview\"\n11: },\n12: \"dependencies\": {\n13: \"@dnd-kit/core\": \"^6.3.1\",\n14: \"@dnd-kit/sortable\": \"^10.0.0\",\n15: \"@dnd-kit/utilities\": \"^3.2.2\",\n16: \"@internationalized/date\": \"^3.10.0\",\n17: \"@marsidev/react-turnstile\": \"^1.1.0\",\n18: \"@radix-ui/react-accordion\": \"^1.2.0\",\n19: \"@radix-ui/react-checkbox\": \"^1.1.0\",\n20: \"@radix-ui/react-dialog\": \"^1.1.0\",\n21: \"@radix-ui/react-dropdown-menu\": \"^2.1.0\",\n22: \"@radix-ui/react-label\": \"^2.1.0\",\n23: \"@radix-ui/react-progress\": \"^1.1.0\",\n24: \"@radix-ui/react-radio-group\": \"^1.2.0\",\n25: \"@radix-ui/react-select\": \"^2.1.0\",\n26: \"@radix-ui/react-separator\": \"^1.1.0\",\n27: \"@radix-ui/react-slot\": \"^1.1.0\",\n28: \"@radix-ui/react-switch\": \"^1.1.0\",\n29: \"@radix-ui/react-tabs\": \"^1.1.0\",\n30: \"@react-aria/i18n\": \"^3.12.12\",\n31: \"@react-aria/visually-hidden\": \"3.8.25\",\n32: \"@react-types/shared\": \"3.30.0\",\n33: \"@tailwindcss/postcss\": \"4.1.11\",\n34: \"@tailwindcss/vite\": \"^4.1.18\",\n35: \"@types/react-beautiful-dnd\": \"^13.1.8\",\n36: \"axios\": \"^1.11.0\",\n37: \"class-variance-authority\": \"^0.7.1\",\n38: \"clsx\": \"2.1.1\",\n39: \"framer-motion\": \"11.18.2\",\n40: \"lucide-react\": \"^0.542.0\",\n41: \"react\": \"18.3.1\",\n42: \"react-beautiful-dnd\": \"^13.1.1\",\n43: \"react-dom\": \"18.3.1\",\n44: \"react-hot-toast\": \"^2.5.2\",\n45: \"react-is\": \"^19.2.4\",\n46: \"react-markdown\": \"^10.1.0\",\n47: \"react-router-dom\": \"6.30.3\",\n48: \"recharts\": \"^3.1.1\",\n49: \"rehype-sanitize\": \"^6.0.0\",\n50: \"remark-gfm\": \"^4.0.1\",\n51: \"sonner\": \"^2.0.6\",\n52: \"tailwind-merge\": \"^2.5.5\",\n53: \"tailwind-variants\": \"1.0.0\",\n54: \"tailwindcss\": \"4.1.11\",\n55: \"tw-animate-css\": \"^1.3.0\"\n56: },\n57: \"devDependencies\": {\n58: \"@eslint/compat\": \"1.2.8\",\n59: \"@eslint/eslintrc\": \"3.3.1\",\n60: \"@eslint/js\": \"9.39.2\",\n61: \"@types/node\": \"^24.3.0\",\n62: \"@types/react\": \"18.3.3\",\n63: \"@types/react-dom\": \"18.3.0\",\n64: \"@typescript-eslint/eslint-plugin\": \"8.31.1\",\n65: \"@typescript-eslint/parser\": \"8.31.1\",\n66: \"@vitejs/plugin-react\": \"^5.1.3\",\n67: \"eslint\": \"9.39.2\",\n68: \"eslint-config-prettier\": \"9.1.0\",\n69: \"eslint-plugin-import\": \"2.31.0\",\n70: \"eslint-plugin-jsx-a11y\": \"6.10.2\",\n71: \"eslint-plugin-node\": \"11.1.0\",\n72: \"eslint-plugin-prettier\": \"5.2.1\",\n73: \"eslint-plugin-react\": \"7.37.5\",\n74: \"eslint-plugin-react-hooks\": \"5.2.0\",\n75: \"eslint-plugin-unused-imports\": \"4.1.4\",\n76: \"globals\": \"16.0.0\",\n77: \"postcss\": \"8.5.6\",\n78: \"prettier\": \"3.5.3\",\n79: \"typescript\": \"5.6.3\",\n80: \"vite\": \"npm:rolldown-vite@^7.3.1\",\n81: \"vite-plugin-pwa\": \"^1.1.0\",\n82: \"vite-tsconfig-paths\": \"^6.0.5\"\n83: },\n84: \"overrides\": {\n85: \"serialize-javascript\": \"7.0.3\"\n86: }\n87: }\n\n(End of file - total 87 lines)\n", "title": "vite-frontend/package.json", "metadata": { "preview": "{\n \"name\": \"flvx\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"lint\": \"eslint --fix\",\n \"preview\": \"vite preview\"\n },\n \"dependencies\": {\n \"@dnd-kit/core\": \"^6.3.1\",\n \"@dnd-kit/sortable\": \"^10.0.0\",\n \"@dnd-kit/utilities\": \"^3.2.2\",\n \"@internationalized/date\": \"^3.10.0\",\n \"@marsidev/react-turnstile\": \"^1.1.0\",\n \"@radix-ui/react-accordion\": \"^1.2.0\",\n \"@radix-ui/react-checkbox\": \"^1.1.0\",\n \"@radix-ui/react-dialog\": \"^1.1.0\",", "truncated": false, "loaded": [] }, "time": { "start": 1772527925138, "end": 1772527925146 } }, "id": "prt_cb2e5b6c2001Jel2wWOHNh7kKn", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e593ba0028hetOWz0GsZMSk" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "a065b6a10de4bd0df5c764079bb7633783a676df", "cost": 0, "tokens": { "total": 37770, "input": 23935, "output": 139, "reasoning": 67, "cache": { "read": 13696, "write": 0 } }, "id": "prt_cb2e5b7fb001Lr1xrcEUNYNEeH", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e593ba0028hetOWz0GsZMSk" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527925314, "completed": 1772527943165 }, "parentID": "msg_cb2e428a7001Ejj8LNAqWjo6fE", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "build", "agent": "build", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 39326, "input": 39023, "output": 303, "reasoning": 219, "cache": { "read": 0, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e5b842002DLHxClVUGRCVwj", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "a065b6a10de4bd0df5c764079bb7633783a676df", "id": "prt_cb2e5e310001AbKnajbFu8C00b", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e5b842002DLHxClVUGRCVwj" }, { "type": "reasoning", "text": "**Evaluating TypeScript test run options**", "time": { "start": 1772527936273, "end": 1772527939094 }, "id": "prt_cb2e5e311001KeifnnWO4BgiGP", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e5b842002DLHxClVUGRCVwj" }, { "type": "tool", "callID": "call_xotCWjwHtd1csepYJ9P2Bfwi", "tool": "bash", "state": { "status": "completed", "input": { "command": "npm run build", "timeout": 120000, "workdir": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend", "description": "Build frontend to validate TypeScript changes" }, "output": "\n> flvx@0.0.0 build\n> tsc && vite build\n\nrolldown-vite v7.3.1 building client environment for production...\n\u001b[2K\rtransforming...✓ 3180 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/manifest.webmanifest 0.43 kB\ndist/index.html 7.51 kB │ gzip: 2.51 kB\ndist/assets/index-D7vS0DQY.css 105.58 kB │ gzip: 16.60 kB\ndist/assets/workbox-window.prod.es5-Bm4xbD7U.js 8.29 kB │ gzip: 2.81 kB\ndist/assets/index-B-lJZk9F.js 3,935.86 kB │ gzip: 812.13 kB\n✓ built in 595ms\n\nPWA v1.2.0\nmode generateSW\nprecache 9 entries (3962.17 KiB)\nfiles generated\n dist/sw.js\n dist/workbox-8c29f6e4.js\n", "title": "Build frontend to validate TypeScript changes", "metadata": { "output": "\n> flvx@0.0.0 build\n> tsc && vite build\n\nrolldown-vite v7.3.1 building client environment for production...\n\u001b[2K\rtransforming...✓ 3180 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/manifest.webmanifest 0.43 kB\ndist/index.html 7.51 kB │ gzip: 2.51 kB\ndist/assets/index-D7vS0DQY.css 105.58 kB │ gzip: 16.60 kB\ndist/assets/workbox-window.prod.es5-Bm4xbD7U.js 8.29 kB │ gzip: 2.81 kB\ndist/assets/index-B-lJZk9F.js 3,935.86 kB │ gzip: 812.13 kB\n✓ built in 595ms\n\nPWA v1.2.0\nmode generateSW\nprecache 9 entries (3962.17 KiB)\nfiles generated\n dist/sw.js\n dist/workbox-8c29f6e4.js\n", "exit": 0, "description": "Build frontend to validate TypeScript changes", "truncated": false }, "time": { "start": 1772527939012, "end": 1772527943109 } }, "id": "prt_cb2e5e389001cXE1bRkbPaF3uO", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e5b842002DLHxClVUGRCVwj" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "a065b6a10de4bd0df5c764079bb7633783a676df", "cost": 0, "tokens": { "total": 39326, "input": 39023, "output": 303, "reasoning": 219, "cache": { "read": 0, "write": 0 } }, "id": "prt_cb2e5fdc50010z9ZlZDqGPzj6p", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e5b842002DLHxClVUGRCVwj" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527943168, "completed": 1772527948211 }, "parentID": "msg_cb2e428a7001Ejj8LNAqWjo6fE", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "build", "agent": "build", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 39477, "input": 4672, "output": 117, "reasoning": 26, "cache": { "read": 34688, "write": 0 } }, "variant": "xhigh", "finish": "tool-calls", "id": "msg_cb2e5fe00002pjWjatAaHUJrgG", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "a065b6a10de4bd0df5c764079bb7633783a676df", "id": "prt_cb2e60f24001bobuMdkDd9jeS6", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e5fe00002pjWjatAaHUJrgG" }, { "type": "reasoning", "text": "**Planning to run import-format tests**", "time": { "start": 1772527947557, "end": 1772527948129 }, "id": "prt_cb2e60f25001BaZhsRP1Jz44B6", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e5fe00002pjWjatAaHUJrgG" }, { "type": "tool", "callID": "call_nLbdXEvJ2CYcxBC1AUvhRIpm", "tool": "bash", "state": { "status": "completed", "input": { "command": "node --test src/pages/forward/import-format.test.ts", "timeout": 120000, "workdir": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula/vite-frontend", "description": "Run ny import parser unit tests" }, "output": "✔ parseNyFormatData parses concatenated ny JSON objects (1.443291ms)\n✔ parseNyFormatData parses newline-separated ny JSON objects (0.081334ms)\n✔ parseNyFormatData returns validation errors for invalid fields (0.101333ms)\n✔ parseNyFormatData allows missing listen_port for auto assignment (0.051375ms)\n✔ parseNyFormatData supports ny alias fields (0.395417ms)\n✔ convertNyItemToForwardInput maps ny fields correctly (0.06ms)\n✔ convertNyItemToForwardInput keeps null inPort for auto assignment (0.0405ms)\nℹ tests 7\nℹ suites 0\nℹ pass 7\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 108.674167\n", "title": "Run ny import parser unit tests", "metadata": { "output": "✔ parseNyFormatData parses concatenated ny JSON objects (1.443291ms)\n✔ parseNyFormatData parses newline-separated ny JSON objects (0.081334ms)\n✔ parseNyFormatData returns validation errors for invalid fields (0.101333ms)\n✔ parseNyFormatData allows missing listen_port for auto assignment (0.051375ms)\n✔ parseNyFormatData supports ny alias fields (0.395417ms)\n✔ convertNyItemToForwardInput maps ny fields correctly (0.06ms)\n✔ convertNyItemToForwardInput keeps null inPort for auto assignment (0.0405ms)\nℹ tests 7\nℹ suites 0\nℹ pass 7\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 108.674167\n", "exit": 0, "description": "Run ny import parser unit tests", "truncated": false }, "time": { "start": 1772527947970, "end": 1772527948165 } }, "id": "prt_cb2e6101d001kh0Upn618g5ev4", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e5fe00002pjWjatAaHUJrgG" }, { "type": "step-finish", "reason": "tool-calls", "snapshot": "a065b6a10de4bd0df5c764079bb7633783a676df", "cost": 0, "tokens": { "total": 39477, "input": 4672, "output": 117, "reasoning": 26, "cache": { "read": 34688, "write": 0 } }, "id": "prt_cb2e61185001RraRi9I860HCMX", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e5fe00002pjWjatAaHUJrgG" } ] }, { "info": { "role": "assistant", "time": { "created": 1772527948214, "completed": 1772527957264 }, "parentID": "msg_cb2e428a7001Ejj8LNAqWjo6fE", "modelID": "gpt-5.3-codex", "providerID": "api", "mode": "build", "agent": "build", "path": { "cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula", "root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/mighty-nebula" }, "cost": 0, "tokens": { "total": 40003, "input": 39639, "output": 364, "reasoning": 0, "cache": { "read": 0, "write": 0 } }, "variant": "xhigh", "finish": "stop", "id": "msg_cb2e611b6002I3TGaGgnjW7TCi", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY" }, "parts": [ { "type": "step-start", "snapshot": "a065b6a10de4bd0df5c764079bb7633783a676df", "id": "prt_cb2e61cec001lueaQ6O0IS4075", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e611b6002I3TGaGgnjW7TCi" }, { "type": "text", "text": "已实施完成,这次改动把 `ny` 导入完全对齐到“选隧道”流程,并按你指定支持“端口缺失自动分配”。\n\n- `ny` 解析兼容增强:支持字段别名、可选端口 \n - 修改 `vite-frontend/src/pages/forward/import-format.ts` \n - `listen_port` 现在允许为 `null`(缺失即自动分配) \n - 兼容字段:`dest/dst/target/targets`、`listen_port/listenPort/port/in_port/inPort`、`name/forward_name/forwardName` \n - `dest` 支持数组或逗号分隔字符串,统一归一化 \n- 导入 UI 文案已同步更新(强调按所选隧道导入 + 端口可省略) \n - 修改 `vite-frontend/src/pages/forward.tsx:4623`、`vite-frontend/src/pages/forward.tsx:4699` \n- 测试已补齐并通过 \n - 修改 `vite-frontend/src/pages/forward/import-format.test.ts` \n - 新增用例:缺失端口自动分配、别名字段兼容、`inPort: null` 映射 \n- 计划文档已新增并完成勾选 \n - `plans/002-ny-import-tunnel-selection-compat.md`\n\n验证结果:\n- `npm run build`(`vite-frontend`)通过\n- `node --test src/pages/forward/import-format.test.ts` 7/7 通过\n\n如果你要,我下一步可以直接帮你提交一个规范 commit。", "time": { "start": 1772527957173, "end": 1772527957173 }, "id": "prt_cb2e61ced001GIpF9RHLplQQD7", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e611b6002I3TGaGgnjW7TCi" }, { "type": "step-finish", "reason": "stop", "snapshot": "a065b6a10de4bd0df5c764079bb7633783a676df", "cost": 0, "tokens": { "total": 40003, "input": 39639, "output": 364, "reasoning": 0, "cache": { "read": 0, "write": 0 } }, "id": "prt_cb2e634b80019hazEJZNQylZT0", "sessionID": "ses_34d1fa4b1ffeH4sVbjXkh5P6IY", "messageID": "msg_cb2e611b6002I3TGaGgnjW7TCi" } ] } ] }