mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-11-14 16:54:24 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27d6691232 | |||
| cc4b8a916a | |||
| d9dd5131b2 | |||
| a98c9f4f59 | |||
| 5cb935e0e5 | |||
| 6f59e4be0c | |||
| 647446a2a2 | |||
| a2ec08f033 | |||
| f8809d73fb | |||
| e5339a8072 | |||
| fbb4d82a44 | |||
| 61b71a11c7 | |||
| 4c69ff491d | |||
| 0ad4904e20 |
@@ -0,0 +1,30 @@
|
||||
# Session Context
|
||||
|
||||
Session ID: ses_32f7e3b05ffeK8IxNvZAZISGWO
|
||||
Commit Message: 编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||
|
||||
## Prompts
|
||||
|
||||
### Prompt 1
|
||||
|
||||
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||
|
||||
### Prompt 2
|
||||
|
||||
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||
|
||||
### Prompt 3
|
||||
|
||||
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||
|
||||
### Prompt 4
|
||||
|
||||
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||
|
||||
### Prompt 5
|
||||
|
||||
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||
|
||||
## Summary
|
||||
|
||||
已成功构建并推送 `ghcr.io/sagit-chu/vite-frontend:beta` (linux/amd64)
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||
|
||||
---
|
||||
|
||||
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||
|
||||
---
|
||||
|
||||
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||
|
||||
---
|
||||
|
||||
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||
|
||||
---
|
||||
|
||||
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||
@@ -0,0 +1 @@
|
||||
已成功构建并推送 `ghcr.io/sagit-chu/vite-frontend:beta` (linux/amd64)
|
||||
@@ -7,13 +7,13 @@
|
||||
"title": "规则页精简模式显示规则数量",
|
||||
"version": "1.2.21",
|
||||
"summary": {
|
||||
"additions": 67,
|
||||
"deletions": 57,
|
||||
"files": 1
|
||||
"additions": 4529,
|
||||
"deletions": 65,
|
||||
"files": 11
|
||||
},
|
||||
"time": {
|
||||
"created": 1773020783005,
|
||||
"updated": 1773020978012
|
||||
"updated": 1773022814998
|
||||
}
|
||||
},
|
||||
"messages": [
|
||||
@@ -3536,6 +3536,505 @@
|
||||
"messageID": "msg_cd0491b35001Gyu7B4fNEHivH2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"info": {
|
||||
"role": "user",
|
||||
"time": {
|
||||
"created": 1773022792451
|
||||
},
|
||||
"summary": {
|
||||
"diffs": []
|
||||
},
|
||||
"agent": "build",
|
||||
"model": {
|
||||
"providerID": "api",
|
||||
"modelID": "gemini-3.1-pro-high"
|
||||
},
|
||||
"id": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "这个没改对,页面没变化,请检查",
|
||||
"id": "prt_cd064ca87002we266x5BvrANpB",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd064ca87001mKUQE2EyR8PezH"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"info": {
|
||||
"role": "assistant",
|
||||
"time": {
|
||||
"created": 1773022792467,
|
||||
"completed": 1773022801627
|
||||
},
|
||||
"parentID": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||
"modelID": "gemini-3.1-pro-high",
|
||||
"providerID": "api",
|
||||
"mode": "build",
|
||||
"agent": "build",
|
||||
"path": {
|
||||
"cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon",
|
||||
"root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon"
|
||||
},
|
||||
"cost": 0,
|
||||
"tokens": {
|
||||
"total": 58585,
|
||||
"input": 58084,
|
||||
"output": 59,
|
||||
"reasoning": 442,
|
||||
"cache": {
|
||||
"read": 0,
|
||||
"write": 0
|
||||
}
|
||||
},
|
||||
"finish": "tool-calls",
|
||||
"id": "msg_cd064cb13001TWoWh6nBESUPZl",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"type": "step-start",
|
||||
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||
"id": "prt_cd064ee23001PcfPcy835V87tU",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd064cb13001TWoWh6nBESUPZl"
|
||||
},
|
||||
{
|
||||
"type": "tool",
|
||||
"callID": "bash-1773022801288808061-252",
|
||||
"tool": "bash",
|
||||
"state": {
|
||||
"status": "completed",
|
||||
"input": {
|
||||
"command": "git show HEAD:vite-frontend/src/pages/forward.tsx | grep -B 5 -A 20 \"根据显示模式渲染不同内容\"",
|
||||
"description": "Check original code structure for rendering modes"
|
||||
},
|
||||
"output": " value={batchProgress.percent}\n />\n </div>\n )}\n\n {/* 根据显示模式渲染不同内容 */}\n {compactMode ? (\n viewMode === \"grouped\" ? (\n sortedForwards.length > 0 ? (\n <>\n <div className=\"flex items-center justify-between px-1 mb-3\">\n <span className=\"text-sm font-semibold text-foreground\">\n 全部规则\n </span>\n <span className=\"text-xs text-default-600\">\n {sortedForwards.length} 条规则\n </span>\n </div>\n <div className=\"overflow-hidden rounded-xl border border-divider bg-content1 shadow-md\">\n <DndContext\n collisionDetection={closestCenter}\n sensors={sensors}\n onDragEnd={handleDragEnd}\n >\n <SortableContext\n items={sortableForwardIds}\n",
|
||||
"title": "Check original code structure for rendering modes",
|
||||
"metadata": {
|
||||
"output": " value={batchProgress.percent}\n />\n </div>\n )}\n\n {/* 根据显示模式渲染不同内容 */}\n {compactMode ? (\n viewMode === \"grouped\" ? (\n sortedForwards.length > 0 ? (\n <>\n <div className=\"flex items-center justify-between px-1 mb-3\">\n <span className=\"text-sm font-semibold text-foreground\">\n 全部规则\n </span>\n <span className=\"text-xs text-default-600\">\n {sortedForwards.length} 条规则\n </span>\n </div>\n <div className=\"overflow-hidden rounded-xl border border-divider bg-content1 shadow-md\">\n <DndContext\n collisionDetection={closestCenter}\n sensors={sensors}\n onDragEnd={handleDragEnd}\n >\n <SortableContext\n items={sortableForwardIds}\n",
|
||||
"exit": 0,
|
||||
"description": "Check original code structure for rendering modes",
|
||||
"truncated": false
|
||||
},
|
||||
"time": {
|
||||
"start": 1773022801444,
|
||||
"end": 1773022801445
|
||||
}
|
||||
},
|
||||
"id": "prt_cd064ee24001VvqiBXMcI8KgPH",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd064cb13001TWoWh6nBESUPZl"
|
||||
},
|
||||
{
|
||||
"type": "step-finish",
|
||||
"reason": "tool-calls",
|
||||
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||
"cost": 0,
|
||||
"tokens": {
|
||||
"total": 58585,
|
||||
"input": 58084,
|
||||
"output": 59,
|
||||
"reasoning": 442,
|
||||
"cache": {
|
||||
"read": 0,
|
||||
"write": 0
|
||||
}
|
||||
},
|
||||
"id": "prt_cd064ee250016oJVS2eLQzwT01",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd064cb13001TWoWh6nBESUPZl"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"info": {
|
||||
"role": "assistant",
|
||||
"time": {
|
||||
"created": 1773022801632,
|
||||
"completed": 1773022806653
|
||||
},
|
||||
"parentID": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||
"modelID": "gemini-3.1-pro-high",
|
||||
"providerID": "api",
|
||||
"mode": "build",
|
||||
"agent": "build",
|
||||
"path": {
|
||||
"cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon",
|
||||
"root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon"
|
||||
},
|
||||
"cost": 0,
|
||||
"tokens": {
|
||||
"total": 58423,
|
||||
"input": 1479,
|
||||
"output": 44,
|
||||
"reasoning": 0,
|
||||
"cache": {
|
||||
"read": 56900,
|
||||
"write": 0
|
||||
}
|
||||
},
|
||||
"finish": "tool-calls",
|
||||
"id": "msg_cd064eedf001lx0GW03y2zsL2c",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"type": "step-start",
|
||||
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||
"id": "prt_cd06501d0001fbtq5FOqwf6wp7",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd064eedf001lx0GW03y2zsL2c"
|
||||
},
|
||||
{
|
||||
"type": "tool",
|
||||
"callID": "bash-1773022806321799405-253",
|
||||
"tool": "bash",
|
||||
"state": {
|
||||
"status": "completed",
|
||||
"input": {
|
||||
"description": "Check exactly what I changed in my last commit",
|
||||
"command": "git diff HEAD~1 HEAD vite-frontend/src/pages/forward.tsx"
|
||||
},
|
||||
"output": "",
|
||||
"title": "Check exactly what I changed in my last commit",
|
||||
"metadata": {
|
||||
"output": "",
|
||||
"exit": 0,
|
||||
"description": "Check exactly what I changed in my last commit",
|
||||
"truncated": false
|
||||
},
|
||||
"time": {
|
||||
"start": 1773022806480,
|
||||
"end": 1773022806481
|
||||
}
|
||||
},
|
||||
"id": "prt_cd06501d0002aJQPoXzQjQ6QxP",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd064eedf001lx0GW03y2zsL2c"
|
||||
},
|
||||
{
|
||||
"type": "step-finish",
|
||||
"reason": "tool-calls",
|
||||
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||
"cost": 0,
|
||||
"tokens": {
|
||||
"total": 58423,
|
||||
"input": 1479,
|
||||
"output": 44,
|
||||
"reasoning": 0,
|
||||
"cache": {
|
||||
"read": 56900,
|
||||
"write": 0
|
||||
}
|
||||
},
|
||||
"id": "prt_cd06501d1001UHeBODXWcVRT3P",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd064eedf001lx0GW03y2zsL2c"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"info": {
|
||||
"role": "assistant",
|
||||
"time": {
|
||||
"created": 1773022806657,
|
||||
"completed": 1773022810679
|
||||
},
|
||||
"parentID": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||
"modelID": "gemini-3.1-pro-high",
|
||||
"providerID": "api",
|
||||
"mode": "build",
|
||||
"agent": "build",
|
||||
"path": {
|
||||
"cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon",
|
||||
"root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon"
|
||||
},
|
||||
"cost": 0,
|
||||
"tokens": {
|
||||
"total": 58460,
|
||||
"input": 1540,
|
||||
"output": 25,
|
||||
"reasoning": 0,
|
||||
"cache": {
|
||||
"read": 56895,
|
||||
"write": 0
|
||||
}
|
||||
},
|
||||
"finish": "tool-calls",
|
||||
"id": "msg_cd065028100100U8RbhbWKxrf3",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"type": "step-start",
|
||||
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||
"id": "prt_cd06511890014PkWgUY9CAmTPd",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd065028100100U8RbhbWKxrf3"
|
||||
},
|
||||
{
|
||||
"type": "tool",
|
||||
"callID": "bash-1773022810346559153-254",
|
||||
"tool": "bash",
|
||||
"state": {
|
||||
"status": "completed",
|
||||
"input": {
|
||||
"description": "Check latest commit",
|
||||
"command": "git log -1"
|
||||
},
|
||||
"output": "commit 78af992b6fde82eb3af4d09daa02a43866210636\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:59:59 2026 +0800\n\n fix: restore renewal.ts from main to resolve build errors\n",
|
||||
"title": "Check latest commit",
|
||||
"metadata": {
|
||||
"output": "commit 78af992b6fde82eb3af4d09daa02a43866210636\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:59:59 2026 +0800\n\n fix: restore renewal.ts from main to resolve build errors\n",
|
||||
"exit": 0,
|
||||
"description": "Check latest commit",
|
||||
"truncated": false
|
||||
},
|
||||
"time": {
|
||||
"start": 1773022810506,
|
||||
"end": 1773022810506
|
||||
}
|
||||
},
|
||||
"id": "prt_cd065118a0014hx1B9opNcSfJl",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd065028100100U8RbhbWKxrf3"
|
||||
},
|
||||
{
|
||||
"type": "step-finish",
|
||||
"reason": "tool-calls",
|
||||
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||
"cost": 0,
|
||||
"tokens": {
|
||||
"total": 58460,
|
||||
"input": 1540,
|
||||
"output": 25,
|
||||
"reasoning": 0,
|
||||
"cache": {
|
||||
"read": 56895,
|
||||
"write": 0
|
||||
}
|
||||
},
|
||||
"id": "prt_cd065118b001SISeN0O2IRDH5C",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd065028100100U8RbhbWKxrf3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"info": {
|
||||
"role": "assistant",
|
||||
"time": {
|
||||
"created": 1773022810683,
|
||||
"completed": 1773022814810
|
||||
},
|
||||
"parentID": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||
"modelID": "gemini-3.1-pro-high",
|
||||
"providerID": "api",
|
||||
"mode": "build",
|
||||
"agent": "build",
|
||||
"path": {
|
||||
"cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon",
|
||||
"root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon"
|
||||
},
|
||||
"cost": 0,
|
||||
"tokens": {
|
||||
"total": 58595,
|
||||
"input": 1681,
|
||||
"output": 24,
|
||||
"reasoning": 0,
|
||||
"cache": {
|
||||
"read": 56890,
|
||||
"write": 0
|
||||
}
|
||||
},
|
||||
"finish": "tool-calls",
|
||||
"id": "msg_cd065123a001uXPsKZwVH7RDJb",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"type": "step-start",
|
||||
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||
"id": "prt_cd06521a8001yiaSoVddfbN1tj",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd065123a001uXPsKZwVH7RDJb"
|
||||
},
|
||||
{
|
||||
"type": "tool",
|
||||
"callID": "bash-1773022814469937121-255",
|
||||
"tool": "bash",
|
||||
"state": {
|
||||
"status": "completed",
|
||||
"input": {
|
||||
"description": "Check history",
|
||||
"command": "git log -3"
|
||||
},
|
||||
"output": "commit 78af992b6fde82eb3af4d09daa02a43866210636\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:59:59 2026 +0800\n\n fix: restore renewal.ts from main to resolve build errors\n\ncommit f790becda8ead611b847637daafad6e7d40d8115\nMerge: e7548e7 413081f\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:58:02 2026 +0800\n\n merge: resolve conflicts with main branch\n\ncommit e7548e7b30ecd1c73e4ef98f5d36e6b6394c209a\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:51:28 2026 +0800\n\n style(frontend): format code and improve node card layout\n \n Entire-Checkpoint: 08b760b76538\n",
|
||||
"title": "Check history",
|
||||
"metadata": {
|
||||
"output": "commit 78af992b6fde82eb3af4d09daa02a43866210636\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:59:59 2026 +0800\n\n fix: restore renewal.ts from main to resolve build errors\n\ncommit f790becda8ead611b847637daafad6e7d40d8115\nMerge: e7548e7 413081f\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:58:02 2026 +0800\n\n merge: resolve conflicts with main branch\n\ncommit e7548e7b30ecd1c73e4ef98f5d36e6b6394c209a\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:51:28 2026 +0800\n\n style(frontend): format code and improve node card layout\n \n Entire-Checkpoint: 08b760b76538\n",
|
||||
"exit": 0,
|
||||
"description": "Check history",
|
||||
"truncated": false
|
||||
},
|
||||
"time": {
|
||||
"start": 1773022814633,
|
||||
"end": 1773022814633
|
||||
}
|
||||
},
|
||||
"id": "prt_cd06521a8002I2rlXCrLkmKM1d",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd065123a001uXPsKZwVH7RDJb"
|
||||
},
|
||||
{
|
||||
"type": "step-finish",
|
||||
"reason": "tool-calls",
|
||||
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||
"cost": 0,
|
||||
"tokens": {
|
||||
"total": 58595,
|
||||
"input": 1681,
|
||||
"output": 24,
|
||||
"reasoning": 0,
|
||||
"cache": {
|
||||
"read": 56890,
|
||||
"write": 0
|
||||
}
|
||||
},
|
||||
"id": "prt_cd06521a9001CzTlcvvzi6vd8Y",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd065123a001uXPsKZwVH7RDJb"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"info": {
|
||||
"role": "assistant",
|
||||
"time": {
|
||||
"created": 1773022814814,
|
||||
"completed": 1773022818942
|
||||
},
|
||||
"parentID": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||
"modelID": "gemini-3.1-pro-high",
|
||||
"providerID": "api",
|
||||
"mode": "build",
|
||||
"agent": "build",
|
||||
"path": {
|
||||
"cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon",
|
||||
"root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon"
|
||||
},
|
||||
"cost": 0,
|
||||
"tokens": {
|
||||
"total": 58974,
|
||||
"input": 2054,
|
||||
"output": 33,
|
||||
"reasoning": 0,
|
||||
"cache": {
|
||||
"read": 56887,
|
||||
"write": 0
|
||||
}
|
||||
},
|
||||
"finish": "tool-calls",
|
||||
"id": "msg_cd065225e0010wlkl67le6h46g",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"type": "step-start",
|
||||
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||
"id": "prt_cd06531d8001vtXeqUZ9z4fx14",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd065225e0010wlkl67le6h46g"
|
||||
},
|
||||
{
|
||||
"type": "tool",
|
||||
"callID": "bash-1773022818624349202-256",
|
||||
"tool": "bash",
|
||||
"state": {
|
||||
"status": "completed",
|
||||
"input": {
|
||||
"command": "git log --oneline | head -n 10",
|
||||
"description": "Check recent commits"
|
||||
},
|
||||
"output": "78af992 fix: restore renewal.ts from main to resolve build errors\nf790bec merge: resolve conflicts with main branch\ne7548e7 style(frontend): format code and improve node card layout\n50e4107 fix(frontend): show rule count in compact view mode for forward pages\n413081f feat(nodes): add renewal cycle and auto-advance scheduling (#296)\nbfac640 feat(nodes): add renewal cycle and auto-advance scheduling\n508a37a feat(nodes): add node metadata and expiry reminders (#293)\nd606550 feat(forward): display tunnel traffic ratio on forward page\n31ef861 feat(nodes): add node metadata and expiry reminders (#246)\nf1bdb2e feat(frontend): implement AJAX no-refresh UX improvements (#276)\n",
|
||||
"title": "Check recent commits",
|
||||
"metadata": {
|
||||
"output": "78af992 fix: restore renewal.ts from main to resolve build errors\nf790bec merge: resolve conflicts with main branch\ne7548e7 style(frontend): format code and improve node card layout\n50e4107 fix(frontend): show rule count in compact view mode for forward pages\n413081f feat(nodes): add renewal cycle and auto-advance scheduling (#296)\nbfac640 feat(nodes): add renewal cycle and auto-advance scheduling\n508a37a feat(nodes): add node metadata and expiry reminders (#293)\nd606550 feat(forward): display tunnel traffic ratio on forward page\n31ef861 feat(nodes): add node metadata and expiry reminders (#246)\nf1bdb2e feat(frontend): implement AJAX no-refresh UX improvements (#276)\n",
|
||||
"exit": 0,
|
||||
"description": "Check recent commits",
|
||||
"truncated": false
|
||||
},
|
||||
"time": {
|
||||
"start": 1773022818776,
|
||||
"end": 1773022818776
|
||||
}
|
||||
},
|
||||
"id": "prt_cd06531d8002HjsfEaDHfZzBM7",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd065225e0010wlkl67le6h46g"
|
||||
},
|
||||
{
|
||||
"type": "step-finish",
|
||||
"reason": "tool-calls",
|
||||
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||
"cost": 0,
|
||||
"tokens": {
|
||||
"total": 58974,
|
||||
"input": 2054,
|
||||
"output": 33,
|
||||
"reasoning": 0,
|
||||
"cache": {
|
||||
"read": 56887,
|
||||
"write": 0
|
||||
}
|
||||
},
|
||||
"id": "prt_cd06531d9001eYxQ5nQBoU1rsk",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||
"messageID": "msg_cd065225e0010wlkl67le6h46g"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"info": {
|
||||
"role": "assistant",
|
||||
"time": {
|
||||
"created": 1773022818946,
|
||||
"completed": 1773022824318
|
||||
},
|
||||
"error": {
|
||||
"name": "MessageAbortedError",
|
||||
"data": {
|
||||
"message": "The operation was aborted."
|
||||
}
|
||||
},
|
||||
"parentID": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||
"modelID": "gemini-3.1-pro-high",
|
||||
"providerID": "api",
|
||||
"mode": "build",
|
||||
"agent": "build",
|
||||
"path": {
|
||||
"cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon",
|
||||
"root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon"
|
||||
},
|
||||
"cost": 0,
|
||||
"tokens": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"reasoning": 0,
|
||||
"cache": {
|
||||
"read": 0,
|
||||
"write": 0
|
||||
}
|
||||
},
|
||||
"id": "msg_cd0653282001hDYLdWTwLVss9i",
|
||||
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||
},
|
||||
"parts": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
规则标签页下,精简模式,和非精简模式一样要显示规则数量
|
||||
这个没改对,页面没变化,请检查
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -1612,13 +1613,14 @@ func buildForwarderNodes(targets []string) []map[string]interface{} {
|
||||
}
|
||||
|
||||
func processServerAddress(serverAddr string) string {
|
||||
serverAddr = strings.TrimSpace(serverAddr)
|
||||
serverAddr = normalizeServerAddressInput(serverAddr)
|
||||
if serverAddr == "" {
|
||||
return serverAddr
|
||||
}
|
||||
if strings.HasPrefix(serverAddr, "[") {
|
||||
return serverAddr
|
||||
}
|
||||
|
||||
idx := strings.LastIndex(serverAddr, ":")
|
||||
if idx < 0 {
|
||||
if looksLikeIPv6(serverAddr) {
|
||||
@@ -1637,6 +1639,27 @@ func processServerAddress(serverAddr string) string {
|
||||
return serverAddr
|
||||
}
|
||||
|
||||
func normalizeServerAddressInput(serverAddr string) string {
|
||||
serverAddr = strings.TrimSpace(serverAddr)
|
||||
if serverAddr == "" {
|
||||
return serverAddr
|
||||
}
|
||||
|
||||
if idx := strings.Index(serverAddr, "://"); idx > 0 {
|
||||
if parsed, err := url.Parse(serverAddr); err == nil {
|
||||
if host := strings.TrimSpace(parsed.Host); host != "" {
|
||||
return host
|
||||
}
|
||||
}
|
||||
serverAddr = serverAddr[idx+3:]
|
||||
}
|
||||
|
||||
if idx := strings.IndexAny(serverAddr, "/?#"); idx >= 0 {
|
||||
serverAddr = serverAddr[:idx]
|
||||
}
|
||||
return strings.TrimSpace(serverAddr)
|
||||
}
|
||||
|
||||
func looksLikeIPv6(address string) bool {
|
||||
return strings.Count(address, ":") >= 2
|
||||
}
|
||||
|
||||
@@ -420,3 +420,68 @@ func TestBuildForwardServiceConfigs_BindIPAlreadyContainsPort(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessServerAddress_StripsURLSchemeAndPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "https with path",
|
||||
in: "https://panel.example.com:8443/api/v1",
|
||||
want: "panel.example.com:8443",
|
||||
},
|
||||
{
|
||||
name: "wss with query",
|
||||
in: "wss://panel.example.com:443/system-info?x=1",
|
||||
want: "panel.example.com:443",
|
||||
},
|
||||
{
|
||||
name: "http without port",
|
||||
in: "http://panel.example.com",
|
||||
want: "panel.example.com",
|
||||
},
|
||||
{
|
||||
name: "manual host with trailing path",
|
||||
in: "panel.example.com:8080/path",
|
||||
want: "panel.example.com:8080",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := processServerAddress(tt.in); got != tt.want {
|
||||
t.Fatalf("%s: expected %q, got %q", tt.name, tt.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessServerAddress_NormalizesIPv6(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "ipv6 host only",
|
||||
in: "2001:db8::1",
|
||||
want: "[2001:db8::1]",
|
||||
},
|
||||
{
|
||||
name: "ipv6 host and port",
|
||||
in: "https://[2001:db8::1]:8443/path",
|
||||
want: "[2001:db8::1]:8443",
|
||||
},
|
||||
{
|
||||
name: "already bracketed",
|
||||
in: "[2001:db8::2]:9000",
|
||||
want: "[2001:db8::2]:9000",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := processServerAddress(tt.in); got != tt.want {
|
||||
t.Fatalf("%s: expected %q, got %q", tt.name, tt.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +262,6 @@ func (h *Handler) nodeCreate(w http.ResponseWriter, r *http.Request) {
|
||||
nullableText(asString(req["interfaceName"])),
|
||||
nullableText(""),
|
||||
nullableText(strings.TrimSpace(asString(req["remark"]))),
|
||||
nullableText(strings.TrimSpace(asString(req["tags"]))),
|
||||
nullableUnixMilli(asInt64(req["expiryTime"], 0)),
|
||||
nullableText(normalizeNodeRenewalCycle(asString(req["renewalCycle"]))),
|
||||
asInt(req["http"], 0),
|
||||
@@ -331,7 +330,6 @@ func (h *Handler) nodeUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
nullableText(asString(req["interfaceName"])),
|
||||
nullableText(asString(req["extraIPs"])),
|
||||
nullableText(strings.TrimSpace(asString(req["remark"]))),
|
||||
nullableText(strings.TrimSpace(asString(req["tags"]))),
|
||||
nullableUnixMilli(asInt64(req["expiryTime"], 0)),
|
||||
nullableText(normalizeNodeRenewalCycle(asString(req["renewalCycle"]))),
|
||||
newHTTP,
|
||||
|
||||
@@ -61,7 +61,6 @@ type Node struct {
|
||||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Name string `gorm:"type:varchar(100);not null"`
|
||||
Remark sql.NullString `gorm:"column:remark;type:text"`
|
||||
Tags sql.NullString `gorm:"column:tags;type:text"`
|
||||
ExpiryTime sql.NullInt64 `gorm:"column:expiry_time"`
|
||||
RenewalCycle sql.NullString `gorm:"column:renewal_cycle;type:varchar(20)"`
|
||||
Secret string `gorm:"type:varchar(100);not null"`
|
||||
@@ -341,7 +340,6 @@ type NodeBackup struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
Tags string `json:"tags,omitempty"`
|
||||
ExpiryTime int64 `json:"expiryTime,omitempty"`
|
||||
RenewalCycle string `json:"renewalCycle,omitempty"`
|
||||
Secret string `json:"secret"`
|
||||
|
||||
@@ -260,7 +260,7 @@ func prepareSQLiteLegacyColumns(db *gorm.DB) error {
|
||||
m := db.Migrator()
|
||||
|
||||
if m.HasTable(&model.Node{}) {
|
||||
for _, field := range []string{"ServerIPV4", "ServerIPV6", "ExtraIPs", "TCPListenAddr", "UDPListenAddr", "Inx", "IsRemote", "RemoteURL", "RemoteToken", "RemoteConfig", "Remark", "Tags", "ExpiryTime", "RenewalCycle"} {
|
||||
for _, field := range []string{"ServerIPV4", "ServerIPV6", "ExtraIPs", "TCPListenAddr", "UDPListenAddr", "Inx", "IsRemote", "RemoteURL", "RemoteToken", "RemoteConfig", "Remark", "ExpiryTime", "RenewalCycle"} {
|
||||
if m.HasColumn(&model.Node{}, field) {
|
||||
continue
|
||||
}
|
||||
@@ -635,7 +635,6 @@ func (r *Repository) ListNodes() ([]map[string]interface{}, error) {
|
||||
items = append(items, map[string]interface{}{
|
||||
"id": n.ID, "inx": n.Inx, "name": n.Name,
|
||||
"remark": nullableString(n.Remark),
|
||||
"tags": nullableString(n.Tags),
|
||||
"expiryTime": nullableInt64(n.ExpiryTime),
|
||||
"renewalCycle": nullableString(n.RenewalCycle),
|
||||
"ip": n.ServerIP, "serverIp": n.ServerIP,
|
||||
@@ -1820,7 +1819,7 @@ func (r *Repository) exportNodes() ([]model.NodeBackup, error) {
|
||||
for _, n := range nodes {
|
||||
b := model.NodeBackup{
|
||||
ID: n.ID, Name: n.Name, Secret: n.Secret, ServerIP: n.ServerIP,
|
||||
Remark: n.Remark.String, Tags: n.Tags.String, RenewalCycle: n.RenewalCycle.String,
|
||||
Remark: n.Remark.String, RenewalCycle: n.RenewalCycle.String,
|
||||
Port: n.Port, HTTP: n.HTTP, TLS: n.TLS, Socks: n.Socks,
|
||||
CreatedTime: n.CreatedTime, Status: n.Status,
|
||||
TCPListenAddr: n.TCPListenAddr, UDPListenAddr: n.UDPListenAddr,
|
||||
@@ -2179,7 +2178,6 @@ func importNodes(tx *gorm.DB, nodes []model.NodeBackup, now int64) (int, error)
|
||||
ID: n.ID,
|
||||
Name: n.Name,
|
||||
Remark: sql.NullString{String: n.Remark, Valid: n.Remark != ""},
|
||||
Tags: sql.NullString{String: n.Tags, Valid: n.Tags != ""},
|
||||
ExpiryTime: sql.NullInt64{Int64: n.ExpiryTime, Valid: n.ExpiryTime > 0},
|
||||
RenewalCycle: sql.NullString{String: n.RenewalCycle, Valid: n.RenewalCycle != ""},
|
||||
Secret: n.Secret,
|
||||
@@ -2206,7 +2204,7 @@ func importNodes(tx *gorm.DB, nodes []model.NodeBackup, now int64) (int, error)
|
||||
err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"name", "remark", "tags", "expiry_time", "renewal_cycle", "secret", "server_ip", "server_ip_v4", "server_ip_v6", "port", "interface_name", "version",
|
||||
"name", "remark", "expiry_time", "renewal_cycle", "secret", "server_ip", "server_ip_v4", "server_ip_v6", "port", "interface_name", "version",
|
||||
"http", "tls", "socks", "updated_time", "status", "tcp_listen_addr", "udp_listen_addr",
|
||||
"inx", "is_remote", "remote_url", "remote_token", "remote_config",
|
||||
}),
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestPrepareSQLiteLegacyColumnsAddsNodeMetadataColumns(t *testing.T) {
|
||||
}
|
||||
|
||||
m := db.Migrator()
|
||||
for _, field := range []string{"Remark", "Tags", "ExpiryTime", "RenewalCycle"} {
|
||||
for _, field := range []string{"Remark", "ExpiryTime", "RenewalCycle"} {
|
||||
if !m.HasColumn(&model.Node{}, field) {
|
||||
t.Fatalf("expected node.%s column to exist", field)
|
||||
}
|
||||
|
||||
@@ -197,14 +197,13 @@ func (r *Repository) GetUserDefaultsForTunnel(userID int64) (flow int64, num int
|
||||
return user.Flow, user.Num, user.ExpTime, user.FlowResetTime, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateNode(name, secret, serverIP string, serverIPV4, serverIPV6, port, interfaceName, version, remark, tags, expiryTime, renewalCycle interface{}, httpFlag, tlsFlag, socksFlag int, now int64, status int, tcpAddr, udpAddr string, inx, isRemote int, remoteURL, remoteToken, remoteConfig, extraIPs interface{}) error {
|
||||
func (r *Repository) CreateNode(name, secret, serverIP string, serverIPV4, serverIPV6, port, interfaceName, version, remark, expiryTime, renewalCycle interface{}, httpFlag, tlsFlag, socksFlag int, now int64, status int, tcpAddr, udpAddr string, inx, isRemote int, remoteURL, remoteToken, remoteConfig, extraIPs interface{}) error {
|
||||
if r == nil || r.db == nil {
|
||||
return errors.New("repository not initialized")
|
||||
}
|
||||
node := model.Node{
|
||||
Name: name,
|
||||
Remark: nullStringFromInterface(remark),
|
||||
Tags: nullStringFromInterface(tags),
|
||||
ExpiryTime: nullInt64FromInterface(expiryTime),
|
||||
RenewalCycle: nullStringFromInterface(renewalCycle),
|
||||
Secret: secret,
|
||||
@@ -244,7 +243,7 @@ func (r *Repository) GetNodeStatusFields(nodeID int64) (status, httpFlag, tlsFla
|
||||
return node.Status, node.HTTP, node.TLS, node.Socks, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateNode(id int64, name, serverIP string, serverIPV4, serverIPV6, port, interfaceName, extraIPs, remark, tags, expiryTime, renewalCycle interface{}, httpFlag, tlsFlag, socksFlag int, tcpAddr, udpAddr string, now int64) error {
|
||||
func (r *Repository) UpdateNode(id int64, name, serverIP string, serverIPV4, serverIPV6, port, interfaceName, extraIPs, remark, expiryTime, renewalCycle interface{}, httpFlag, tlsFlag, socksFlag int, tcpAddr, udpAddr string, now int64) error {
|
||||
if r == nil || r.db == nil {
|
||||
return errors.New("repository not initialized")
|
||||
}
|
||||
@@ -253,7 +252,6 @@ func (r *Repository) UpdateNode(id int64, name, serverIP string, serverIPV4, ser
|
||||
Updates(map[string]interface{}{
|
||||
"name": name,
|
||||
"remark": nullStringFromInterface(remark),
|
||||
"tags": nullStringFromInterface(tags),
|
||||
"expiry_time": nullInt64FromInterface(expiryTime),
|
||||
"renewal_cycle": nullStringFromInterface(renewalCycle),
|
||||
"server_ip": serverIP,
|
||||
|
||||
+2
-2
@@ -109,12 +109,12 @@ func main() {
|
||||
// 加载配置文件
|
||||
config, err := LoadConfig("config.json")
|
||||
if err != nil {
|
||||
fmt.Println("❌ 配置加载失败: %v\n", err)
|
||||
fmt.Printf("❌ 配置加载失败: %v\n", err)
|
||||
fmt.Println("请确保当前目录存在 config.json 文件")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("✅ 配置加载成功 - addr: %s", config.Addr)
|
||||
fmt.Printf("✅ 配置加载成功 - addr: %s\n", config.Addr)
|
||||
|
||||
log := xlogger.NewLogger()
|
||||
logger.SetDefault(log)
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/observer/stats"
|
||||
@@ -18,6 +20,15 @@ import (
|
||||
var httpReportURL string
|
||||
var configReportURL string
|
||||
var httpAESCrypto *crypto.AESCrypto // 新增:HTTP上报加密器
|
||||
var reportURLPreferenceMutex sync.RWMutex
|
||||
var preferredUploadURL string
|
||||
var preferredConfigURL string
|
||||
var reportDo = func(ctx context.Context, req *http.Request, timeout time.Duration) (*http.Response, error) {
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
return client.Do(req.WithContext(ctx))
|
||||
}
|
||||
|
||||
// TrafficReportItem 流量报告项(压缩格式)
|
||||
type TrafficReportItem struct {
|
||||
@@ -27,8 +38,17 @@ type TrafficReportItem struct {
|
||||
}
|
||||
|
||||
func SetHTTPReportURL(addr string, secret string) {
|
||||
httpReportURL = "http://" + addr + "/flow/upload?secret=" + secret
|
||||
configReportURL = "http://" + addr + "/flow/config?secret=" + secret
|
||||
uploadURLs, configURLs := buildReportURLCandidates(addr, secret)
|
||||
if len(uploadURLs) > 0 {
|
||||
httpReportURL = strings.Join(uploadURLs, ",")
|
||||
}
|
||||
if len(configURLs) > 0 {
|
||||
configReportURL = strings.Join(configURLs, ",")
|
||||
}
|
||||
reportURLPreferenceMutex.Lock()
|
||||
preferredUploadURL = ""
|
||||
preferredConfigURL = ""
|
||||
reportURLPreferenceMutex.Unlock()
|
||||
|
||||
// 创建 AES 加密器
|
||||
var err error
|
||||
@@ -41,8 +61,173 @@ func SetHTTPReportURL(addr string, secret string) {
|
||||
}
|
||||
}
|
||||
|
||||
func buildReportURLCandidates(addr string, secret string) (upload []string, config []string) {
|
||||
normalizedAddr, explicitScheme := normalizeReportAddress(addr)
|
||||
if normalizedAddr == "" {
|
||||
normalizedAddr = strings.TrimSpace(addr)
|
||||
}
|
||||
|
||||
schemes := []string{"https", "http"}
|
||||
if mappedScheme := mapToHTTPScheme(explicitScheme); mappedScheme == "http" {
|
||||
schemes = []string{"http", "https"}
|
||||
}
|
||||
|
||||
upload = []string{
|
||||
schemes[0] + "://" + normalizedAddr + "/flow/upload?secret=" + secret,
|
||||
schemes[1] + "://" + normalizedAddr + "/flow/upload?secret=" + secret,
|
||||
}
|
||||
config = []string{
|
||||
schemes[0] + "://" + normalizedAddr + "/flow/config?secret=" + secret,
|
||||
schemes[1] + "://" + normalizedAddr + "/flow/config?secret=" + secret,
|
||||
}
|
||||
return upload, config
|
||||
}
|
||||
|
||||
func normalizeReportAddress(addr string) (string, string) {
|
||||
raw := strings.TrimSpace(addr)
|
||||
if raw == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
scheme := ""
|
||||
if idx := strings.Index(raw, "://"); idx > 0 {
|
||||
scheme = strings.ToLower(strings.TrimSpace(raw[:idx]))
|
||||
if parsed, err := url.Parse(raw); err == nil {
|
||||
if host := strings.TrimSpace(parsed.Host); host != "" {
|
||||
return host, scheme
|
||||
}
|
||||
}
|
||||
raw = raw[idx+3:]
|
||||
}
|
||||
|
||||
if idx := strings.IndexAny(raw, "/?#"); idx >= 0 {
|
||||
raw = raw[:idx]
|
||||
}
|
||||
return strings.TrimSpace(raw), scheme
|
||||
}
|
||||
|
||||
func mapToHTTPScheme(scheme string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(scheme)) {
|
||||
case "https", "wss":
|
||||
return "https"
|
||||
case "http", "ws":
|
||||
return "http"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func loadPreferredURL(preferred *string) string {
|
||||
if preferred == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
reportURLPreferenceMutex.RLock()
|
||||
defer reportURLPreferenceMutex.RUnlock()
|
||||
return *preferred
|
||||
}
|
||||
|
||||
func storePreferredURL(preferred *string, value string) {
|
||||
if preferred == nil {
|
||||
return
|
||||
}
|
||||
|
||||
reportURLPreferenceMutex.Lock()
|
||||
defer reportURLPreferenceMutex.Unlock()
|
||||
*preferred = value
|
||||
}
|
||||
|
||||
func prioritizeURLs(urls []string, preferred string) []string {
|
||||
ordered := append([]string(nil), urls...)
|
||||
if preferred == "" || len(ordered) < 2 {
|
||||
return ordered
|
||||
}
|
||||
|
||||
for i, targetURL := range ordered {
|
||||
if targetURL == preferred {
|
||||
if i > 0 {
|
||||
ordered[0], ordered[i] = ordered[i], ordered[0]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ordered
|
||||
}
|
||||
|
||||
func postJSONWithFallback(ctx context.Context, urls []string, requestBody []byte, userAgent string, timeout time.Duration, preferred *string) (bool, error) {
|
||||
if len(urls) == 0 {
|
||||
return false, fmt.Errorf("上报URL未设置")
|
||||
}
|
||||
|
||||
orderedURLs := prioritizeURLs(urls, loadPreferredURL(preferred))
|
||||
|
||||
var errs []string
|
||||
for i, targetURL := range orderedURLs {
|
||||
req, err := http.NewRequest("POST", targetURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s => 创建请求失败: %v", targetURL, err))
|
||||
if i < len(orderedURLs)-1 {
|
||||
fmt.Printf("⚠️ HTTP上报尝试失败,准备回退: %s => 创建请求失败: %v\n", targetURL, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
resp, err := reportDo(ctx, req, timeout)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s => 请求失败: %v", targetURL, err))
|
||||
if i < len(orderedURLs)-1 {
|
||||
fmt.Printf("⚠️ HTTP上报尝试失败,准备回退: %s => 请求失败: %v\n", targetURL, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var responseBytes bytes.Buffer
|
||||
_, readErr := responseBytes.ReadFrom(resp.Body)
|
||||
resp.Body.Close()
|
||||
if readErr != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s => 读取响应失败: %v", targetURL, readErr))
|
||||
if i < len(orderedURLs)-1 {
|
||||
fmt.Printf("⚠️ HTTP上报尝试失败,准备回退: %s => 读取响应失败: %v\n", targetURL, readErr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errs = append(errs, fmt.Sprintf("%s => HTTP响应错误: %d %s", targetURL, resp.StatusCode, resp.Status))
|
||||
if i < len(orderedURLs)-1 {
|
||||
fmt.Printf("⚠️ HTTP上报尝试失败,准备回退: %s => HTTP响应错误: %d %s\n", targetURL, resp.StatusCode, resp.Status)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
responseText := strings.TrimSpace(responseBytes.String())
|
||||
if responseText == "ok" {
|
||||
if i > 0 {
|
||||
fmt.Printf("↪️ HTTP上报已自动回退到: %s\n", targetURL)
|
||||
}
|
||||
storePreferredURL(preferred, targetURL)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
errs = append(errs, fmt.Sprintf("%s => 服务器响应: %s (期望: ok)", targetURL, responseText))
|
||||
if i < len(orderedURLs)-1 {
|
||||
fmt.Printf("⚠️ HTTP上报尝试失败,准备回退: %s => 服务器响应: %s (期望: ok)\n", targetURL, responseText)
|
||||
}
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("发送HTTP请求失败: %s", strings.Join(errs, " | "))
|
||||
}
|
||||
|
||||
// sendBatchTrafficReport 批量发送多个服务的流量报告到HTTP接口
|
||||
func sendBatchTrafficReport(ctx context.Context, reportItems []TrafficReportItem) (bool, error) {
|
||||
if httpReportURL == "" {
|
||||
return false, fmt.Errorf("流量上报URL未设置")
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reportItems)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("序列化报告数据失败: %v", err)
|
||||
@@ -73,46 +258,16 @@ func sendBatchTrafficReport(ctx context.Context, reportItems []TrafficReportItem
|
||||
requestBody = jsonData
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", httpReportURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("创建HTTP请求失败: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "GOST-Traffic-Reporter/1.0")
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("发送HTTP请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, fmt.Errorf("HTTP响应错误: %d %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
// 读取响应内容
|
||||
var responseBytes bytes.Buffer
|
||||
_, err = responseBytes.ReadFrom(resp.Body)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("读取响应内容失败: %v", err)
|
||||
}
|
||||
|
||||
responseText := strings.TrimSpace(responseBytes.String())
|
||||
|
||||
// 检查响应是否为"ok"
|
||||
if responseText == "ok" {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, fmt.Errorf("服务器响应: %s (期望: ok)", responseText)
|
||||
}
|
||||
return postJSONWithFallback(
|
||||
ctx,
|
||||
strings.Split(httpReportURL, ","),
|
||||
requestBody,
|
||||
"GOST-Traffic-Reporter/1.0",
|
||||
5*time.Second,
|
||||
&preferredUploadURL,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// sendConfigReport 发送配置报告到HTTP接口
|
||||
func sendConfigReport(ctx context.Context) (bool, error) {
|
||||
if configReportURL == "" {
|
||||
@@ -150,43 +305,14 @@ func sendConfigReport(ctx context.Context) (bool, error) {
|
||||
requestBody = configData
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", configReportURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("创建HTTP请求失败: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "Config-Reporter/1.0")
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second, // 配置上报可以稍长一些
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("发送HTTP请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, fmt.Errorf("HTTP响应错误: %d %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
// 读取响应内容
|
||||
var responseBytes bytes.Buffer
|
||||
_, err = responseBytes.ReadFrom(resp.Body)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("读取响应内容失败: %v", err)
|
||||
}
|
||||
|
||||
responseText := strings.TrimSpace(responseBytes.String())
|
||||
|
||||
// 检查响应是否为"ok"
|
||||
if responseText == "ok" {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, fmt.Errorf("服务器响应: %s (期望: ok)", responseText)
|
||||
}
|
||||
return postJSONWithFallback(
|
||||
ctx,
|
||||
strings.Split(configReportURL, ","),
|
||||
requestBody,
|
||||
"Config-Reporter/1.0",
|
||||
10*time.Second,
|
||||
&preferredConfigURL,
|
||||
)
|
||||
}
|
||||
|
||||
// StartConfigReporter 启动配置定时上报器(每10分钟上报一次)
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBuildReportURLCandidatesSecureFirst(t *testing.T) {
|
||||
upload, config := buildReportURLCandidates("panel.example.com:443", "abc")
|
||||
|
||||
if len(upload) != 2 {
|
||||
t.Fatalf("expected 2 upload candidates, got %d", len(upload))
|
||||
}
|
||||
if len(config) != 2 {
|
||||
t.Fatalf("expected 2 config candidates, got %d", len(config))
|
||||
}
|
||||
|
||||
if upload[0] != "https://panel.example.com:443/flow/upload?secret=abc" {
|
||||
t.Fatalf("unexpected upload[0]: %s", upload[0])
|
||||
}
|
||||
if upload[1] != "http://panel.example.com:443/flow/upload?secret=abc" {
|
||||
t.Fatalf("unexpected upload[1]: %s", upload[1])
|
||||
}
|
||||
if config[0] != "https://panel.example.com:443/flow/config?secret=abc" {
|
||||
t.Fatalf("unexpected config[0]: %s", config[0])
|
||||
}
|
||||
if config[1] != "http://panel.example.com:443/flow/config?secret=abc" {
|
||||
t.Fatalf("unexpected config[1]: %s", config[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildReportURLCandidatesNormalizeSchemeAddr(t *testing.T) {
|
||||
upload, config := buildReportURLCandidates("https://panel.example.com:8443/path", "abc")
|
||||
|
||||
if upload[0] != "https://panel.example.com:8443/flow/upload?secret=abc" {
|
||||
t.Fatalf("unexpected upload[0]: %s", upload[0])
|
||||
}
|
||||
if upload[1] != "http://panel.example.com:8443/flow/upload?secret=abc" {
|
||||
t.Fatalf("unexpected upload[1]: %s", upload[1])
|
||||
}
|
||||
if config[0] != "https://panel.example.com:8443/flow/config?secret=abc" {
|
||||
t.Fatalf("unexpected config[0]: %s", config[0])
|
||||
}
|
||||
if config[1] != "http://panel.example.com:8443/flow/config?secret=abc" {
|
||||
t.Fatalf("unexpected config[1]: %s", config[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostJSONWithFallbackUsesHTTPAfterHTTPSFailure(t *testing.T) {
|
||||
orig := reportDo
|
||||
defer func() { reportDo = orig }()
|
||||
|
||||
var calls []string
|
||||
reportDo = func(_ context.Context, req *http.Request, _ time.Duration) (*http.Response, error) {
|
||||
calls = append(calls, req.URL.String())
|
||||
if strings.HasPrefix(req.URL.String(), "https://") {
|
||||
return nil, errors.New("tls handshake failed")
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader("ok")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
ok, err := postJSONWithFallback(
|
||||
context.Background(),
|
||||
[]string{
|
||||
"https://panel.example.com:443/flow/upload?secret=abc",
|
||||
"http://panel.example.com:443/flow/upload?secret=abc",
|
||||
},
|
||||
[]byte(`[]`),
|
||||
"GOST-Traffic-Reporter/1.0",
|
||||
5*time.Second,
|
||||
nil,
|
||||
)
|
||||
if !ok || err != nil {
|
||||
t.Fatalf("expected fallback success, ok=%v err=%v", ok, err)
|
||||
}
|
||||
if len(calls) != 2 {
|
||||
t.Fatalf("expected 2 calls, got %d", len(calls))
|
||||
}
|
||||
if !strings.HasPrefix(calls[0], "https://") || !strings.HasPrefix(calls[1], "http://") {
|
||||
t.Fatalf("unexpected call order: %#v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostJSONWithFallbackRemembersDetectedURL(t *testing.T) {
|
||||
orig := reportDo
|
||||
defer func() { reportDo = orig }()
|
||||
|
||||
targets := []string{
|
||||
"https://panel.example.com:443/flow/upload?secret=abc",
|
||||
"http://panel.example.com:443/flow/upload?secret=abc",
|
||||
}
|
||||
|
||||
var preferred string
|
||||
var calls []string
|
||||
reportDo = func(_ context.Context, req *http.Request, _ time.Duration) (*http.Response, error) {
|
||||
calls = append(calls, req.URL.String())
|
||||
if strings.HasPrefix(req.URL.String(), "https://") {
|
||||
return nil, errors.New("tls handshake failed")
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader("ok")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
ok, err := postJSONWithFallback(
|
||||
context.Background(),
|
||||
targets,
|
||||
[]byte(`[]`),
|
||||
"GOST-Traffic-Reporter/1.0",
|
||||
5*time.Second,
|
||||
&preferred,
|
||||
)
|
||||
if !ok || err != nil {
|
||||
t.Fatalf("expected first call success, ok=%v err=%v", ok, err)
|
||||
}
|
||||
if preferred != targets[1] {
|
||||
t.Fatalf("expected preferred url to be remembered as %s, got %s", targets[1], preferred)
|
||||
}
|
||||
if len(calls) != 2 {
|
||||
t.Fatalf("expected 2 calls on first attempt, got %d", len(calls))
|
||||
}
|
||||
|
||||
calls = nil
|
||||
ok, err = postJSONWithFallback(
|
||||
context.Background(),
|
||||
targets,
|
||||
[]byte(`[]`),
|
||||
"GOST-Traffic-Reporter/1.0",
|
||||
5*time.Second,
|
||||
&preferred,
|
||||
)
|
||||
if !ok || err != nil {
|
||||
t.Fatalf("expected second call success, ok=%v err=%v", ok, err)
|
||||
}
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("expected second call to use remembered url once, got %d calls", len(calls))
|
||||
}
|
||||
if !strings.HasPrefix(calls[0], "http://") {
|
||||
t.Fatalf("expected remembered http url first, got %s", calls[0])
|
||||
}
|
||||
}
|
||||
@@ -97,20 +97,25 @@ const (
|
||||
)
|
||||
|
||||
type WebSocketReporter struct {
|
||||
url string
|
||||
addr string // 保存服务器地址
|
||||
secret string // 保存密钥
|
||||
version string // 保存版本号
|
||||
conn *websocket.Conn
|
||||
reconnectTime time.Duration
|
||||
pingInterval time.Duration
|
||||
configInterval time.Duration
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
connected bool
|
||||
connecting bool // 新增:正在连接状态
|
||||
connMutex sync.Mutex // 新增:连接状态锁
|
||||
aesCrypto *crypto.AESCrypto // 新增:AES加密器
|
||||
url string
|
||||
addr string // 保存服务器地址
|
||||
secret string // 保存密钥
|
||||
version string // 保存版本号
|
||||
preferredWSScheme string
|
||||
conn *websocket.Conn
|
||||
reconnectTime time.Duration
|
||||
pingInterval time.Duration
|
||||
configInterval time.Duration
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
connected bool
|
||||
connecting bool // 新增:正在连接状态
|
||||
connMutex sync.Mutex // 新增:连接状态锁
|
||||
aesCrypto *crypto.AESCrypto // 新增:AES加密器
|
||||
}
|
||||
|
||||
var wsDial = func(dialer *websocket.Dialer, rawURL string) (*websocket.Conn, *http.Response, error) {
|
||||
return dialer.Dial(rawURL, nil)
|
||||
}
|
||||
|
||||
// NewWebSocketReporter 创建一个新的WebSocket报告器
|
||||
@@ -223,21 +228,14 @@ func (w *WebSocketReporter) connect() error {
|
||||
json.Unmarshal(b, &cfg)
|
||||
}
|
||||
|
||||
// 使用最新的配置重新构建 URL
|
||||
currentURL := "ws://" + w.addr + "/system-info?type=1&secret=" + w.secret + "&version=" + w.version +
|
||||
"&http=" + strconv.Itoa(cfg.Http) + "&tls=" + strconv.Itoa(cfg.Tls) + "&socks=" + strconv.Itoa(cfg.Socks)
|
||||
|
||||
u, err := url.Parse(currentURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析URL失败: %v", err)
|
||||
}
|
||||
candidates := buildWebSocketCandidates(w.addr, w.secret, w.version, cfg.Http, cfg.Tls, cfg.Socks, w.preferredWSScheme)
|
||||
|
||||
dialer := websocket.DefaultDialer
|
||||
dialer.HandshakeTimeout = 10 * time.Second
|
||||
|
||||
conn, _, err := dialer.Dial(u.String(), nil)
|
||||
conn, usedURL, err := dialWebSocketWithFallback(dialer, candidates)
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接WebSocket失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果在连接过程中已经有连接了,关闭新连接
|
||||
@@ -248,6 +246,9 @@ func (w *WebSocketReporter) connect() error {
|
||||
|
||||
w.conn = conn
|
||||
w.connected = true
|
||||
if scheme := detectWebSocketScheme(usedURL); scheme != "" {
|
||||
w.preferredWSScheme = scheme
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Now().Add(reporterReadWait))
|
||||
conn.SetPingHandler(func(appData string) error {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(reporterReadWait))
|
||||
@@ -265,10 +266,145 @@ func (w *WebSocketReporter) connect() error {
|
||||
return nil
|
||||
})
|
||||
|
||||
fmt.Printf("✅ WebSocket连接建立成功 (http=%d, tls=%d, socks=%d)\n", cfg.Http, cfg.Tls, cfg.Socks)
|
||||
fmt.Printf("✅ WebSocket连接建立成功 (%s, http=%d, tls=%d, socks=%d)\n", sanitizeWebSocketURL(usedURL), cfg.Http, cfg.Tls, cfg.Socks)
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildWebSocketCandidates(addr string, secret string, version string, http int, tls int, socks int, preferredScheme string) []string {
|
||||
normalizedAddr, explicitScheme := normalizeReporterAddress(addr)
|
||||
if normalizedAddr == "" {
|
||||
normalizedAddr = strings.TrimSpace(addr)
|
||||
}
|
||||
|
||||
query := "/system-info?type=1&secret=" + secret + "&version=" + version +
|
||||
"&http=" + strconv.Itoa(http) + "&tls=" + strconv.Itoa(tls) + "&socks=" + strconv.Itoa(socks)
|
||||
|
||||
schemes := []string{"wss", "ws"}
|
||||
if mappedScheme := mapToWebSocketScheme(explicitScheme); mappedScheme != "" {
|
||||
if mappedScheme == "ws" {
|
||||
schemes = []string{"ws", "wss"}
|
||||
}
|
||||
} else if preferredScheme == "ws" {
|
||||
schemes = []string{"ws", "wss"}
|
||||
}
|
||||
|
||||
return []string{
|
||||
schemes[0] + "://" + normalizedAddr + query,
|
||||
schemes[1] + "://" + normalizedAddr + query,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeReporterAddress(addr string) (string, string) {
|
||||
raw := strings.TrimSpace(addr)
|
||||
if raw == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
scheme := ""
|
||||
if idx := strings.Index(raw, "://"); idx > 0 {
|
||||
scheme = strings.ToLower(strings.TrimSpace(raw[:idx]))
|
||||
if parsed, err := url.Parse(raw); err == nil {
|
||||
if host := strings.TrimSpace(parsed.Host); host != "" {
|
||||
return host, scheme
|
||||
}
|
||||
}
|
||||
raw = raw[idx+3:]
|
||||
}
|
||||
|
||||
if idx := strings.IndexAny(raw, "/?#"); idx >= 0 {
|
||||
raw = raw[:idx]
|
||||
}
|
||||
return strings.TrimSpace(raw), scheme
|
||||
}
|
||||
|
||||
func mapToWebSocketScheme(scheme string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(scheme)) {
|
||||
case "wss", "https":
|
||||
return "wss"
|
||||
case "ws", "http":
|
||||
return "ws"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func detectWebSocketScheme(rawURL string) string {
|
||||
if strings.HasPrefix(rawURL, "wss://") {
|
||||
return "wss"
|
||||
}
|
||||
if strings.HasPrefix(rawURL, "ws://") {
|
||||
return "ws"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func dialWebSocketWithFallback(dialer *websocket.Dialer, candidates []string) (*websocket.Conn, string, error) {
|
||||
if len(candidates) == 0 {
|
||||
return nil, "", fmt.Errorf("WebSocket候选地址为空")
|
||||
}
|
||||
|
||||
var errs []string
|
||||
for i, targetURL := range candidates {
|
||||
conn, resp, err := wsDial(dialer, targetURL)
|
||||
if err == nil {
|
||||
if i > 0 {
|
||||
fmt.Printf("↪️ WebSocket已自动回退成功: %s\n", sanitizeWebSocketURL(targetURL))
|
||||
}
|
||||
return conn, targetURL, nil
|
||||
}
|
||||
errMsg := formatWebSocketDialError(err, resp)
|
||||
errs = append(errs, fmt.Sprintf("%s => %s", sanitizeWebSocketURL(targetURL), errMsg))
|
||||
if i < len(candidates)-1 {
|
||||
fmt.Printf(
|
||||
"⚠️ WebSocket连接失败,准备从 %s 回退到 %s: %s\n",
|
||||
strings.ToUpper(detectWebSocketScheme(targetURL)),
|
||||
strings.ToUpper(detectWebSocketScheme(candidates[i+1])),
|
||||
errMsg,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, "", fmt.Errorf("连接WebSocket失败(已尝试%d种协议): %s", len(candidates), strings.Join(errs, " | "))
|
||||
}
|
||||
|
||||
func sanitizeWebSocketURL(rawURL string) string {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return rawURL
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
if q.Get("secret") != "" {
|
||||
q.Set("secret", "***")
|
||||
u.RawQuery = q.Encode()
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func formatWebSocketDialError(err error, resp *http.Response) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
if resp == nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("%s (HTTP %s)", err, resp.Status)
|
||||
if resp.Body == nil {
|
||||
return msg
|
||||
}
|
||||
|
||||
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 256))
|
||||
if readErr != nil {
|
||||
return msg
|
||||
}
|
||||
bodyText := strings.TrimSpace(string(body))
|
||||
if bodyText == "" {
|
||||
return msg
|
||||
}
|
||||
return fmt.Sprintf("%s, body=%q", msg, bodyText)
|
||||
}
|
||||
|
||||
// handleConnection 处理WebSocket连接
|
||||
func (w *WebSocketReporter) handleConnection() {
|
||||
defer func() {
|
||||
@@ -1290,7 +1426,8 @@ func getMemoryInfo() MemoryInfo {
|
||||
func StartWebSocketReporterWithConfig(addr string, secret string, http int, tls int, socks int, version string) *WebSocketReporter {
|
||||
|
||||
// 构建初始 WebSocket URL
|
||||
fullURL := "ws://" + addr + "/system-info?type=1&secret=" + secret + "&version=" + version + "&http=" + strconv.Itoa(http) + "&tls=" + strconv.Itoa(tls) + "&socks=" + strconv.Itoa(socks)
|
||||
candidates := buildWebSocketCandidates(addr, secret, version, http, tls, socks, "")
|
||||
fullURL := candidates[0]
|
||||
|
||||
fmt.Printf("🔗 WebSocket连接URL: %s\n", fullURL)
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package socket
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func TestBuildWebSocketCandidatesSecureFirst(t *testing.T) {
|
||||
candidates := buildWebSocketCandidates("panel.example.com:443", "abc", "2.0.2", 1, 0, 1, "")
|
||||
|
||||
if len(candidates) != 2 {
|
||||
t.Fatalf("expected 2 candidates, got %d", len(candidates))
|
||||
}
|
||||
if !strings.HasPrefix(candidates[0], "wss://") {
|
||||
t.Fatalf("expected first candidate to start with wss://, got %s", candidates[0])
|
||||
}
|
||||
if !strings.HasPrefix(candidates[1], "ws://") {
|
||||
t.Fatalf("expected second candidate to start with ws://, got %s", candidates[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWebSocketCandidatesUsesPreferredScheme(t *testing.T) {
|
||||
candidates := buildWebSocketCandidates("panel.example.com:443", "abc", "2.0.2", 1, 0, 1, "ws")
|
||||
|
||||
if len(candidates) != 2 {
|
||||
t.Fatalf("expected 2 candidates, got %d", len(candidates))
|
||||
}
|
||||
if !strings.HasPrefix(candidates[0], "ws://") {
|
||||
t.Fatalf("expected preferred ws:// candidate first, got %s", candidates[0])
|
||||
}
|
||||
if !strings.HasPrefix(candidates[1], "wss://") {
|
||||
t.Fatalf("expected fallback wss:// candidate second, got %s", candidates[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWebSocketCandidatesNormalizesSchemePrefixedAddr(t *testing.T) {
|
||||
candidates := buildWebSocketCandidates("https://panel.example.com:443/path?q=1", "abc", "2.0.2", 0, 0, 0, "")
|
||||
|
||||
if len(candidates) != 2 {
|
||||
t.Fatalf("expected 2 candidates, got %d", len(candidates))
|
||||
}
|
||||
if !strings.HasPrefix(candidates[0], "wss://panel.example.com:443/") {
|
||||
t.Fatalf("expected normalized wss candidate, got %s", candidates[0])
|
||||
}
|
||||
if !strings.HasPrefix(candidates[1], "ws://panel.example.com:443/") {
|
||||
t.Fatalf("expected normalized ws fallback candidate, got %s", candidates[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialWebSocketWithFallbackTriesWSAfterWSSFailure(t *testing.T) {
|
||||
orig := wsDial
|
||||
defer func() { wsDial = orig }()
|
||||
|
||||
var attempts []string
|
||||
wsDial = func(_ *websocket.Dialer, rawURL string) (*websocket.Conn, *http.Response, error) {
|
||||
attempts = append(attempts, rawURL)
|
||||
if strings.HasPrefix(rawURL, "wss://") {
|
||||
return nil, nil, errors.New("tls failed")
|
||||
}
|
||||
return &websocket.Conn{}, nil, nil
|
||||
}
|
||||
|
||||
_, usedURL, err := dialWebSocketWithFallback(
|
||||
&websocket.Dialer{},
|
||||
[]string{
|
||||
"wss://panel.example.com/system-info?type=1&secret=abc",
|
||||
"ws://panel.example.com/system-info?type=1&secret=abc",
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected fallback success, got err=%v", err)
|
||||
}
|
||||
if !strings.HasPrefix(usedURL, "ws://") {
|
||||
t.Fatalf("expected fallback ws:// url, got %s", usedURL)
|
||||
}
|
||||
if len(attempts) != 2 {
|
||||
t.Fatalf("expected 2 attempts, got %d", len(attempts))
|
||||
}
|
||||
if !strings.HasPrefix(attempts[0], "wss://") || !strings.HasPrefix(attempts[1], "ws://") {
|
||||
t.Fatalf("unexpected attempt order: %#v", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectWebSocketScheme(t *testing.T) {
|
||||
if detectWebSocketScheme("wss://panel.example.com/system-info") != "wss" {
|
||||
t.Fatalf("expected wss detection")
|
||||
}
|
||||
if detectWebSocketScheme("ws://panel.example.com/system-info") != "ws" {
|
||||
t.Fatalf("expected ws detection")
|
||||
}
|
||||
if detectWebSocketScheme("http://panel.example.com/system-info") != "" {
|
||||
t.Fatalf("expected empty detection for non-websocket scheme")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeWebSocketURL(t *testing.T) {
|
||||
raw := "wss://panel.example.com/system-info?type=1&secret=abc&version=2.0.2"
|
||||
sanitized := sanitizeWebSocketURL(raw)
|
||||
|
||||
if strings.Contains(sanitized, "secret=abc") {
|
||||
t.Fatalf("expected secret to be masked, got %s", sanitized)
|
||||
}
|
||||
if !strings.Contains(sanitized, "secret=%2A%2A%2A") {
|
||||
t.Fatalf("expected masked secret in url, got %s", sanitized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWebSocketDialErrorIncludesHTTPStatus(t *testing.T) {
|
||||
err := errors.New("websocket: bad handshake")
|
||||
resp := &http.Response{
|
||||
Status: "403 Forbidden",
|
||||
Body: io.NopCloser(strings.NewReader("forbidden")),
|
||||
}
|
||||
|
||||
msg := formatWebSocketDialError(err, resp)
|
||||
if !strings.Contains(msg, "HTTP 403 Forbidden") {
|
||||
t.Fatalf("expected status in message, got %s", msg)
|
||||
}
|
||||
if !strings.Contains(msg, "forbidden") {
|
||||
t.Fatalf("expected response body in message, got %s", msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Node Full-Stack Tags Removal Plan
|
||||
|
||||
- [x] Remove node tags usage from frontend node management and dashboard views
|
||||
- [x] Remove node tags fields from backend models, handlers, repository, and backup logic
|
||||
- [x] Verify frontend build and backend tests pass after the removal
|
||||
@@ -0,0 +1,5 @@
|
||||
# PR 292 Node Page Merge Conflict Resolution Plan
|
||||
|
||||
- [x] Review the conflicted node page and identify all overlapping feature areas from main and PR #292
|
||||
- [x] Merge tab split, per-tab search, remote usage cards, expiry filters, and renewal indicators into `vite-frontend/src/pages/node.tsx`
|
||||
- [x] Build `vite-frontend` and fix any integration issues from the merged result
|
||||
@@ -4,7 +4,6 @@ export interface NodeApiItem {
|
||||
status: number;
|
||||
inx?: number;
|
||||
remark?: string;
|
||||
tags?: string;
|
||||
expiryTime?: number;
|
||||
renewalCycle?: "month" | "quarter" | "year" | "";
|
||||
syncError?: string;
|
||||
@@ -220,7 +219,6 @@ export interface NodeMutationPayload {
|
||||
status?: number;
|
||||
inx?: number;
|
||||
remark?: string;
|
||||
tags?: string;
|
||||
expiryTime?: number;
|
||||
renewalCycle?: "month" | "quarter" | "year" | "";
|
||||
serverIp?: string;
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function DashboardPage() {
|
||||
};
|
||||
}
|
||||
|
||||
const diffDays = Math.ceil(
|
||||
const diffDays = Math.ceil(
|
||||
(nextDueTime - Date.now()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
|
||||
@@ -122,16 +122,6 @@ const diffDays = Math.ceil(
|
||||
};
|
||||
};
|
||||
|
||||
const parseNodeTags = (tags?: string) => {
|
||||
if (!tags) return [];
|
||||
|
||||
return tags
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 3);
|
||||
};
|
||||
|
||||
const renderNodeExpiryCard = (node: DashboardNodeExpiryItem) => {
|
||||
const renewalSnapshot = getNodeRenewalSnapshot(
|
||||
node.expiryTime,
|
||||
@@ -141,7 +131,6 @@ const diffDays = Math.ceil(
|
||||
renewalSnapshot.nextDueTime,
|
||||
renewalSnapshot.state,
|
||||
);
|
||||
const tags = parseNodeTags(node.tags);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -177,19 +166,6 @@ const diffDays = Math.ceil(
|
||||
{node.remark.trim()}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={`${node.id}-${tag}`}
|
||||
className="rounded-full bg-white/80 px-2 py-1 text-[11px] text-default-600 ring-1 ring-amber-200/80 dark:bg-white/5 dark:text-default-300 dark:ring-amber-500/20"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -58,7 +58,6 @@ export interface DashboardNodeExpiryItem {
|
||||
id: number;
|
||||
name: string;
|
||||
remark?: string;
|
||||
tags?: string;
|
||||
expiryTime?: number;
|
||||
renewalCycle?: "month" | "quarter" | "year" | "";
|
||||
}
|
||||
@@ -222,7 +221,6 @@ const normalizeNodeExpiryReminders = (items: NodeApiItem[]) => {
|
||||
id: item.id,
|
||||
name: item.name || "",
|
||||
remark: typeof item.remark === "string" ? item.remark : "",
|
||||
tags: typeof item.tags === "string" ? item.tags : "",
|
||||
renewalCycle: normalizeDashboardRenewalCycle(item.renewalCycle),
|
||||
expiryTime:
|
||||
typeof item.expiryTime === "number" && item.expiryTime > 0
|
||||
|
||||
+606
-261
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user