├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1.issue.yml │ ├── 2.feature.yml │ └── config.yml ├── close_issue.py └── workflows │ ├── CloseIssue.yml │ ├── housekeeping-stale-issues.yaml │ └── issue-translator.yml_bak ├── .gitignore ├── Ad └── README.md ├── Dockerfile ├── Issue ├── issue.en.md └── issue.md ├── LICENSE ├── README.en.md ├── README.md ├── caddy └── Caddyfile ├── cloud ├── ClawCloud │ └── README.md ├── Koyeb │ └── README.md └── Render │ └── README.md ├── config ├── README.md ├── registry-elastic.yml ├── registry-gcr.yml ├── registry-ghcr.yml ├── registry-hub.yml ├── registry-k8s.yml ├── registry-k8sgcr.yml ├── registry-mcr.yml ├── registry-nvcr.yml └── registry-quay.yml ├── docker-compose.yaml ├── hubcmdui ├── README.md ├── app.js ├── cleanup.js ├── compatibility-layer.js ├── config.js ├── config.json ├── config │ ├── menu.json │ └── monitoring.json ├── data │ └── config.json ├── docker-compose.yaml ├── documentation │ ├── .DS_Store │ ├── 1743542841590.json │ ├── 1743543376091.json │ └── 1743543400369.json ├── download-images.js ├── init-dirs.js ├── logger.js ├── middleware │ ├── auth.js │ └── client-error.js ├── models │ └── MenuItem.js ├── package.json ├── routes │ ├── auth.js │ ├── config.js │ ├── docker.js │ ├── dockerhub.js │ ├── documentation.js │ ├── health.js │ ├── index.js │ ├── login.js │ ├── monitoring.js │ ├── routeLoader.js │ ├── system.js │ └── systemStatus.js ├── scripts │ ├── diagnostics.js │ ├── init-menu.js │ └── init-system.js ├── server-utils.js ├── server.js ├── services │ ├── configService.js │ ├── dockerHubService.js │ ├── dockerService.js │ ├── documentationService.js │ ├── monitoringService.js │ ├── networkService.js │ ├── notificationService.js │ ├── systemService.js │ └── userService.js ├── start-diagnostic.js ├── users.json └── web │ ├── .DS_Store │ ├── admin.html │ ├── compatibility-layer.js │ ├── css │ ├── admin.css │ └── custom.css │ ├── data │ └── documentation │ │ └── index.json │ ├── images │ └── login-bg.jpg │ ├── index.html │ ├── js │ ├── app.js │ ├── auth.js │ ├── core.js │ ├── dockerManager.js │ ├── documentManager.js │ ├── error-handler.js │ ├── menuManager.js │ ├── nav-menu.js │ ├── networkTest.js │ ├── systemStatus.js │ └── userCenter.js │ ├── services │ └── documentationService.js │ └── style.css ├── install └── DockerProxy_Install.sh └── nginx └── registry-proxy.conf /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://github.com/dqzboy/dqzboy/blob/main/.github/FUNDING.md'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1.issue.yml: -------------------------------------------------------------------------------- 1 | name: 反馈问题 🐛 2 | description: 项目运行中遇到的Bug或问题。 3 | title: "🐞 反馈问题:" 4 | labels: ['status: needs check'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | ### ⚠️ 前置确认 (温馨提示: 未star项目会被自动关闭issue哦!) 10 | 1. 是否国外服务器,并且未被墙(必须) 11 | 2. 服务器规格是否 >= 1C1G(必须) 12 | 3. 在提交issue之前,请务必确保没有包含个人数据!!! 13 | - type: checkboxes 14 | attributes: 15 | label: 前置确认 16 | options: 17 | - label: 我确认使用的是国外未被墙的服务器,并且服务器规格 >= 1C1G 18 | required: true 19 | - type: checkboxes 20 | attributes: 21 | label: ⚠️ 采用脚本进行部署,非脚本部署问题将不被受理 22 | options: 23 | - label: 我确认使用的是最新脚本部署 24 | required: true 25 | - type: checkboxes 26 | attributes: 27 | label: ⚠️ 搜索issues中是否已存在类似问题 28 | description: > 29 | 请在 [历史issue](https://github.com/dqzboy/Docker-Proxy/issues) 中清空输入框,搜索你的问题或在[问题总结](https://github.com/dqzboy/Docker-Proxy/blob/main/Issue/issue.md)查找相关问题,以及相关日志的关键词来查找是否存在类似问题。 30 | options: 31 | - label: 我已经搜索过issues、disscussions和问题总结,没有跟我遇到的问题相关的issue 32 | required: true 33 | - type: markdown 34 | attributes: 35 | value: | 36 | 请在上方的`title`中填写你对你所遇到问题的简略总结,这将帮助其他人更好的找到相似问题,谢谢❤️。 37 | - type: dropdown 38 | attributes: 39 | label: 操作系统类型? 40 | description: > 41 | 请选择你运行程序的操作系统类型。 42 | options: 43 | - CentOS 7 44 | - CentOS 8 45 | - Redhat 46 | - Ubuntu 47 | - Other (请在问题中说明) 48 | validations: 49 | required: true 50 | - type: textarea 51 | attributes: 52 | label: 复现步骤 🕹 53 | description: | 54 | **⚠️ 不能复现将会关闭issue.** 55 | - type: textarea 56 | attributes: 57 | label: 问题描述 😯 58 | description: 详细描述出现的问题,或提供有关截图。 59 | - type: textarea 60 | attributes: 61 | label: 终端日志 📒 62 | description: | 63 | 在此处粘贴终端日志 64 | value: | 65 | ```log 66 | <此处粘贴终端日志> 67 | ``` 68 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2.feature.yml: -------------------------------------------------------------------------------- 1 | name: 功能建议 🚀 2 | description: 提出你对项目的新想法或建议。 3 | title: "🚀 功能建议:" 4 | labels: ['status: needs check'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 请在上方的`title`中填写简略总结,谢谢❤️。 10 | ⚠️ 温馨提示: 未`star`项目会被自动关闭issue哦! 11 | - type: checkboxes 12 | attributes: 13 | label: ⚠️ 搜索是否存在类似issue 14 | description: > 15 | 请在 [历史issue](https://github.com/dqzboy/Docker-Proxy/issues) 中清空输入框,搜索你的问题或在[问题总结](https://github.com/dqzboy/Docker-Proxy/blob/main/Issue/issue.md)查找相关问题 16 | options: 17 | - label: 我已经搜索过issues、disscussions和问题总结,没有跟我遇到的问题相关的issue 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: 总结 22 | description: 描述feature的功能。 23 | - type: textarea 24 | attributes: 25 | label: 举例 26 | description: 提供聊天示例,草图或相关网址。 27 | - type: textarea 28 | attributes: 29 | label: 动机 30 | description: 描述你提出该feature的动机,比如没有这项feature对你的使用造成了怎样的影响。 请提供更详细的场景描述,这可能会帮助我们发现并提出更好的解决方案。 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 海外高性价比VPS 4 | url: https://dqzboy.github.io/proxyui/racknerd 5 | about: 高性能VPS托管解决方案,满足您的各种网络需求 6 | -------------------------------------------------------------------------------- /.github/close_issue.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | 4 | issue_labels = ['no respect'] 5 | github_repo = 'dqzboy/Docker-Proxy' 6 | github_token = os.getenv("GITHUB_TOKEN") 7 | headers = { 8 | 'Authorization': 'Bearer ' + github_token, 9 | 'Accept': 'application/vnd.github+json', 10 | 'X-GitHub-Api-Version': '2022-11-28', 11 | } 12 | 13 | def get_stargazers(repo): 14 | page = 1 15 | _stargazers = {} 16 | while True: 17 | queries = { 18 | 'per_page': 100, 19 | 'page': page, 20 | } 21 | url = f'https://api.github.com/repos/{repo}/stargazers' 22 | resp = requests.get(url, headers=headers, params=queries) 23 | if resp.status_code != 200: 24 | raise Exception('Error get stargazers: ' + resp.text) 25 | data = resp.json() 26 | if not data: 27 | break 28 | for stargazer in data: 29 | _stargazers[stargazer['login']] = True 30 | page += 1 31 | print('list stargazers done, total: ' + str(len(_stargazers))) 32 | return _stargazers 33 | 34 | def get_issues(repo): 35 | page = 1 36 | _issues = [] 37 | while True: 38 | queries = { 39 | 'state': 'open', 40 | 'sort': 'created', 41 | 'direction': 'desc', 42 | 'per_page': 100, 43 | 'page': page, 44 | } 45 | url = f'https://api.github.com/repos/{repo}/issues' 46 | resp = requests.get(url, headers=headers, params=queries) 47 | if resp.status_code != 200: 48 | raise Exception('Error get issues: ' + resp.text) 49 | data = resp.json() 50 | if not data: 51 | break 52 | _issues += data 53 | page += 1 54 | print('list issues done, total: ' + str(len(_issues))) 55 | return _issues 56 | 57 | def close_issue(repo, issue_number): 58 | url = f'https://api.github.com/repos/{repo}/issues/{issue_number}' 59 | data = { 60 | 'state': 'closed', 61 | 'state_reason': 'not_planned', 62 | 'labels': issue_labels, 63 | } 64 | resp = requests.patch(url, headers=headers, json=data) 65 | if resp.status_code != 200: 66 | raise Exception('Error close issue: ' + resp.text) 67 | print('issue: {} closed'.format(issue_number)) 68 | 69 | def lock_issue(repo, issue_number): 70 | url = f'https://api.github.com/repos/{repo}/issues/{issue_number}/lock' 71 | data = { 72 | 'lock_reason': 'spam', 73 | } 74 | resp = requests.put(url, headers=headers, json=data) 75 | if resp.status_code != 204: 76 | raise Exception('Error lock issue: ' + resp.text) 77 | print('issue: {} locked'.format(issue_number)) 78 | 79 | if '__main__' == __name__: 80 | try: 81 | stargazers = get_stargazers(github_repo) 82 | issues = get_issues(github_repo) 83 | for issue in issues: 84 | login = issue['user']['login'] 85 | if login not in stargazers: 86 | print('issue: {}, login: {} not in stargazers'.format(issue['number'], login)) 87 | close_issue(github_repo, issue['number']) 88 | lock_issue(github_repo, issue['number']) 89 | print('done') 90 | except Exception as e: 91 | print(f"Error occurred: {str(e)}") 92 | raise 93 | -------------------------------------------------------------------------------- /.github/workflows/CloseIssue.yml: -------------------------------------------------------------------------------- 1 | name: CloseIssue 2 | 3 | on: 4 | workflow_dispatch: 5 | issues: 6 | types: [opened] 7 | 8 | jobs: 9 | run-python-script: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.10" 17 | 18 | - name: Install Dependencies 19 | run: pip install requests 20 | 21 | - name: Run close_issue.py Script 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | run: python .github/close_issue.py 25 | -------------------------------------------------------------------------------- /.github/workflows/housekeeping-stale-issues.yaml: -------------------------------------------------------------------------------- 1 | name: Housekeeping - Close stale issues # 工作流程名称:处理陈旧问题 2 | on: 3 | schedule: 4 | - cron: '0 9 * * *' # 定时触发:每天早上 9 点运行 5 | 6 | jobs: 7 | stale: # 作业名称 8 | runs-on: ubuntu-latest # 在最新版本的 Ubuntu 环境中运行 9 | steps: 10 | - uses: actions/stale@v9.0.0 # 使用 GitHub 官方的 stale action,版本 9.0.0 11 | with: 12 | # 当问题被标记为陈旧时的提示消息 13 | stale-issue-message: 'This issue is being marked stale due to a period of inactivity...' 14 | # 当问题被关闭时的提示消息 15 | close-issue-message: 'This issue was closed because it has been stalled for 30 days...' 16 | # 问题在 60 天无活动后被标记为陈旧 17 | days-before-issue-stale: 60 18 | # 问题被标记为陈旧后,再过 30 天自动关闭 19 | days-before-issue-close: 30 20 | # 排除带有 "upcoming" 里程碑的问题 21 | exempt-milestones: "upcoming" 22 | # 每次运行处理的最大操作数量 23 | operations-per-run: 1000 24 | -------------------------------------------------------------------------------- /.github/workflows/issue-translator.yml_bak: -------------------------------------------------------------------------------- 1 | name: Issue Translator 2 | on: 3 | issue_comment: 4 | types: [created] 5 | issues: 6 | types: [opened] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: usthe/issues-translate-action@v2.7 13 | with: 14 | IS_MODIFY_TITLE: false 15 | CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # poetry 97 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 98 | # This is especially recommended for binary packages to ensure reproducibility, and is more 99 | # commonly ignored for libraries. 100 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 101 | #poetry.lock 102 | 103 | # pdm 104 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 105 | #pdm.lock 106 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 107 | # in version control. 108 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 109 | .pdm.toml 110 | .pdm-python 111 | .pdm-build/ 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | #.idea/ 162 | node_modules -------------------------------------------------------------------------------- /Ad/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |
5 | 自建Docker镜像加速服务,国外高性价比 VPS 主机推荐. 6 |

7 |
8 | 9 | ## 官网地址 10 | 11 | > 高性价比海外VPS: [点击跳转官网](https://my.racknerd.com/aff.php?aff=12151) 12 | 13 | ## 促销优惠VPS 14 | 15 | #### 2024 端午节 VPS 促销优惠活动 16 | - 多个美国机房可选。 17 | 18 | | 内存 | CPU | 硬盘(SSD) | 流量 | 带宽 | 价格(续费同价) | 购买链接 | 19 | |------|-----|-----------|------|------|----------------|----------| 20 | | 1.25G | 1核 | 20G | 2T/月 | 1Gbps | $12.88/年 | [点击购买](https://my.racknerd.com/aff.php?aff=12151&pid=850) | 21 | | 2G | 2核 | 30G | 4T/月 | 1Gbps | $19.88/年 | [点击购买](https://my.racknerd.com/aff.php?aff=12151&pid=851) | 22 | | 3G | 2核 | 45G | 5T/月 | 1Gbps | $26.88/年 | [点击购买](https://my.racknerd.com/aff.php?aff=12151&pid=852) | 23 | 24 | #### 2024 中国春节 促销优惠活动 25 | 26 | | 内存 | CPU | 硬盘(SSD) | 流量 | 带宽 | 价格(续费同价) | 购买链接 | 27 | |------|-----|-----------|------|------|----------------|----------| 28 | | 1G | 1核 | 15G | 2T/月 | 1Gbps | $10.99/年 | [点击购买](https://my.racknerd.com/aff.php?aff=12151&pid=838) | 29 | | 1.5G | 1核 | 25G | 4T/月 | 1Gbps | $16.88/年 | [点击购买](https://my.racknerd.com/aff.php?aff=12151&pid=839) | 30 | | 2.5G | 2核 | 38G | 6T/月 | 1Gbps | $23.88/年 | [点击购买](https://my.racknerd.com/aff.php?aff=12151&pid=840) | 31 | 32 | #### 2024 新年 VPS 促销优惠活动 33 | - 有多个美国机房可选,2G 及以上配置以上可选洛杉矶 DC-02 机房 (可申请 IPv6 地址)。 34 | 35 | | 内存 | CPU | 硬盘(SSD) | 流量 | 带宽 | 价格(续费同价) | 购买链接 | 36 | |------|-----|-----------|------|------|----------------|----------| 37 | | 1G | 1核 | 21G | 1.5T/月 | 1Gbps | $11.49/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=826) | 38 | | 2G | 1核 | 35G | 2.5T/月 | 1Gbps | $17.38/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=827) | 39 | | 3G | 2核 | 45G | 5T/月 | 1Gbps | $27.98/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=828) | 40 | | 4G | 2核 | 60G | 8T/月 | 1Gbps | $37.38/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=829) | 41 | 42 | #### 2023 黑色星期五套餐 43 | 44 | | 内存 | CPU | 硬盘(SSD) | 流量 | 带宽 | 价格(续费同价) | 购买链接 | 45 | |------|-----|-----------|------|------|----------------|----------| 46 | | 768MB | 1核 | 15G | 1.5T/月 | 1Gbps | $10.18/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=792) | 47 | | 2G | 1核 | 30G | 2.5T/月 | 1Gbps | $16.98/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=793) | 48 | | 2.5G | 2核 | 50G | 5T/月 | 1Gbps | $25.49/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=794) | 49 | | 4G | 2核 | 80G | 8T/月 | 1Gbps | $38.88/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=795) | 50 | 51 | #### 2023 新年 促销优惠活动 52 | 53 | | 内存 | CPU | 硬盘(SSD) | 流量 | 带宽 | 价格(续费同价) | 购买链接 | 54 | |------|-----|-----------|------|------|----------------|----------| 55 | | 512MB | 1核 | 10G | 1.5T/月 | 1Gbps | $10.18/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=734) | 56 | | 1G | 1核 | 25G | 4T/月 | 1Gbps | $12.98/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=735) | 57 | | 2.5G | 2核 | 45G | 6T/月 | 1Gbps | $24.88/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=736) | 58 | 59 | #### 2022 黑色星期五套餐 60 | 61 | | 内存 | CPU | 硬盘(SSD) | 流量 | 带宽 | 价格(续费同价) | 购买链接 | 62 | |------|-----|-----------|------|------|----------------|----------| 63 | | 768MB | 1核 | 10G | 1T/月 | 1Gbps | $10.28/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=695) | 64 | | 1.5G | 1核 | 30G | 3T/月 | 1Gbps | $16.88/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=696) | 65 | | 2.5G | 2核 | 60G | 7T/月 | 1Gbps | $28.55/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=697) | 66 | | 3.5G | 2核 | 80G | 10T/月 | 1Gbps | $38.88/年 | [点此购买](https://my.racknerd.com/aff.php?aff=12151&pid=698) | 67 | 68 | 69 | ## 注意事项 70 | - 建议购买至少 1GB 内存配置的 VPS ,以获得更好的使用体验。 71 | - 如果分到了被墙的 IP 可以在购买后 `72 小时`内免费更换 (点击`Change IP`按钮自助更换),之后更换一次 IP 需支付 `3` 美元。 72 | - 洛杉矶(Los Angeles) DC-02 机房可免费提供多个 IPv6 地址,可以发工单询问与获取。 73 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | # 设置工作目录 3 | WORKDIR /app 4 | # 复制项目文件到工作目录 5 | COPY hubcmdui/ . 6 | # 安装项目依赖 7 | RUN npm install 8 | # 暴露应用程序的端口 9 | EXPOSE 3000 10 | # 运行应用程序 11 | CMD ["node", "server.js"] -------------------------------------------------------------------------------- /Issue/issue.en.md: -------------------------------------------------------------------------------- 1 |

2 | 中文 | English 3 |

4 |

5 | 6 |
7 | Deployment and Usage Issues Summary for Docker Proxy Service. 8 |

9 | 10 | 11 | --- 12 | 13 | [Docker Proxy-Communication Group](https://t.me/+ghs_XDp1vwxkMGU9) 14 | 15 | --- 16 | 17 | ## 👨🏻‍💻 Problem Summary 18 | 19 | #### 1. Unable to delete a specific image tag through the UI. 20 | **Known Issue:** Deletion is not supported when using `registry` as a proxy cache. 21 | 22 | **Related Issues:** [#3853](https://github.com/distribution/distribution/issues/3853) 23 | 24 | #### 2. The pull speed from within China is not ideal. 25 | **Known Issue:** The network route from your foreign server to China is suboptimal. 26 | 27 | **Solutions:** 28 | - (1) Enable BBR on the server to optimize network performance (with limited effect). 29 | - (2) Switch to a server that has better network optimization for routes to China. 30 | 31 | #### 3. How long does the registry image cache last, and how to adjust it? 32 | **Known Issue:** The default cache time is 168 hours, which is 7 days. Adjust the cache time by modifying the ttl in the proxy configuration section of the configuration file 33 | 34 | #### 4. Regarding the solution for pulling images from the 'Hub' public namespace with or without adding the 'library' prefix when using a mirror registry for acceleration. 35 | 36 | - This scheme was provided by a senior member in the communication group and has been implemented and tested through Nginx. 37 | ```shell 38 | http { 39 | map $http_upgrade $connection_upgrade { 40 | default upgrade; 41 | '' close; 42 | } 43 | 44 | server { 45 | listen 80; 46 | server_name hub.your_domain.com; 47 | 48 | # 在docker hub的配置中添加下面的location规则 49 | location ~ ^/v2/([^/]+)/(manifests|blobs)/(.*)$ { 50 | rewrite ^/v2/(.*)$ /v2/library/$1 break; 51 | proxy_pass http://127.0.0.1:51000; 52 | proxy_set_header Host $host; 53 | proxy_set_header X-Real-IP $remote_addr; 54 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 55 | proxy_set_header REMOTE-HOST $remote_addr; 56 | proxy_set_header Upgrade $http_upgrade; 57 | proxy_set_header Connection $connection_upgrade; 58 | proxy_http_version 1.1; 59 | add_header X-Cache $upstream_cache_status; 60 | } 61 | 62 | location / { 63 | proxy_pass http://127.0.0.1:51000; 64 | proxy_set_header Host $host; 65 | proxy_set_header X-Real-IP $remote_addr; 66 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 67 | proxy_set_header REMOTE-HOST $remote_addr; 68 | proxy_set_header Upgrade $http_upgrade; 69 | proxy_set_header Connection $connection_upgrade; 70 | proxy_http_version 1.1; 71 | add_header X-Cache $upstream_cache_status; 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | #### 5. An error occurs when pulling an image `tls: failed to verify certificate: x509: certificate signed by unknown authority` 78 | 79 | **Known Issue:** Certificate issue. Indicates that the certificate was issued by an unknown or untrusted certificate Authority (CA). 80 | -------------------------------------------------------------------------------- /Issue/issue.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |
5 | 部署Docker Proxy服务和后期使用相关等问题总结. 6 |

7 | 8 | 9 | --- 10 | 11 | [Docker Proxy-交流群](https://t.me/+ghs_XDp1vwxkMGU9) 12 | 13 | --- 14 | 15 | ## 👨🏻‍💻 问题总结 16 | 17 | #### 1、无法通过UI界面删除某一镜像的TAG 18 | - [x] **已知问题:** 使用`registry`作为代理缓存时不支持删除 19 | 20 | - [x] **issues:** [#3853](https://github.com/distribution/distribution/issues/3853) 21 | 22 | - [x] **代码片段:** [configure as a pull through cache](https://github.com/distribution/distribution/blob/main/registry/handlers/app.go#L349) 23 | 24 | #### 2、搭建好了,但是国内拉取速度不理想 25 | - [x] **已知问题:** 你的国外服务器到国内的网络线路不理想 26 | 27 | - [x] **解决方案:** 28 | - (1) 开启服务器BBR,优化网络性能(效果有限) 29 | - (2) 更换对国内网络线路优化更好的服务器 30 | 31 | #### 3、registry 镜像缓存多少时间,如何调整,如何禁用 32 | - [x] **已知问题:** 默认缓存`168h`,也就是`7天`。修改配置文件中`proxy`配置块中的`ttl` 参数调整缓存时间,`0` 禁用缓存过期。默认单位ns 33 | 34 | - - 要是调度程序正常清理旧数据,需要配置中将 `delete` 开启(本项目默认已开启) 35 | 36 | #### 4、使用镜像加速拉取`hub`公共空间下的镜像时兼容不添加`library`的情况 37 | 38 | - 此方案来自交流群里大佬提供,通过nginx实现并实测 39 | ```shell 40 | # 将下面的$http_upgrade $connection_upgrade变量添加到http块中 41 | http { 42 | # 用于支持WebSocket 43 | map $http_upgrade $connection_upgrade { 44 | default upgrade; 45 | '' close; 46 | } 47 | 48 | server { 49 | listen 80; 50 | server_name hub.your_domain.com; 51 | 52 | # 在docker hub的配置中添加下面的location规则 53 | location ~ ^/v2/([^/]+)/(manifests|blobs)/(.*)$ { 54 | rewrite ^/v2/(.*)$ /v2/library/$1 break; 55 | proxy_pass http://127.0.0.1:51000; 56 | proxy_set_header Host $host; 57 | proxy_set_header X-Real-IP $remote_addr; 58 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 59 | proxy_set_header REMOTE-HOST $remote_addr; 60 | proxy_set_header Upgrade $http_upgrade; 61 | proxy_set_header Connection $connection_upgrade; 62 | proxy_http_version 1.1; 63 | add_header X-Cache $upstream_cache_status; 64 | } 65 | 66 | location / { 67 | proxy_pass http://127.0.0.1:51000; 68 | proxy_set_header Host $host; 69 | proxy_set_header X-Real-IP $remote_addr; 70 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 71 | proxy_set_header REMOTE-HOST $remote_addr; 72 | proxy_set_header Upgrade $http_upgrade; 73 | proxy_set_header Connection $connection_upgrade; 74 | proxy_http_version 1.1; 75 | add_header X-Cache $upstream_cache_status; 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | #### 5、拉取镜像报错 `tls: failed to verify certificate: x509: certificate signed by unknown authority` 82 | - [x] **已知问题:** 证书问题。表示证书是由一个未知的或不受信任的证书颁发机构(CA)签发的。 83 | - [x] **解决方案:** 修改Docker配置 `daemon.json` 添加 `insecure-registries` 并填入您的加速域名(非URL) 84 | 85 | #### 6、通过docker-compose部署,如何设置Proxy认证 86 | - [x] **已知问题:** 查看教程:[自建Docker镜像加速服务](https://www.dqzboy.com/8709.html) 87 | 88 | #### 7、对于服务器的规格要求,内存、CPU、磁盘、带宽网络等 89 | - [x] **已知问题:** 建议最低使用1C1G的服务器,磁盘大小取决于你拉取镜像的频率以及保存镜像缓存的时长决定(默认缓存7天,部署时可自定义);如果对拉取速度有要求,最好选择针对中国大陆进行网络优化的服务器 90 | 91 | #### 8、缓存在本地磁盘上的数据是否会自动清理?如何判断缓存是否被清理? 92 | - [x] **已知问题:** Registry会定期删除旧内容以节省磁盘空间。下面为官方原文解释: 93 | > In environments with high churn rates, stale data can build up in the cache. When running as a pull through cache the Registry periodically removes old content to save disk space. Subsequent requests for removed content causes a remote fetch and local re-caching. 94 | > To ensure best performance and guarantee correctness the Registry cache should be configured to use the `filesystem` driver for storage. 95 | 96 | - [x] **已知问题:** 当我们在配置文件开启了 `delete.enabled.true` 那么调度程序会自动清理过期的镜像层或未使用的镜像标签 97 | 98 | - ⚠️ 注意:不要通过在UI上查看某个镜像是否被删除来判断调度程序是否已自动执行删除操作。而是查看对应代理服务的容器日志 99 | 100 | - [x] **解决方案:** 目前 `registry` 的清理效果对于磁盘小的机器并不理想,如果并不希望镜像文件长时间缓存在本地磁盘,建议通过脚本结合系统`crontab`实现定时清理 101 | 102 | #### 9、拉取了一个镜像之后,发现UI界面没有显示? 103 | - [x] **已知问题:** Docker-Proxy部署的服务器上的存储空间不足。宿主机默认的挂载目录 `./registry/data` 104 | 105 | #### 10、开启认证后,配置`daemon.json`指定了代理地址,可以正常登入,但是`docker pull`镜像时无法拉取镜像 106 | - [x] **已知问题:** 因为对于私有镜像仓库,docker客户端对镜像的相关操作,比如pull push支持不友好(历史遗留问题)相关 [issues](https://github.com/docker/cli/issues/3793#issuecomment-1269051403) 107 | 108 | - [x] **解决方案:** 109 | - - (1)首先通过docker login <私有镜像地址> 登入私有镜像仓库,登入成功之后,会在对应的用户家目录下生成 `.docker/config.json` 配置文件 110 | 111 | - - (2)通过`vi`命令打开配置文件,然后手动在`auths`配置块里面添加官方地址`https://index.docker.io/v1/`,`auth`哈希值与你的私有镜像地址的auth保持一致,然后重启docker即可直接通过`docker pull`拉取了 112 | 113 | ```bash 114 | vi $HOME/.docker/config.json 115 | { 116 | "auths": { 117 | "https://index.docker.io/v1/": { 118 | "auth": "复制下面私有镜像登入认证的哈希值填到这里" 119 | }, 120 | "你的私有镜像地址": { 121 | "auth": "自动生成的认证哈希值" 122 | } 123 | } 124 | } 125 | 126 | # 重启 docker 127 | systemctl restart docker 128 | 129 | # 拉取镜像 130 | docker pull nginx 131 | ``` 132 | 133 | #### 11、如何配置才能让数据不保留到磁盘中? 134 | - [x] **已知问题:** 默认使用的存储系统为`filesystem` 使用本地磁盘存储注册表文件 135 | - [x] **解决方案:** 修改对应Registry的配置,将`Storage driver` 存储驱动改为 `inmemory`,⚠️ 注意:此存储驱动程序不会在运行期间保留数据。这就是为什么它只适合测试。切勿在生产中使用此驱动程序。 136 | 137 | 138 | #### 12、关于Docker Hub免费拉取政策再次变更后的解决方案? 139 | - [x] **已知问题:** Docker Hub从 2020年11月2日起就已经限制非付费用户的拉取频率了,只是这次又变更了拉取政策。匿名用户每小时10次,登入用户每小时100次 140 | - [x] **解决方案:** 修改项目中Docker Hub对应的配置文件`registry-hub.yml` 添加Docker Hub用户,添加后重新启动Docker Registry容器即可! 141 | ``` 142 | ... 143 | 144 | # username 输入docker hub账号,password 输入对应账号密码 145 | proxy: 146 | remoteurl: https://registry-1.docker.io 147 | username: 148 | password: 149 | ttl: 168h 150 | ``` 151 | -------------------------------------------------------------------------------- /caddy/Caddyfile: -------------------------------------------------------------------------------- 1 | ui.your_domain_name { 2 | reverse_proxy localhost:50000 { 3 | header_up Host {host} 4 | header_up Origin {scheme}://{host} 5 | header_up X-Forwarded-For {remote_addr} 6 | header_up X-Forwarded-Proto {scheme} 7 | header_up X-Forwarded-Ssl on 8 | header_up X-Forwarded-Port {server_port} 9 | header_up X-Forwarded-Host {host} 10 | } 11 | } 12 | 13 | hub.your_domain_name { 14 | reverse_proxy localhost:51000 { 15 | header_up Host {host} 16 | header_up X-Real-IP {remote_addr} 17 | header_up X-Forwarded-For {remote_addr} 18 | header_up X-Nginx-Proxy true 19 | } 20 | } 21 | 22 | ghcr.your_domain_name { 23 | reverse_proxy localhost:52000 { 24 | header_up Host {host} 25 | header_up X-Real-IP {remote_addr} 26 | header_up X-Forwarded-For {remote_addr} 27 | header_up X-Nginx-Proxy true 28 | } 29 | } 30 | 31 | gcr.your_domain_name { 32 | reverse_proxy localhost:53000 { 33 | header_up Host {host} 34 | header_up X-Real-IP {remote_addr} 35 | header_up X-Forwarded-For {remote_addr} 36 | header_up X-Nginx-Proxy true 37 | } 38 | } 39 | 40 | k8s-gcr.your_domain_name { 41 | reverse_proxy localhost:54000 { 42 | header_up Host {host} 43 | header_up X-Real-IP {remote_addr} 44 | header_up X-Forwarded-For {remote_addr} 45 | header_up X-Nginx-Proxy true 46 | } 47 | } 48 | 49 | k8s.your_domain_name { 50 | reverse_proxy localhost:55000 { 51 | header_up Host {host} 52 | header_up X-Real-IP {remote_addr} 53 | header_up X-Forwarded-For {remote_addr} 54 | header_up X-Nginx-Proxy true 55 | } 56 | } 57 | 58 | quay.your_domain_name { 59 | reverse_proxy localhost:56000 { 60 | header_up Host {host} 61 | header_up X-Real-IP {remote_addr} 62 | header_up X-Forwarded-For {remote_addr} 63 | header_up X-Nginx-Proxy true 64 | } 65 | } 66 | 67 | mcr.your_domain_name { 68 | reverse_proxy localhost:57000 { 69 | header_up Host {host} 70 | header_up X-Real-IP {remote_addr} 71 | header_up X-Forwarded-For {remote_addr} 72 | header_up X-Nginx-Proxy true 73 | } 74 | } 75 | 76 | elastic.your_domain_name { 77 | reverse_proxy localhost:58000 { 78 | header_up Host {host} 79 | header_up X-Real-IP {remote_addr} 80 | header_up X-Forwarded-For {remote_addr} 81 | header_up X-Nginx-Proxy true 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cloud/ClawCloud/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |
5 | 使用 Claw Cloud 免费容器服务快速部署我们的Docker镜像加速服务. 6 |

7 | 8 | 9 | --- 10 | 11 | [Docker Proxy-交流群](https://t.me/+ghs_XDp1vwxkMGU9) 12 | 13 | --- 14 | 15 | 16 | ## 📦 部署 17 | > 以下步骤需要有 Claw Cloud 账号,没有账号的需要先注册 18 | 19 | - **提醒**: 点击下面链接注册账号,推荐使用注册时长超过180天的GitHub账号注册,GitHub账号超过180天的用户注册,可解锁每月5$ 20 | 21 | 22 | **1. 注册账号 [点击此处,注册账号](https://console.run.claw.cloud/signin?link=PZNPEDMUAT4G)** 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | 39 |
40 | 41 | 42 | **2. 创建服务** 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 |
54 | 55 | **3. 选择以docker容器的方式部署,输入下面任一镜像地址** 56 | 57 | 58 | | 镜像 | 平台 | 59 | |-------|---------------| 60 | | dqzboy/mirror-hub:latest | docker hub 61 | | dqzboy/mirror-gcr:latest | Google Container Registry 62 | | dqzboy/mirror-ghcr:latest | GitHub Container Registry 63 | | dqzboy/mirror-k8sgcr:latest | Kubernetes Container Registry 64 | | dqzboy/mirror-k8sreg:latest | Kubernetes's container image registry 65 | | dqzboy/mirror-quay:latest | Quay Container Registry 66 | | dqzboy/mirror-elastic:latest | Microsoft Container Registry 67 | | dqzboy/mirror-mcr:latest | Elastic Stack 68 | 69 | 70 | 71 | 72 | 73 |
74 | 75 | 76 | 77 | 78 | 79 |
80 | 81 | 82 | 83 | 84 | 85 | 86 |
87 | 88 | 89 | **4. 服务运行完成之后,等待一些时间后,使用外网域名进行访问,显示空白页面即表示正常** 90 | 91 | 92 | 93 | 94 |
95 | 96 | ## ✨ 如何使用 97 | 98 | > 下面是以加速Docker Hub平台镜像下载举例 99 | 100 | **1. 改`Docker的daemon.json`配置,配置你 Claw Cloud 分配的外网地址。修改后重启docker** 101 | 102 | ```shell 103 | ~]# vim /etc/docker/daemon.json 104 | { 105 | "registry-mirrors": [ "https://your_ClawCloud_url" ], 106 | "log-opts": { 107 | "max-size": "100m", 108 | "max-file": "5" 109 | } 110 | } 111 | ``` 112 | **2. 使用Claw Cloud服务地址替换官方的 Registry 地址拉取镜像** 113 | ```shell 114 | # docker hub Registry 115 | ## 源:redis:latest 116 | ## 替换 117 | docker pull your_ClawCloud_url/library/redis:latest 118 | ``` 119 | 120 | > **说明**:如果上面配置了docker的`daemon.json`,那么拉取镜像的时候就不需要在镜像前面加`Claw Cloud`了。【只针对拉取Docker Hub上的镜像有效】 -------------------------------------------------------------------------------- /cloud/Koyeb/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |
5 | 使用 Koyeb 快速部署我们的Docker镜像加速服务. 6 |

7 | 8 | 9 | --- 10 | 11 | [Docker Proxy-交流群](https://t.me/+ghs_XDp1vwxkMGU9) 12 | 13 | --- 14 | 15 | 16 | ## 📦 部署 17 | > 以下步骤需要有Koyeb账号,没有账号的可以先注册 18 | 19 | **1. 登入 [Koyeb](https://app.koyeb.com/auth/signup/)** 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | **2. 创建我们的服务** 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | **3. 选择以docker容器的方式部署,输入下面任一镜像地址** 36 | 37 | | 镜像 | 平台 | 38 | |-------|---------------| 39 | | dqzboy/mirror-hub:latest | docker hub 40 | | dqzboy/mirror-gcr:latest | Google Container Registry 41 | | dqzboy/mirror-ghcr:latest | GitHub Container Registry 42 | | dqzboy/mirror-k8sgcr:latest | Kubernetes Container Registry 43 | | dqzboy/mirror-k8sreg:latest | Kubernetes's container image registry 44 | | dqzboy/mirror-quay:latest | Quay Container Registry 45 | | dqzboy/mirror-mcr:latest | Microsoft Container 46 | | dqzboy/mirror-elastic:latest | Elastic Stack 47 | 48 | 49 | 50 | 51 | 52 |
53 | 54 | 55 | 56 | 57 | 58 |
59 | 60 | 61 | **4. 实例类型选择免费即可** 62 | 63 | 64 | 65 | 66 | 67 |
68 | 69 | **5. 暴露端口改为5000,自定义服务名称,然后直接创建即可** 70 | 71 | 72 | 73 | 74 |
75 | 76 | **6. 等待服务运行完成之后,使用分配的外网域名即可愉快的使用了** 77 | 78 | 79 | 80 | 81 |
82 | 83 | 84 | 85 | 86 |
87 | 88 | 89 | ## ✨ 使用 90 | 91 | **1. 改Docker的daemon.json配置,配置你Koyeb服务地址。修改后重启docker** 92 | ```shell 93 | ~]# vim /etc/docker/daemon.json 94 | { 95 | "registry-mirrors": [ "https://your_koyeb_url" ], 96 | "log-opts": { 97 | "max-size": "100m", 98 | "max-file": "5" 99 | } 100 | } 101 | ``` 102 | **2. 使用Koyeb服务地址替换官方的 Registry 地址拉取镜像** 103 | ```shell 104 | # docker hub Registry 105 | ## 源:redis:latest 106 | ## 替换 107 | docker pull your_koyeb_url/library/redis:latest 108 | ``` 109 | 110 | > **说明**:如果上面配置了docker的`daemon.json`,那么拉取镜像的时候就不需要在镜像前面加`Koyeb_URL`了。【只针对拉取Docker Hub上的镜像有效】 111 | 112 | 113 | **3. 前缀替换的 Registry 的参考** 114 | 115 | | 源站 | 替换为 | 平台 | 116 | |-------|---------------|----------| 117 | | docker.io | your_render_url | docker hub 118 | | gcr.io | your_render_url | Google Container Registry 119 | | ghcr.io | your_render_url | GitHub Container Registry 120 | | k8s.gcr.io | your_render_url | Kubernetes Container Registry 121 | | quay.io | your_render_url | Quay Container Registry 122 | | mcr.microsoft.com | mcr.your_domain_name | Microsoft Container Registry 123 | | docker.elastic.co | elastic.your_domain_name | Elastic Stack 124 | 125 | **4. 说明:** 测试发现Koyeb所解析的IP为cloudfare的,国内部分地区运营商对cloudfare进行了阻断,所以这些地区则无法正常访问! 126 | 127 | --- 128 | 129 | ## ✨ 将镜像上传到自己的Docker Hub仓库 130 | 131 | #### 步骤 1: 登录到 Docker Hub 132 | - 打开终端输入以下命令并按提示输入你的 Docker Hub 用户名和密码: 133 | 134 | ```shell 135 | docker login 136 | ``` 137 | 138 | #### 步骤 2: 拉取镜像 139 | - 使用 docker pull 命令拉取上面的镜像,这里以 dqzboy/mirror-hub:latest 举例: 140 | 141 | ```shell 142 | docker pull dqzboy/mirror-hub:latest 143 | ``` 144 | 145 | #### 步骤 3: 标记镜像 146 | - 给拉下来的镜像打一个新标签,使其指向你的 Docker Hub 用户名。 147 | - 假设你的 Docker Hub 用户名是 yourusername,你可以使用以下命令: 148 | 149 | ```shell 150 | docker tag dqzboy/mirror-hub:latest yourusername/mirror-hub:latest 151 | ``` 152 | 153 | #### 步骤 4: 上传镜像 154 | - 使用 docker push 命令上传标记的镜像到你的 Docker Hub 仓库: 155 | 156 | ```shell 157 | docker push yourusername/mirror-hub:latest 158 | ``` 159 | 160 | #### 步骤 5: 验证上传 161 | - 上传完成后,你可以登录到 Docker Hub 网站,查看你的仓库中是否已经存在刚刚上传的镜像。 162 | -------------------------------------------------------------------------------- /cloud/Render/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |
5 | 使用 Render 快速部署我们的Docker镜像加速服务. 6 |

7 | 8 | 9 | --- 10 | 11 | [Docker Proxy-交流群](https://t.me/+ghs_XDp1vwxkMGU9) 12 | 13 | --- 14 | 15 | 16 | ## 📦 部署 17 | > 以下步骤需要有Render账号,没有账号的可以先注册 18 | 19 | **1. 登入 [Render](https://dashboard.render.com)** 20 | 21 | **2. 创建我们的服务** 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | **3. 选择以docker容器的方式部署,输入下面任一镜像地址** 35 | 36 | > **⚠️ 特别说明:当前作者Docker Hub仓库账号已被Render特殊对待了,建议大家把下面的镜像下载到自己本地,然后上传到自己的Docker hub仓库。下面的镜像地址也会随时被Render限制使用[具体操作可以看此教程](#-将镜像上传到自己的docker-hub仓库)** 37 | 38 | | 镜像 | 平台 | 39 | |-------|---------------| 40 | | mirhub/mirror-hub:latest | docker hub 41 | | mirhub/mirror-gcr:latest | Google Container Registry 42 | | mirhub/mirror-ghcr:latest | GitHub Container Registry 43 | | mirhub/mirror-k8sgcr:latest | Kubernetes Container Registry 44 | | mirhub/mirror-k8sreg:latest | Kubernetes's container image registry 45 | | mirhub/mirror-quay:latest | Quay Container Registry 46 | | mirhub/mirror-elastic:latest | Microsoft Container Registry 47 | | mirhub/mirror-mcr:latest | Elastic Stack 48 | 49 | 50 | 51 | 52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 |
60 | 61 | **4. 实例类型选择免费即可(免费实例需要保活,可使用 [uptime-kuma](https://uptime.kuma.pet/) 或 [D监控](https://www.dnspod.cn/Products/Monitor) 实现)** 62 | 63 | 64 | 65 | 66 | 67 |
68 | 69 | **5. 环境变量不用添加,直接选择创建即可** 70 | 71 | 72 | 73 | 74 |
75 | 76 | **6. 等待服务运行完成之后,使用分配的外网域名即可愉快的使用了** 77 | 78 | 79 | 80 | 81 |
82 | 83 | ## ✨ 使用 84 | 85 | **1. 改Docker的daemon.json配置,配置你Render服务地址。修改后重启docker** 86 | ```shell 87 | ~]# vim /etc/docker/daemon.json 88 | { 89 | "registry-mirrors": [ "https://your_render_url" ], 90 | "log-opts": { 91 | "max-size": "100m", 92 | "max-file": "5" 93 | } 94 | } 95 | ``` 96 | **2. 使用Render服务地址替换官方的 Registry 地址拉取镜像** 97 | ```shell 98 | # docker hub Registry 99 | ## 源:redis:latest 100 | ## 替换 101 | docker pull your_render_url/library/redis:latest 102 | ``` 103 | 104 | > **说明**:如果上面配置了docker的`daemon.json`,那么拉取镜像的时候就不需要在镜像前面加`Render_URL`了。【只针对拉取Docker Hub上的镜像有效】 105 | 106 | **3. 拉取速度测试,效果还是可以的,主要是免费** 107 | ![image](https://github.com/dqzboy/Blog-Image/assets/42825450/06ad14d4-cb0f-4924-ab41-5c3f001261a2) 108 | 109 | **4. 前缀替换的 Registry 的参考** 110 | 111 | | 源站 | 替换为 | 平台 | 112 | |-------|---------------|----------| 113 | | docker.io | your_render_url | docker hub 114 | | gcr.io | your_render_url | Google Container Registry 115 | | ghcr.io | your_render_url | GitHub Container Registry 116 | | k8s.gcr.io | your_render_url | Kubernetes Container Registry 117 | | quay.io | your_render_url | Quay Container Registry 118 | | mcr.microsoft.com | mcr.your_domain_name | Microsoft Container Registry 119 | | docker.elastic.co | elastic.your_domain_name | Elastic Stack 120 | 121 | 122 | --- 123 | 124 | ## ✨ 将镜像上传到自己的Docker Hub仓库 125 | 126 | #### 镜像下载地址 127 | | 镜像 | 平台 | 128 | |-------|---------------| 129 | | dqzboy/mirror-hub:latest | docker hub 130 | | dqzboy/mirror-gcr:latest | Google Container Registry 131 | | dqzboy/mirror-ghcr:latest | GitHub Container Registry 132 | | dqzboy/mirror-k8sgcr:latest | Kubernetes Container Registry 133 | | dqzboy/mirror-k8sreg:latest | Kubernetes's container image registry 134 | | dqzboy/mirror-quay:latest | Quay Container Registry 135 | | dqzboy/mirror-mcr:latest | Microsoft Container 136 | | dqzboy/mirror-elastic:latest | Elastic Stack 137 | 138 | #### 步骤 1: 登录到 Docker Hub 139 | - 打开终端输入以下命令并按提示输入你的 Docker Hub 用户名和密码: 140 | 141 | ```shell 142 | docker login 143 | ``` 144 | 145 | #### 步骤 2: 拉取镜像 146 | - 使用 docker pull 命令拉取上面的镜像,这里以 dqzboy/mirror-hub:latest 举例: 147 | 148 | ```shell 149 | docker pull dqzboy/mirror-hub:latest 150 | ``` 151 | 152 | #### 步骤 3: 标记镜像 153 | - 给拉下来的镜像打一个新标签,使其指向你的 Docker Hub 用户名。 154 | - 假设你的 Docker Hub 用户名是 yourusername,你可以使用以下命令: 155 | 156 | ```shell 157 | docker tag dqzboy/mirror-hub:latest yourusername/mirror-hub:latest 158 | ``` 159 | 160 | #### 步骤 4: 上传镜像 161 | - 使用 docker push 命令上传标记的镜像到你的 Docker Hub 仓库: 162 | 163 | ```shell 164 | docker push yourusername/mirror-hub:latest 165 | ``` 166 | 167 | #### 步骤 5: 验证上传 168 | - 上传完成后,你可以登录到 Docker Hub 网站,查看你的仓库中是否已经存在刚刚上传的镜像。 169 | 170 | --- 171 | 172 | ## ⚠️ 注意 173 | **1.** 免费实例如果15分钟内未收到入站流量,Render会关闭实例的网络服务。Render 会在下次收到处理请求时重新启动该服务。 174 | 175 | **2.** Render每月为每个用户和团队提供 750 小时的免费实例时间: 176 | - 免费网络服务在运行期间会消耗这些时间(停止服务不会消耗免费实例小时数) 177 | - 如果您在某个月内用完了所有免费实例小时数,Render将暂停您的所有免费网络服务,直到下个月开始 178 | - 每个月开始时,您的免费实例小时数将重置为 750 小时(剩余小时数不会结转) 179 | 180 | **3.** 最好自己个人使用或者小团队使用,如果你的服务使用人多了,Render照样会把你的服务给删除掉,并且没有任何提醒或通知! -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |
5 | Docker、K8s、Quay、Ghcr镜像加速服务配置文件. 6 |

7 | -------------------------------------------------------------------------------- /config/registry-elastic.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | log: 3 | accesslog: 4 | disabled: true 5 | level: info 6 | formatter: text 7 | fields: 8 | service: registry 9 | environment: staging 10 | storage: 11 | cache: 12 | # 改为 blan 禁用 blob 描述符缓存,可解决docker pull error:unexpected EOF 13 | blobdescriptor: inmemory 14 | filesystem: 15 | rootdirectory: /var/lib/registry 16 | #inmemory: # 此存储驱动程序不会在运行期间保留任何数据,适合磁盘空间小的机器使用(但是会使用内存开销,只适合测试) 17 | maintenance: 18 | uploadpurging: 19 | enabled: false 20 | tag: 21 | concurrencylimit: 8 22 | delete: 23 | enabled: true 24 | 25 | http: 26 | addr: :5000 27 | headers: 28 | X-Content-Type-Options: [nosniff] 29 | Access-Control-Allow-Origin: ['*'] 30 | Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE'] 31 | Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control'] 32 | Access-Control-Max-Age: [1728000] 33 | Access-Control-Allow-Credentials: [true] 34 | Access-Control-Expose-Headers: ['Docker-Content-Digest'] 35 | 36 | health: 37 | storagedriver: 38 | enabled: true 39 | interval: 10s 40 | threshold: 3 41 | 42 | proxy: 43 | remoteurl: https://docker.elastic.co 44 | username: 45 | password: 46 | ttl: 168h -------------------------------------------------------------------------------- /config/registry-gcr.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | log: 3 | accesslog: 4 | disabled: true 5 | level: info 6 | formatter: text 7 | fields: 8 | service: registry 9 | environment: staging 10 | storage: 11 | cache: 12 | # 改为 blan 禁用 blob 描述符缓存,可解决docker pull error:unexpected EOF 13 | blobdescriptor: inmemory 14 | filesystem: 15 | rootdirectory: /var/lib/registry 16 | #inmemory: # 此存储驱动程序不会在运行期间保留任何数据,适合磁盘空间小的机器使用(但是会使用内存开销,只适合测试) 17 | maintenance: 18 | uploadpurging: 19 | enabled: false 20 | tag: 21 | concurrencylimit: 8 22 | delete: 23 | enabled: true 24 | 25 | http: 26 | addr: :5000 27 | headers: 28 | X-Content-Type-Options: [nosniff] 29 | Access-Control-Allow-Origin: ['*'] 30 | Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE'] 31 | Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control'] 32 | Access-Control-Max-Age: [1728000] 33 | Access-Control-Allow-Credentials: [true] 34 | Access-Control-Expose-Headers: ['Docker-Content-Digest'] 35 | 36 | health: 37 | storagedriver: 38 | enabled: true 39 | interval: 10s 40 | threshold: 3 41 | 42 | proxy: 43 | remoteurl: https://gcr.io 44 | username: 45 | password: 46 | ttl: 168h -------------------------------------------------------------------------------- /config/registry-ghcr.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | log: 3 | accesslog: 4 | disabled: true 5 | level: info 6 | formatter: text 7 | fields: 8 | service: registry 9 | environment: staging 10 | storage: 11 | cache: 12 | # 改为 blan 禁用 blob 描述符缓存,可解决docker pull error:unexpected EOF 13 | blobdescriptor: inmemory 14 | filesystem: 15 | rootdirectory: /var/lib/registry 16 | #inmemory: # 此存储驱动程序不会在运行期间保留任何数据,适合磁盘空间小的机器使用(但是会使用内存开销,只适合测试) 17 | maintenance: 18 | uploadpurging: 19 | enabled: false 20 | tag: 21 | concurrencylimit: 8 22 | delete: 23 | enabled: true 24 | 25 | http: 26 | addr: :5000 27 | headers: 28 | X-Content-Type-Options: [nosniff] 29 | Access-Control-Allow-Origin: ['*'] 30 | Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE'] 31 | Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control'] 32 | Access-Control-Max-Age: [1728000] 33 | Access-Control-Allow-Credentials: [true] 34 | Access-Control-Expose-Headers: ['Docker-Content-Digest'] 35 | 36 | health: 37 | storagedriver: 38 | enabled: true 39 | interval: 10s 40 | threshold: 3 41 | 42 | proxy: 43 | remoteurl: https://ghcr.io 44 | username: 45 | password: 46 | ttl: 168h -------------------------------------------------------------------------------- /config/registry-hub.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | log: 3 | accesslog: 4 | disabled: true 5 | level: info 6 | formatter: text 7 | fields: 8 | service: registry 9 | environment: staging 10 | storage: 11 | cache: 12 | # 改为 blan 禁用 blob 描述符缓存,可解决docker pull error:unexpected EOF 13 | blobdescriptor: inmemory 14 | filesystem: 15 | rootdirectory: /var/lib/registry 16 | #inmemory: # 此存储驱动程序不会在运行期间保留任何数据,适合磁盘空间小的机器使用(但是会使用内存开销,只适合测试) 17 | maintenance: 18 | uploadpurging: 19 | enabled: false 20 | tag: 21 | concurrencylimit: 8 22 | delete: 23 | enabled: true 24 | 25 | http: 26 | addr: :5000 27 | headers: 28 | X-Content-Type-Options: [nosniff] 29 | Access-Control-Allow-Origin: ['*'] 30 | Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE'] 31 | Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control'] 32 | Access-Control-Max-Age: [1728000] 33 | Access-Control-Allow-Credentials: [true] 34 | Access-Control-Expose-Headers: ['Docker-Content-Digest'] 35 | 36 | health: 37 | storagedriver: 38 | enabled: true 39 | interval: 10s 40 | threshold: 3 41 | 42 | proxy: 43 | remoteurl: https://registry-1.docker.io 44 | username: 45 | password: 46 | ttl: 168h -------------------------------------------------------------------------------- /config/registry-k8s.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | log: 3 | accesslog: 4 | disabled: true 5 | level: info 6 | formatter: text 7 | fields: 8 | service: registry 9 | environment: staging 10 | storage: 11 | cache: 12 | # 改为 blan 禁用 blob 描述符缓存,可解决docker pull error:unexpected EOF 13 | blobdescriptor: inmemory 14 | filesystem: 15 | rootdirectory: /var/lib/registry 16 | #inmemory: # 此存储驱动程序不会在运行期间保留任何数据,适合磁盘空间小的机器使用(但是会使用内存开销,只适合测试) 17 | maintenance: 18 | uploadpurging: 19 | enabled: false 20 | tag: 21 | concurrencylimit: 8 22 | delete: 23 | enabled: true 24 | 25 | http: 26 | addr: :5000 27 | headers: 28 | X-Content-Type-Options: [nosniff] 29 | Access-Control-Allow-Origin: ['*'] 30 | Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE'] 31 | Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control'] 32 | Access-Control-Max-Age: [1728000] 33 | Access-Control-Allow-Credentials: [true] 34 | Access-Control-Expose-Headers: ['Docker-Content-Digest'] 35 | 36 | health: 37 | storagedriver: 38 | enabled: true 39 | interval: 10s 40 | threshold: 3 41 | 42 | proxy: 43 | remoteurl: https://registry.k8s.io 44 | username: 45 | password: 46 | ttl: 168h -------------------------------------------------------------------------------- /config/registry-k8sgcr.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | log: 3 | accesslog: 4 | disabled: true 5 | level: info 6 | formatter: text 7 | fields: 8 | service: registry 9 | environment: staging 10 | storage: 11 | cache: 12 | # 改为 blan 禁用 blob 描述符缓存,可解决docker pull error:unexpected EOF 13 | blobdescriptor: inmemory 14 | filesystem: 15 | rootdirectory: /var/lib/registry 16 | #inmemory: # 此存储驱动程序不会在运行期间保留任何数据,适合磁盘空间小的机器使用(但是会使用内存开销,只适合测试) 17 | maintenance: 18 | uploadpurging: 19 | enabled: false 20 | tag: 21 | concurrencylimit: 8 22 | delete: 23 | enabled: true 24 | 25 | http: 26 | addr: :5000 27 | headers: 28 | X-Content-Type-Options: [nosniff] 29 | Access-Control-Allow-Origin: ['*'] 30 | Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE'] 31 | Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control'] 32 | Access-Control-Max-Age: [1728000] 33 | Access-Control-Allow-Credentials: [true] 34 | Access-Control-Expose-Headers: ['Docker-Content-Digest'] 35 | 36 | health: 37 | storagedriver: 38 | enabled: true 39 | interval: 10s 40 | threshold: 3 41 | 42 | proxy: 43 | remoteurl: https://k8s.gcr.io 44 | username: 45 | password: 46 | ttl: 168h -------------------------------------------------------------------------------- /config/registry-mcr.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | log: 3 | accesslog: 4 | disabled: true 5 | level: info 6 | formatter: text 7 | fields: 8 | service: registry 9 | environment: staging 10 | storage: 11 | cache: 12 | # 改为 blan 禁用 blob 描述符缓存,可解决docker pull error:unexpected EOF 13 | blobdescriptor: inmemory 14 | filesystem: 15 | rootdirectory: /var/lib/registry 16 | #inmemory: # 此存储驱动程序不会在运行期间保留任何数据,适合磁盘空间小的机器使用(但是会使用内存开销,只适合测试) 17 | maintenance: 18 | uploadpurging: 19 | enabled: false 20 | tag: 21 | concurrencylimit: 8 22 | delete: 23 | enabled: true 24 | 25 | http: 26 | addr: :5000 27 | headers: 28 | X-Content-Type-Options: [nosniff] 29 | Access-Control-Allow-Origin: ['*'] 30 | Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE'] 31 | Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control'] 32 | Access-Control-Max-Age: [1728000] 33 | Access-Control-Allow-Credentials: [true] 34 | Access-Control-Expose-Headers: ['Docker-Content-Digest'] 35 | 36 | health: 37 | storagedriver: 38 | enabled: true 39 | interval: 10s 40 | threshold: 3 41 | 42 | proxy: 43 | remoteurl: https://mcr.microsoft.com 44 | username: 45 | password: 46 | ttl: 168h -------------------------------------------------------------------------------- /config/registry-nvcr.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | log: 3 | accesslog: 4 | disabled: true 5 | level: info 6 | formatter: text 7 | fields: 8 | service: registry 9 | environment: staging 10 | storage: 11 | cache: 12 | # 改为 blan 禁用 blob 描述符缓存,可解决docker pull error:unexpected EOF 13 | blobdescriptor: inmemory 14 | filesystem: 15 | rootdirectory: /var/lib/registry 16 | #inmemory: # 此存储驱动程序不会在运行期间保留任何数据,适合磁盘空间小的机器使用(但是会使用内存开销,只适合测试) 17 | maintenance: 18 | uploadpurging: 19 | enabled: false 20 | tag: 21 | concurrencylimit: 8 22 | delete: 23 | enabled: true 24 | 25 | http: 26 | addr: :5000 27 | headers: 28 | X-Content-Type-Options: [nosniff] 29 | Access-Control-Allow-Origin: ['*'] 30 | Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE'] 31 | Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control'] 32 | Access-Control-Max-Age: [1728000] 33 | Access-Control-Allow-Credentials: [true] 34 | Access-Control-Expose-Headers: ['Docker-Content-Digest'] 35 | 36 | health: 37 | storagedriver: 38 | enabled: true 39 | interval: 10s 40 | threshold: 3 41 | 42 | proxy: 43 | remoteurl: https://nvcr.io 44 | username: 45 | password: 46 | ttl: 168h -------------------------------------------------------------------------------- /config/registry-quay.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | log: 3 | accesslog: 4 | disabled: true 5 | level: info 6 | formatter: text 7 | fields: 8 | service: registry 9 | environment: staging 10 | storage: 11 | cache: 12 | # 改为 blan 禁用 blob 描述符缓存,可解决docker pull error:unexpected EOF 13 | blobdescriptor: inmemory 14 | filesystem: 15 | rootdirectory: /var/lib/registry 16 | #inmemory: # 此存储驱动程序不会在运行期间保留任何数据,适合磁盘空间小的机器使用(但是会使用内存开销,只适合测试) 17 | maintenance: 18 | uploadpurging: 19 | enabled: false 20 | tag: 21 | concurrencylimit: 8 22 | delete: 23 | enabled: true 24 | 25 | http: 26 | addr: :5000 27 | headers: 28 | X-Content-Type-Options: [nosniff] 29 | Access-Control-Allow-Origin: ['*'] 30 | Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE'] 31 | Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control'] 32 | Access-Control-Max-Age: [1728000] 33 | Access-Control-Allow-Credentials: [true] 34 | Access-Control-Expose-Headers: ['Docker-Content-Digest'] 35 | 36 | health: 37 | storagedriver: 38 | enabled: true 39 | interval: 10s 40 | threshold: 3 41 | 42 | proxy: 43 | remoteurl: https://quay.io 44 | username: 45 | password: 46 | ttl: 168h -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | ## docker hub 3 | dockerhub: 4 | container_name: reg-docker-hub 5 | image: dqzboy/registry:latest 6 | restart: always 7 | environment: 8 | - OTEL_TRACES_EXPORTER=none 9 | #- http=http://host:port 10 | #- https=http://host:port 11 | volumes: 12 | - ./registry/data:/var/lib/registry 13 | - ./registry-hub.yml:/etc/distribution/config.yml 14 | #- ./htpasswd:/auth/htpasswd 15 | ports: 16 | - 51000:5000 17 | networks: 18 | - registry-net 19 | 20 | ## ghcr.io 21 | ghcr: 22 | container_name: reg-ghcr 23 | image: dqzboy/registry:latest 24 | restart: always 25 | environment: 26 | - OTEL_TRACES_EXPORTER=none 27 | #- http=http://host:port 28 | #- https=http://host:port 29 | volumes: 30 | - ./registry/data:/var/lib/registry 31 | - ./registry-ghcr.yml:/etc/distribution/config.yml 32 | #- ./htpasswd:/auth/htpasswd 33 | ports: 34 | - 52000:5000 35 | networks: 36 | - registry-net 37 | 38 | ## gcr.io 39 | gcr: 40 | container_name: reg-gcr 41 | image: dqzboy/registry:latest 42 | restart: always 43 | environment: 44 | - OTEL_TRACES_EXPORTER=none 45 | #- http=http://host:port 46 | #- https=http://host:port 47 | volumes: 48 | - ./registry/data:/var/lib/registry 49 | - ./registry-gcr.yml:/etc/distribution/config.yml 50 | #- ./htpasswd:/auth/htpasswd 51 | ports: 52 | - 53000:5000 53 | networks: 54 | - registry-net 55 | 56 | ## k8s.gcr.io 57 | k8sgcr: 58 | container_name: reg-k8s-gcr 59 | image: dqzboy/registry:latest 60 | restart: always 61 | environment: 62 | - OTEL_TRACES_EXPORTER=none 63 | #- http=http://host:port 64 | #- https=http://host:port 65 | volumes: 66 | - ./registry/data:/var/lib/registry 67 | - ./registry-k8sgcr.yml:/etc/distribution/config.yml 68 | #- ./htpasswd:/auth/htpasswd 69 | ports: 70 | - 54000:5000 71 | networks: 72 | - registry-net 73 | 74 | ## registry.k8s.io 75 | k8s: 76 | container_name: reg-k8s 77 | image: dqzboy/registry:latest 78 | restart: always 79 | environment: 80 | - OTEL_TRACES_EXPORTER=none 81 | #- http=http://host:port 82 | #- https=http://host:port 83 | volumes: 84 | - ./registry/data:/var/lib/registry 85 | - ./registry-k8s.yml:/etc/distribution/config.yml 86 | #- ./htpasswd:/auth/htpasswd 87 | ports: 88 | - 55000:5000 89 | networks: 90 | - registry-net 91 | 92 | ## quay.io 93 | quay: 94 | container_name: reg-quay 95 | image: dqzboy/registry:latest 96 | restart: always 97 | environment: 98 | - OTEL_TRACES_EXPORTER=none 99 | #- http=http://host:port 100 | #- https=http://host:port 101 | volumes: 102 | - ./registry/data:/var/lib/registry 103 | - ./registry-quay.yml:/etc/distribution/config.yml 104 | #- ./htpasswd:/auth/htpasswd 105 | ports: 106 | - 56000:5000 107 | networks: 108 | - registry-net 109 | 110 | ## mcr.microsoft.com 111 | mcr: 112 | container_name: reg-mcr 113 | image: dqzboy/registry:latest 114 | restart: always 115 | environment: 116 | - OTEL_TRACES_EXPORTER=none 117 | #- http=http://host:port 118 | #- https=http://host:port 119 | volumes: 120 | - ./registry/data:/var/lib/registry 121 | - ./registry-mcr.yml:/etc/distribution/config.yml 122 | #- ./htpasswd:/auth/htpasswd 123 | ports: 124 | - 57000:5000 125 | networks: 126 | - registry-net 127 | 128 | ## docker.elastic.co 129 | elastic: 130 | container_name: reg-elastic 131 | image: dqzboy/registry:latest 132 | restart: always 133 | environment: 134 | - OTEL_TRACES_EXPORTER=none 135 | #- http=http://host:port 136 | #- https=http://host:port 137 | volumes: 138 | - ./registry/data:/var/lib/registry 139 | - ./registry-elastic.yml:/etc/distribution/config.yml 140 | #- ./htpasswd:/auth/htpasswd 141 | ports: 142 | - 58000:5000 143 | networks: 144 | - registry-net 145 | 146 | ## nvcr.io 147 | nvcr: 148 | container_name: reg-nvcr 149 | image: dqzboy/registry:latest 150 | restart: always 151 | environment: 152 | - OTEL_TRACES_EXPORTER=none 153 | #- http=http://host:port 154 | #- https=http://host:port 155 | volumes: 156 | - ./registry/data:/var/lib/registry 157 | - ./registry-nvcr.yml:/etc/distribution/config.yml 158 | #- ./htpasswd:/auth/htpasswd 159 | ports: 160 | - 59000:5000 161 | networks: 162 | - registry-net 163 | 164 | ## UI 165 | registry-ui: 166 | container_name: registry-ui 167 | image: dqzboy/docker-registry-ui:latest 168 | environment: 169 | - DOCKER_REGISTRY_URL=http://reg-docker-hub:5000 170 | # [必须]使用 openssl rand -hex 16 生成唯一值 171 | - SECRET_KEY_BASE=9f18244a1e1179fa5aa4a06a335d01b2 172 | # 启用Image TAG 的删除按钮 173 | - ENABLE_DELETE_IMAGES=true 174 | - NO_SSL_VERIFICATION=true 175 | restart: always 176 | ports: 177 | - 50000:8080 178 | networks: 179 | - registry-net 180 | 181 | networks: 182 | registry-net: -------------------------------------------------------------------------------- /hubcmdui/README.md: -------------------------------------------------------------------------------- 1 |

2 | 中文 | English 3 |

4 | 5 |
6 |

7 | 8 |
9 | Docker镜像加速命令查询获取、镜像搜索、配置教程文档展示UI面板. 10 |

11 |
12 | 13 |
14 | 15 | [![Auth](https://img.shields.io/badge/Auth-dqzboy-ff69b4)](https://github.com/dqzboy) 16 | [![GitHub contributors](https://img.shields.io/github/contributors/dqzboy/Docker-Proxy)](https://github.com/dqzboy/Docker-Proxy/graphs/contributors) 17 | [![GitHub Issues](https://img.shields.io/github/issues/dqzboy/Docker-Proxy.svg)](https://github.com/dqzboy/Docker-Proxy/issues) 18 | [![GitHub Pull Requests](https://img.shields.io/github/stars/dqzboy/Docker-Proxy)](https://github.com/dqzboy/Docker-Proxy) 19 | [![HitCount](https://views.whatilearened.today/views/github/dqzboy/Docker-Proxy.svg)](https://github.com/dqzboy/Docker-Proxy) 20 | [![GitHub license](https://img.shields.io/github/license/dqzboy/Docker-Proxy)](https://github.com/dqzboy/Docker-Proxy/blob/main/LICENSE) 21 | 22 | 23 | 📢 Docker Proxy-交流群 24 | 25 |
26 | 27 | --- 28 | 29 | ## 📝 源码构建运行 30 | #### 1. 克隆项目 31 | ```bash 32 | git clone git@github.com:dqzboy/Docker-Proxy.git 33 | ``` 34 | 35 | #### 2. 安装依赖 36 | ```bash 37 | cd Docker-Proxy/hubcmdui 38 | npm install 39 | ``` 40 | 41 | #### 3. 启动服务 42 | ```bash 43 | node server.js 44 | ``` 45 | 46 | ## 📦 Docker 方式运行 47 | 48 | #### 1. 下载 hubcmd-ui 镜像 49 | ```bash 50 | docker pull dqzboy/hubcmd-ui:latest 51 | ``` 52 | 53 | #### 2. 运行 hubcmd-ui 容器 54 | ```bash 55 | docker run -d -v /var/run/docker.sock:/var/run/docker.sock -p 30080:3000 --name hubcmdui-server dqzboy/hubcmd-ui 56 | ``` 57 | - `-v` 参数解释:左边是宿主机上的 Docker socket 文件路径,右边是容器内的映射路径 58 | 59 | ## Docker Compose 部署 60 | 61 | #### 1. 下载 [docker-compose.yaml](https://github.com/dqzboy/Docker-Proxy/blob/main/hubcmdui/docker-compose.yaml) 文件到你本地机器上 62 | 63 | #### 2. 执行 `docker compose` 或 `docker-compose` 命令启动容器服务 64 | 65 | ```shell 66 | docker compose up -d 67 | 68 | # 查看容器日志 69 | docker logs -f [容器ID或名称] 70 | ``` 71 | 72 | --- 73 | 74 | ## UI界面 75 | 76 | - 默认容器监听`3000`端口,映射宿主机端口`30080` 77 | 78 | > 浏览器输入 `服务器地址:30080` 访问前端 79 | 80 | 81 | 82 | 83 | 84 |
85 | 86 | 87 | 88 | 89 | 90 |
91 | 92 | 93 | 94 | 95 | 96 |
97 | 98 | 99 | 100 | 101 | 102 |
103 | 104 | 105 | 106 | 107 | 108 |
109 | 110 | > 浏览器输入 `服务器地址:30080/admin` 访问后端页面,默认登入账号密码: root / admin@123 111 | 112 | 113 | 114 | 115 | 116 |
117 | 118 | 119 | 120 | 121 | 122 |
123 | 124 | 125 | 126 | 127 | 128 |
129 | 130 | 131 | 132 | 133 | 134 |
135 | 136 | 137 | 138 | 139 | 140 |
141 | 142 | --- 143 | 144 | ## 🫶 赞助 145 | 如果你觉得这个项目对你有帮助,请给我点个Star。并且情况允许的话,可以给我一点点支持,总之非常感谢支持😊 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 |
Alipay WeChat Pay
157 | 158 | --- 159 | 160 | ## 😺 其他 161 | 162 | 开源不易,若你参考此项目或基于此项目修改可否麻烦在你的项目文档中标识此项目?谢谢你! 163 | 164 | --- 165 | 166 | ## License 167 | Docker-Proxy is available under the [Apache 2 license](./LICENSE) 168 | -------------------------------------------------------------------------------- /hubcmdui/app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * 应用主入口文件 - 启动服务器并初始化所有组件 5 | */ 6 | 7 | // 记录服务器启动时间 - 最先执行这行代码,确保第一时间记录 8 | global.serverStartTime = Date.now(); 9 | 10 | const express = require('express'); 11 | const session = require('express-session'); 12 | const path = require('path'); 13 | const http = require('http'); 14 | const logger = require('./logger'); 15 | const { ensureDirectoriesExist } = require('./init-dirs'); 16 | const registerRoutes = require('./routes'); 17 | const { requireLogin, sessionActivity, sanitizeRequestBody, securityHeaders } = require('./middleware/auth'); 18 | 19 | // 记录服务器启动时间到日志 20 | console.log(`服务器启动,时间戳: ${global.serverStartTime}`); 21 | logger.warn(`服务器启动,时间戳: ${global.serverStartTime}`); 22 | 23 | // 添加 session 文件存储模块 - 先导入session-file-store并创建对象 24 | const FileStore = require('session-file-store')(session); 25 | 26 | // 确保目录结构存在 27 | ensureDirectoriesExist().catch(err => { 28 | logger.error('创建必要目录失败:', err); 29 | process.exit(1); 30 | }); 31 | 32 | // 初始化Express应用 - 确保正确初始化 33 | const app = express(); 34 | const server = http.createServer(app); 35 | 36 | // 基本中间件配置 37 | app.use(express.json()); 38 | app.use(express.urlencoded({ extended: true })); 39 | app.use(express.static(path.join(__dirname, 'web'))); 40 | // 添加对documentation目录的静态访问 41 | app.use('/documentation', express.static(path.join(__dirname, 'documentation'))); 42 | app.use(sessionActivity); 43 | app.use(sanitizeRequestBody); 44 | app.use(securityHeaders); 45 | 46 | // 会话配置 47 | app.use(session({ 48 | secret: process.env.SESSION_SECRET || 'hubcmdui-secret-key', 49 | resave: false, 50 | saveUninitialized: false, 51 | cookie: { 52 | secure: process.env.NODE_ENV === 'production', 53 | maxAge: 24 * 60 * 60 * 1000 // 24小时 54 | }, 55 | store: new FileStore({ 56 | path: path.join(__dirname, 'data', 'sessions'), 57 | ttl: 86400 58 | }) 59 | })); 60 | 61 | // 添加一个中间件来检查API请求的会话状态 62 | app.use('/api', (req, res, next) => { 63 | // 这些API端点不需要登录 64 | const publicEndpoints = [ 65 | '/api/login', 66 | '/api/logout', 67 | '/api/check-session', 68 | '/api/health', 69 | '/api/system-status', 70 | '/api/system-resource-details', 71 | '/api/menu-items', 72 | '/api/config', 73 | '/api/monitoring-config', 74 | '/api/documentation', 75 | '/api/documentation/file' 76 | ]; 77 | 78 | // 如果是公共API或用户已登录,则继续 79 | if (publicEndpoints.includes(req.path) || 80 | publicEndpoints.some(endpoint => req.path.startsWith(endpoint)) || 81 | (req.session && req.session.user)) { 82 | return next(); 83 | } 84 | 85 | // 否则返回401未授权 86 | logger.warn(`未授权访问: ${req.path}`); 87 | return res.status(401).json({ error: 'Unauthorized' }); 88 | }); 89 | 90 | // 导入并注册所有路由 91 | registerRoutes(app); 92 | 93 | // 默认路由 94 | app.get('/', (req, res) => { 95 | res.sendFile(path.join(__dirname, 'web', 'index.html')); 96 | }); 97 | 98 | app.get('/admin', (req, res) => { 99 | res.sendFile(path.join(__dirname, 'web', 'admin.html')); 100 | }); 101 | 102 | // 404处理 103 | app.use((req, res) => { 104 | res.status(404).json({ error: 'Not Found' }); 105 | }); 106 | 107 | // 错误处理中间件 108 | app.use((err, req, res, next) => { 109 | logger.error('应用错误:', err); 110 | res.status(500).json({ error: '服务器内部错误', details: err.message }); 111 | }); 112 | 113 | // 启动服务器 114 | const PORT = process.env.PORT || 3000; 115 | server.listen(PORT, async () => { 116 | logger.info(`服务器已启动并监听端口 ${PORT}`); 117 | 118 | try { 119 | // 确保目录存在 120 | await ensureDirectoriesExist(); 121 | logger.success('系统初始化完成'); 122 | } catch (error) { 123 | logger.error('系统初始化失败:', error); 124 | } 125 | }); 126 | 127 | // 注册进程事件处理 128 | process.on('SIGINT', () => { 129 | logger.info('接收到中断信号,正在关闭服务...'); 130 | server.close(() => { 131 | logger.info('服务器已关闭'); 132 | process.exit(0); 133 | }); 134 | }); 135 | 136 | process.on('SIGTERM', () => { 137 | logger.info('接收到终止信号,正在关闭服务...'); 138 | server.close(() => { 139 | logger.info('服务器已关闭'); 140 | process.exit(0); 141 | }); 142 | }); 143 | 144 | module.exports = { app, server }; 145 | 146 | // 路由注册函数 147 | function registerRoutes(app) { 148 | try { 149 | logger.info('开始注册路由...'); 150 | 151 | // API端点 152 | app.use('/api', [ 153 | require('./routes/index'), 154 | require('./routes/docker'), 155 | require('./routes/docs'), 156 | require('./routes/users'), 157 | require('./routes/menu'), 158 | require('./routes/server') 159 | ]); 160 | logger.info('基本API路由已注册'); 161 | 162 | // 系统路由 - 函数式注册 163 | const systemRouter = require('./routes/system'); 164 | app.use('/api/system', systemRouter); 165 | logger.info('系统路由已注册'); 166 | 167 | // 认证路由 - 直接使用Router实例 168 | const authRouter = require('./routes/auth'); 169 | app.use('/api', authRouter); 170 | logger.info('认证路由已注册'); 171 | 172 | // 配置路由 - 函数式注册 173 | const configRouter = require('./routes/config'); 174 | if (typeof configRouter === 'function') { 175 | logger.info('配置路由是一个函数,正在注册...'); 176 | configRouter(app); 177 | logger.info('配置路由已注册'); 178 | } else { 179 | logger.error('配置路由不是一个函数,无法注册', typeof configRouter); 180 | } 181 | 182 | logger.success('✓ 所有路由已注册'); 183 | } catch (error) { 184 | logger.error('路由注册失败:', error); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /hubcmdui/cleanup.js: -------------------------------------------------------------------------------- 1 | const logger = require('./logger'); 2 | 3 | // 处理未捕获的异常 4 | process.on('uncaughtException', (error) => { 5 | logger.error('未捕获的异常:', error); 6 | // 打印完整的堆栈跟踪以便调试 7 | console.error('错误堆栈:', error.stack); 8 | // 不立即退出,以便日志能够被写入 9 | setTimeout(() => { 10 | process.exit(1); 11 | }, 1000); 12 | }); 13 | 14 | // 处理未处理的Promise拒绝 15 | process.on('unhandledRejection', (reason, promise) => { 16 | logger.error('未处理的Promise拒绝:', reason); 17 | // 打印堆栈跟踪(如果可用) 18 | if (reason instanceof Error) { 19 | console.error('Promise拒绝堆栈:', reason.stack); 20 | } 21 | }); 22 | 23 | // 处理退出信号 24 | process.on('SIGINT', gracefulShutdown); 25 | process.on('SIGTERM', gracefulShutdown); 26 | 27 | // 优雅退出函数 28 | function gracefulShutdown() { 29 | logger.info('接收到退出信号,正在关闭...'); 30 | 31 | // 这里可以添加清理代码,如关闭数据库连接等 32 | try { 33 | // 关闭任何可能的资源 34 | try { 35 | const docker = require('./services/dockerService').getDockerConnection(); 36 | if (docker) { 37 | logger.info('正在关闭Docker连接...'); 38 | // 如果有活动的Docker连接,可能需要执行一些清理 39 | } 40 | } catch (err) { 41 | // 忽略错误,可能服务未初始化 42 | logger.debug('Docker服务未初始化,跳过清理'); 43 | } 44 | 45 | // 清理监控间隔 46 | try { 47 | const monitoringService = require('./services/monitoringService'); 48 | if (monitoringService.stopMonitoring) { 49 | logger.info('正在停止容器监控...'); 50 | monitoringService.stopMonitoring(); 51 | } 52 | } catch (err) { 53 | // 忽略错误,可能服务未初始化 54 | logger.debug('监控服务未初始化,跳过清理'); 55 | } 56 | 57 | logger.info('所有资源已清理完毕,正在退出...'); 58 | } catch (error) { 59 | logger.error('退出过程中出现错误:', error); 60 | } 61 | 62 | setTimeout(() => { 63 | logger.info('干净退出完成'); 64 | process.exit(0); 65 | }, 1000); 66 | } 67 | 68 | logger.info('错误处理和清理脚本已加载'); 69 | 70 | module.exports = { 71 | gracefulShutdown 72 | }; 73 | -------------------------------------------------------------------------------- /hubcmdui/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 应用全局配置文件 3 | */ 4 | 5 | // 环境变量 6 | const ENV = process.env.NODE_ENV || 'development'; 7 | 8 | // 应用配置 9 | const config = { 10 | // 通用配置 11 | common: { 12 | port: process.env.PORT || 3000, 13 | sessionSecret: process.env.SESSION_SECRET || 'OhTq3faqSKoxbV%NJV', 14 | logLevel: process.env.LOG_LEVEL || 'info' 15 | }, 16 | 17 | // 开发环境配置 18 | development: { 19 | debug: true, 20 | cors: { 21 | origin: '*', 22 | credentials: true 23 | }, 24 | secureSession: false 25 | }, 26 | 27 | // 生产环境配置 28 | production: { 29 | debug: false, 30 | cors: { 31 | origin: 'https://yourdomain.com', 32 | credentials: true 33 | }, 34 | secureSession: true 35 | }, 36 | 37 | // 测试环境配置 38 | test: { 39 | debug: true, 40 | cors: { 41 | origin: '*', 42 | credentials: true 43 | }, 44 | secureSession: false, 45 | port: 3001 46 | } 47 | }; 48 | 49 | // 导出合并后的配置 50 | module.exports = { 51 | ...config.common, 52 | ...config[ENV], 53 | env: ENV 54 | }; 55 | -------------------------------------------------------------------------------- /hubcmdui/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": "light", 3 | "language": "zh_CN", 4 | "notifications": true, 5 | "autoRefresh": true, 6 | "refreshInterval": 30000, 7 | "dockerHost": "localhost", 8 | "dockerPort": 2375, 9 | "useHttps": false, 10 | "menuItems": [ 11 | { 12 | "text": "控制台", 13 | "link": "/admin", 14 | "newTab": false 15 | }, 16 | { 17 | "text": "镜像搜索", 18 | "link": "/", 19 | "newTab": false 20 | }, 21 | { 22 | "text": "文档", 23 | "link": "/docs", 24 | "newTab": false 25 | }, 26 | { 27 | "text": "GitHub", 28 | "link": "https://github.com/dqzboy/hubcmdui", 29 | "newTab": true 30 | } 31 | ], 32 | "monitoringConfig": { 33 | "notificationType": "wechat", 34 | "webhookUrl": "", 35 | "telegramToken": "", 36 | "telegramChatId": "", 37 | "monitorInterval": 60, 38 | "isEnabled": false 39 | } 40 | } -------------------------------------------------------------------------------- /hubcmdui/config/menu.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /hubcmdui/config/monitoring.json: -------------------------------------------------------------------------------- 1 | { 2 | "isEnabled": false, 3 | "notificationType": "wechat", 4 | "webhookUrl": "", 5 | "telegramToken": "", 6 | "telegramChatId": "", 7 | "monitorInterval": 60 8 | } -------------------------------------------------------------------------------- /hubcmdui/data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "logo": "", 3 | "menuItems": [ 4 | { 5 | "text": "首页", 6 | "link": "", 7 | "newTab": false 8 | }, 9 | { 10 | "text": "GitHub", 11 | "link": "https://github.com/dqzboy/Docker-Proxy", 12 | "newTab": true 13 | }, 14 | { 15 | "text": "VPS推荐", 16 | "link": "https://dqzboy.github.io/proxyui/racknerd", 17 | "newTab": true 18 | } 19 | ], 20 | "monitoringConfig": { 21 | "notificationType": "telegram", 22 | "webhookUrl": "", 23 | "telegramToken": "", 24 | "telegramChatId": "", 25 | "monitorInterval": 60, 26 | "isEnabled": false 27 | }, 28 | "proxyDomain": "github.dqzboy.Docker-Proxy" 29 | } -------------------------------------------------------------------------------- /hubcmdui/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | ## HubCMD UI 3 | hubcmd-ui: 4 | container_name: hubcmd-ui 5 | image: dqzboy/hubcmd-ui:latest 6 | restart: always 7 | volumes: 8 | - /var/run/docker.sock:/var/run/docker.sock 9 | ports: 10 | - 30080:3000 11 | environment: 12 | # 日志配置 13 | - LOG_LEVEL=INFO # 可选: TRACE, DEBUG, INFO, SUCCESS, WARN, ERROR, FATAL 14 | - SIMPLE_LOGS=true # 启用简化日志输出,减少冗余信息 15 | # - DETAILED_LOGS=false # 默认关闭详细日志记录(请求体、查询参数等) 16 | # - SHOW_STACK=false # 默认关闭错误堆栈跟踪 17 | # - LOG_FILE_ENABLED=true # 是否启用文件日志,默认启用 18 | # - LOG_CONSOLE_ENABLED=true # 是否启用控制台日志,默认启用 19 | # - LOG_MAX_SIZE=10 # 单个日志文件最大大小(MB),默认10MB 20 | # - LOG_MAX_FILES=14 # 保留的日志文件数量,默认14个 -------------------------------------------------------------------------------- /hubcmdui/documentation/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dqzboy/Docker-Proxy/04cfa607b87f46acd6f8904f6e0be96ab6057b45/hubcmdui/documentation/.DS_Store -------------------------------------------------------------------------------- /hubcmdui/documentation/1743542841590.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Docker 配置镜像加速", 3 | "content": "# Docker 配置镜像加速\n\n- 修改文件 `/etc/docker/daemon.json`(如果不存在则创建)\n\n```\nsudo mkdir -p /etc/docker\nsudo vi /etc/docker/daemon.json\n{\n \"registry-mirrors\": [\"https://<代理加速地址>\"]\n}\n\nsudo systemctl daemon-reload\nsudo systemctl restart docker\n```", 4 | "published": true, 5 | "createdAt": "2025-04-01T21:27:21.591Z", 6 | "updatedAt": "2025-05-10T06:21:33.539Z" 7 | } -------------------------------------------------------------------------------- /hubcmdui/documentation/1743543376091.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Containerd 配置镜像加速", 3 | "content": "# Containerd 配置镜像加速\n\n\n* `/etc/containerd/config.toml`,添加如下的配置:\n\n```bash\n [plugins.\"io.containerd.grpc.v1.cri\".registry]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"docker.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"k8s.gcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"gcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"ghcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"quay.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n```", 4 | "published": true, 5 | "createdAt": "2025-04-01T21:36:16.092Z", 6 | "updatedAt": "2025-05-10T06:21:38.920Z" 7 | } -------------------------------------------------------------------------------- /hubcmdui/documentation/1743543400369.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Podman 配置镜像加速", 3 | "content": "# Podman 配置镜像加速\n\n* 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```bash\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```# Podman 配置镜像加速\n\n* 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```bash\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```", 4 | "published": true, 5 | "createdAt": "2025-04-01T21:36:40.369Z", 6 | "updatedAt": "2025-05-08T15:16:47.900Z" 7 | } -------------------------------------------------------------------------------- /hubcmdui/download-images.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 下载必要的图片资源 3 | */ 4 | const fs = require('fs').promises; 5 | const path = require('path'); 6 | const https = require('https'); 7 | const logger = require('./logger'); 8 | const { ensureDirectoriesExist } = require('./init-dirs'); 9 | 10 | // 背景图片URL 11 | const LOGIN_BG_URL = 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?q=80&w=1470&auto=format&fit=crop'; 12 | 13 | // 下载图片函数 14 | function downloadImage(url, dest) { 15 | return new Promise((resolve, reject) => { 16 | const file = fs.createWriteStream(dest); 17 | 18 | https.get(url, response => { 19 | if (response.statusCode !== 200) { 20 | reject(new Error(`Failed to download image. Status code: ${response.statusCode}`)); 21 | return; 22 | } 23 | 24 | response.pipe(file); 25 | 26 | file.on('finish', () => { 27 | file.close(); 28 | logger.success(`Image downloaded to: ${dest}`); 29 | resolve(); 30 | }); 31 | 32 | file.on('error', err => { 33 | fs.unlink(dest).catch(() => {}); // 删除文件(如果存在) 34 | reject(err); 35 | }); 36 | }).on('error', err => { 37 | fs.unlink(dest).catch(() => {}); // 删除文件(如果存在) 38 | reject(err); 39 | }); 40 | }); 41 | } 42 | 43 | // 主函数 44 | async function downloadImages() { 45 | try { 46 | // 确保目录存在 47 | await ensureDirectoriesExist(); 48 | 49 | // 下载登录背景图片 50 | const loginBgPath = path.join(__dirname, 'web', 'images', 'login-bg.jpg'); 51 | try { 52 | await fs.access(loginBgPath); 53 | logger.info('Login background image already exists, skipping download'); 54 | } catch (error) { 55 | if (error.code === 'ENOENT') { 56 | logger.info('Downloading login background image...'); 57 | try { 58 | // 确保images目录存在 59 | await fs.mkdir(path.dirname(loginBgPath), { recursive: true }); 60 | await downloadImage(LOGIN_BG_URL, loginBgPath); 61 | } catch (downloadError) { 62 | logger.error(`Download error: ${downloadError.message}`); 63 | // 下载失败时使用备用解决方案 64 | await fs.writeFile(loginBgPath, 'Failed to download', 'utf8'); 65 | logger.warn('Created placeholder image file'); 66 | } 67 | } else { 68 | throw error; 69 | } 70 | } 71 | 72 | logger.success('All images downloaded successfully'); 73 | } catch (error) { 74 | logger.error('Error downloading images:', error); 75 | } 76 | } 77 | 78 | // 如果直接运行此脚本 79 | if (require.main === module) { 80 | downloadImages(); 81 | } 82 | 83 | module.exports = { downloadImages }; 84 | -------------------------------------------------------------------------------- /hubcmdui/init-dirs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 目录初始化模块 - 确保应用需要的所有目录都存在 3 | */ 4 | const fs = require('fs').promises; 5 | const path = require('path'); 6 | const logger = require('./logger'); 7 | 8 | /** 9 | * 确保所有必需的目录存在 10 | */ 11 | // 添加缓存机制 12 | const checkedDirs = new Set(); 13 | 14 | async function ensureDirectoriesExist() { 15 | const dirs = [ 16 | // 文档目录 17 | path.join(__dirname, 'documentation'), 18 | // 日志目录 19 | path.join(__dirname, 'logs'), 20 | // 图片目录 21 | path.join(__dirname, 'web', 'images'), 22 | // 数据目录 23 | path.join(__dirname, 'data'), 24 | // 配置目录 25 | path.join(__dirname, 'config'), 26 | // 临时文件目录 27 | path.join(__dirname, 'temp'), 28 | // session 目录 29 | path.join(__dirname, 'data', 'sessions'), 30 | // 文档数据目录 31 | path.join(__dirname, 'web', 'data', 'documentation') 32 | ]; 33 | 34 | for (const dir of dirs) { 35 | if (checkedDirs.has(dir)) continue; 36 | 37 | try { 38 | await fs.access(dir); 39 | logger.info(`目录已存在: ${dir}`); 40 | } catch (error) { 41 | if (error.code === 'ENOENT') { 42 | try { 43 | await fs.mkdir(dir, { recursive: true }); 44 | logger.success(`创建目录: ${dir}`); 45 | } catch (mkdirError) { 46 | logger.error(`创建目录 ${dir} 失败: ${mkdirError.message}`); 47 | throw mkdirError; 48 | } 49 | } else { 50 | logger.error(`检查目录 ${dir} 失败: ${error.message}`); 51 | throw error; 52 | } 53 | } 54 | 55 | checkedDirs.add(dir); 56 | } 57 | 58 | // 确保文档索引存在,但不再添加默认文档 59 | const docIndexPath = path.join(__dirname, 'web', 'data', 'documentation', 'index.json'); 60 | try { 61 | await fs.access(docIndexPath); 62 | logger.info('文档索引已存在'); 63 | } catch (error) { 64 | if (error.code === 'ENOENT') { 65 | try { 66 | // 创建一个空的文档索引 67 | await fs.writeFile(docIndexPath, JSON.stringify([]), 'utf8'); 68 | logger.success('创建了空的文档索引文件'); 69 | } catch (writeError) { 70 | logger.error(`创建文档索引失败: ${writeError.message}`); 71 | } 72 | } 73 | } 74 | } 75 | 76 | // 如果直接运行此脚本 77 | if (require.main === module) { 78 | ensureDirectoriesExist() 79 | .then(() => logger.info('目录初始化完成')) 80 | .catch(err => { 81 | logger.error('目录初始化失败:', err); 82 | process.exit(1); 83 | }); 84 | } 85 | 86 | module.exports = { ensureDirectoriesExist }; 87 | -------------------------------------------------------------------------------- /hubcmdui/middleware/auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 认证相关中间件 3 | */ 4 | const logger = require('../logger'); 5 | 6 | /** 7 | * 检查是否已登录的中间件 8 | */ 9 | function requireLogin(req, res, next) { 10 | // 放开session检查,不强制要求登录 11 | if (req.url.startsWith('/api/documentation') || 12 | req.url.startsWith('/api/system-resources') || 13 | req.url.startsWith('/api/monitoring-config') || 14 | req.url.startsWith('/api/toggle-monitoring') || 15 | req.url.startsWith('/api/test-notification') || 16 | req.url.includes('/docker/status')) { 17 | return next(); // 这些API路径不需要登录 18 | } 19 | 20 | // 检查用户是否登录 21 | if (req.session && req.session.user) { 22 | // 刷新会话 23 | req.session.touch(); 24 | return next(); 25 | } 26 | 27 | // 未登录返回401错误 28 | res.status(401).json({ error: '未登录或会话已过期', code: 'SESSION_EXPIRED' }); 29 | } 30 | 31 | // 修改登录逻辑 32 | async function login(req, res) { 33 | try { 34 | const { username, password } = req.body; 35 | 36 | // 简单验证 37 | if (username === 'admin' && password === 'admin123') { 38 | req.session.user = { username }; 39 | return res.json({ success: true }); 40 | } 41 | 42 | res.status(401).json({ error: '用户名或密码错误' }); 43 | } catch (error) { 44 | logger.error('登录失败:', error); 45 | res.status(500).json({ error: '登录失败' }); 46 | } 47 | } 48 | 49 | /** 50 | * 记录会话活动的中间件 51 | */ 52 | function sessionActivity(req, res, next) { 53 | if (req.session && req.session.user) { 54 | req.session.lastActivity = Date.now(); 55 | req.session.touch(); // 确保会话刷新 56 | } 57 | next(); 58 | } 59 | 60 | // 过滤敏感信息中间件 61 | function sanitizeRequestBody(req, res, next) { 62 | if (req.body) { 63 | const sanitizedBody = {...req.body}; 64 | 65 | // 过滤敏感字段 66 | if (sanitizedBody.password) sanitizedBody.password = '[REDACTED]'; 67 | if (sanitizedBody.currentPassword) sanitizedBody.currentPassword = '[REDACTED]'; 68 | if (sanitizedBody.newPassword) sanitizedBody.newPassword = '[REDACTED]'; 69 | 70 | // 保存清理后的请求体供日志使用 71 | req.sanitizedBody = sanitizedBody; 72 | } 73 | next(); 74 | } 75 | 76 | // 安全头部中间件 77 | function securityHeaders(req, res, next) { 78 | // 添加安全头部 79 | res.setHeader('X-Content-Type-Options', 'nosniff'); 80 | res.setHeader('X-Frame-Options', 'DENY'); 81 | res.setHeader('X-XSS-Protection', '1; mode=block'); 82 | next(); 83 | } 84 | 85 | module.exports = { 86 | requireLogin, 87 | sessionActivity, 88 | sanitizeRequestBody, 89 | securityHeaders 90 | }; 91 | -------------------------------------------------------------------------------- /hubcmdui/middleware/client-error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 客户端错误处理中间件 3 | */ 4 | const logger = require('../logger'); 5 | 6 | // 处理客户端上报的错误 7 | function handleClientError(req, res, next) { 8 | if (req.url === '/api/client-error' && req.method === 'POST') { 9 | const { message, source, lineno, colno, error, stack, userAgent, page } = req.body; 10 | 11 | logger.error('客户端错误:', { 12 | message, 13 | source, 14 | location: `${lineno}:${colno}`, 15 | stack: stack || (error && error.stack), 16 | userAgent, 17 | page 18 | }); 19 | 20 | res.json({ success: true }); 21 | } else { 22 | next(); 23 | } 24 | } 25 | 26 | module.exports = handleClientError; 27 | -------------------------------------------------------------------------------- /hubcmdui/models/MenuItem.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const menuItemSchema = new mongoose.Schema({ 4 | text: { type: String, required: true }, 5 | link: { type: String, required: true }, 6 | icon: String, 7 | newTab: { type: Boolean, default: false }, 8 | enabled: { type: Boolean, default: true }, 9 | order: { type: Number, default: 0 }, 10 | createdAt: { type: Date, default: Date.now } 11 | }); 12 | 13 | module.exports = mongoose.model('MenuItem', menuItemSchema); -------------------------------------------------------------------------------- /hubcmdui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubcmdui", 3 | "version": "1.0.0", 4 | "description": "Docker镜像代理加速系统", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js", 9 | "test": "jest", 10 | "init": "node scripts/init-system.js", 11 | "setup": "npm install && node scripts/init-system.js && echo '系统安装完成,请使用 npm start 启动服务'" 12 | }, 13 | "keywords": [ 14 | "docker", 15 | "proxy", 16 | "management" 17 | ], 18 | "author": "", 19 | "license": "MIT", 20 | "dependencies": { 21 | "axios": "^0.27.2", 22 | "axios-retry": "^3.3.1", 23 | "bcrypt": "^5.0.1", 24 | "body-parser": "^1.20.0", 25 | "chalk": "^4.1.2", 26 | "cors": "^2.8.5", 27 | "dockerode": "^3.3.4", 28 | "express": "^4.21.2", 29 | "express-session": "^1.18.1", 30 | "node-cache": "^5.1.2", 31 | "p-limit": "^4.0.0", 32 | "session-file-store": "^1.5.0", 33 | "systeminformation": "^5.25.11", 34 | "validator": "^13.7.0", 35 | "ws": "^8.8.1" 36 | }, 37 | "devDependencies": { 38 | "jest": "^28.1.3", 39 | "nodemon": "^2.0.19" 40 | }, 41 | "engines": { 42 | "node": ">=14.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /hubcmdui/routes/auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 认证相关路由 3 | */ 4 | const express = require('express'); 5 | const router = express.Router(); 6 | const bcrypt = require('bcrypt'); 7 | const userService = require('../services/userService'); 8 | const logger = require('../logger'); 9 | const { requireLogin } = require('../middleware/auth'); 10 | 11 | // 登录验证 12 | router.post('/login', async (req, res) => { 13 | const { username, password, captcha } = req.body; 14 | if (req.session.captcha !== parseInt(captcha)) { 15 | logger.warn(`Captcha verification failed for user: ${username}`); 16 | return res.status(401).json({ error: '验证码错误' }); 17 | } 18 | 19 | try { 20 | const users = await userService.getUsers(); 21 | const user = users.users.find(u => u.username === username); 22 | 23 | if (!user) { 24 | logger.warn(`User ${username} not found`); 25 | return res.status(401).json({ error: '用户名或密码错误' }); 26 | } 27 | 28 | if (bcrypt.compareSync(req.body.password, user.password)) { 29 | req.session.user = { username: user.username }; 30 | 31 | // 更新用户登录信息 32 | await userService.updateUserLoginInfo(username); 33 | 34 | // 确保服务器启动时间已设置 35 | if (!global.serverStartTime) { 36 | global.serverStartTime = Date.now(); 37 | logger.warn(`登录时设置服务器启动时间: ${global.serverStartTime}`); 38 | } 39 | 40 | logger.info(`User ${username} logged in successfully`); 41 | res.json({ 42 | success: true, 43 | serverStartTime: global.serverStartTime 44 | }); 45 | } else { 46 | logger.warn(`Login failed for user: ${username}`); 47 | res.status(401).json({ error: '用户名或密码错误' }); 48 | } 49 | } catch (error) { 50 | logger.error('登录失败:', error); 51 | res.status(500).json({ error: '登录处理失败', details: error.message }); 52 | } 53 | }); 54 | 55 | // 注销 56 | router.post('/logout', (req, res) => { 57 | req.session.destroy(err => { 58 | if (err) { 59 | logger.error('销毁会话失败:', err); 60 | return res.status(500).json({ error: 'Failed to logout' }); 61 | } 62 | res.clearCookie('connect.sid'); 63 | logger.info('用户已退出登录'); 64 | res.json({ success: true }); 65 | }); 66 | }); 67 | 68 | // 修改密码 69 | router.post('/change-password', requireLogin, async (req, res) => { 70 | const { currentPassword, newPassword } = req.body; 71 | 72 | // 密码复杂度校验 73 | const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/; 74 | if (!passwordRegex.test(newPassword)) { 75 | return res.status(400).json({ error: 'Password must be 8-16 characters long and contain at least one letter, one number, and one special character' }); 76 | } 77 | 78 | try { 79 | const { users } = await userService.getUsers(); 80 | const user = users.find(u => u.username === req.session.user.username); 81 | 82 | if (user && bcrypt.compareSync(currentPassword, user.password)) { 83 | user.password = bcrypt.hashSync(newPassword, 10); 84 | await userService.saveUsers(users); 85 | res.json({ success: true }); 86 | } else { 87 | res.status(401).json({ error: 'Invalid current password' }); 88 | } 89 | } catch (error) { 90 | logger.error('修改密码失败:', error); 91 | res.status(500).json({ error: '修改密码失败', details: error.message }); 92 | } 93 | }); 94 | 95 | // 获取用户信息 96 | router.get('/user-info', requireLogin, async (req, res) => { 97 | try { 98 | const userService = require('../services/userService'); 99 | const userStats = await userService.getUserStats(req.session.user.username); 100 | 101 | res.json(userStats); 102 | } catch (error) { 103 | logger.error('获取用户信息失败:', error); 104 | res.status(500).json({ error: '获取用户信息失败', details: error.message }); 105 | } 106 | }); 107 | 108 | // 生成验证码 109 | router.get('/captcha', (req, res) => { 110 | const num1 = Math.floor(Math.random() * 10); 111 | const num2 = Math.floor(Math.random() * 10); 112 | const captcha = `${num1} + ${num2} = ?`; 113 | req.session.captcha = num1 + num2; 114 | 115 | // 确保serverStartTime已初始化 116 | if (!global.serverStartTime) { 117 | global.serverStartTime = Date.now(); 118 | logger.warn(`初始化服务器启动时间: ${global.serverStartTime}`); 119 | } 120 | 121 | res.json({ 122 | captcha, 123 | serverStartTime: global.serverStartTime 124 | }); 125 | }); 126 | 127 | // 检查会话状态 128 | router.get('/check-session', (req, res) => { 129 | // 如果global.serverStartTime不存在,创建一个 130 | if (!global.serverStartTime) { 131 | global.serverStartTime = Date.now(); 132 | logger.warn(`设置服务器启动时间: ${global.serverStartTime}`); 133 | } 134 | 135 | if (req.session && req.session.user) { 136 | return res.json({ 137 | success: true, 138 | user: { 139 | username: req.session.user.username, 140 | role: req.session.user.role, 141 | }, 142 | serverStartTime: global.serverStartTime // 返回服务器启动时间 143 | }); 144 | } 145 | return res.status(401).json({ 146 | success: false, 147 | message: '未登录', 148 | serverStartTime: global.serverStartTime // 即使未登录也返回服务器时间 149 | }); 150 | }); 151 | 152 | logger.success('✓ 认证路由已加载'); 153 | 154 | // 导出路由 155 | module.exports = router; 156 | -------------------------------------------------------------------------------- /hubcmdui/routes/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 配置路由模块 3 | */ 4 | const express = require('express'); 5 | const router = express.Router(); 6 | const fs = require('fs').promises; 7 | const path = require('path'); 8 | const logger = require('../logger'); 9 | const { requireLogin } = require('../middleware/auth'); 10 | const configService = require('../services/configService'); 11 | 12 | // 修改配置文件路径,使用独立的配置文件 13 | const configFilePath = path.join(__dirname, '../data/config.json'); 14 | 15 | // 默认配置 16 | const DEFAULT_CONFIG = { 17 | proxyDomain: 'registry-1.docker.io', 18 | logo: '', 19 | theme: 'light' 20 | }; 21 | 22 | // 确保配置文件存在 23 | async function ensureConfigFile() { 24 | try { 25 | // 确保目录存在 26 | const dir = path.dirname(configFilePath); 27 | try { 28 | await fs.access(dir); 29 | } catch (error) { 30 | await fs.mkdir(dir, { recursive: true }); 31 | logger.info(`创建目录: ${dir}`); 32 | } 33 | 34 | // 检查文件是否存在 35 | try { 36 | await fs.access(configFilePath); 37 | const data = await fs.readFile(configFilePath, 'utf8'); 38 | return JSON.parse(data); 39 | } catch (error) { 40 | // 文件不存在或JSON解析错误,创建默认配置 41 | await fs.writeFile(configFilePath, JSON.stringify(DEFAULT_CONFIG, null, 2)); 42 | logger.info(`创建默认配置文件: ${configFilePath}`); 43 | return DEFAULT_CONFIG; 44 | } 45 | } catch (error) { 46 | logger.error(`配置文件操作失败: ${error.message}`); 47 | // 出错时返回默认配置以确保API不会失败 48 | return DEFAULT_CONFIG; 49 | } 50 | } 51 | 52 | // 获取配置 53 | router.get('/config', async (req, res) => { 54 | try { 55 | const config = await ensureConfigFile(); 56 | res.json(config); 57 | } catch (error) { 58 | logger.error('获取配置失败:', error); 59 | // 即使失败也返回默认配置 60 | res.json(DEFAULT_CONFIG); 61 | } 62 | }); 63 | 64 | // 保存配置 65 | router.post('/config', async (req, res) => { 66 | try { 67 | const newConfig = req.body; 68 | 69 | // 验证请求数据 70 | if (!newConfig || typeof newConfig !== 'object') { 71 | return res.status(400).json({ 72 | error: '无效的配置数据', 73 | details: '配置必须是一个对象' 74 | }); 75 | } 76 | 77 | // 读取现有配置 78 | let existingConfig; 79 | try { 80 | existingConfig = await ensureConfigFile(); 81 | } catch (error) { 82 | existingConfig = DEFAULT_CONFIG; 83 | } 84 | 85 | // 合并配置 86 | const mergedConfig = { ...existingConfig, ...newConfig }; 87 | 88 | // 保存到文件 89 | await fs.writeFile(configFilePath, JSON.stringify(mergedConfig, null, 2)); 90 | 91 | res.json({ success: true, message: '配置已保存' }); 92 | } catch (error) { 93 | logger.error('保存配置失败:', error); 94 | res.status(500).json({ 95 | error: '保存配置失败', 96 | details: error.message 97 | }); 98 | } 99 | }); 100 | 101 | // 获取监控配置 102 | router.get('/monitoring-config', async (req, res) => { 103 | logger.info('收到监控配置请求'); 104 | 105 | try { 106 | logger.info('读取监控配置...'); 107 | const config = await configService.getConfig(); 108 | 109 | if (!config.monitoringConfig) { 110 | logger.info('监控配置不存在,创建默认配置'); 111 | config.monitoringConfig = { 112 | notificationType: 'wechat', 113 | webhookUrl: '', 114 | telegramToken: '', 115 | telegramChatId: '', 116 | monitorInterval: 60, 117 | isEnabled: false 118 | }; 119 | await configService.saveConfig(config); 120 | } 121 | 122 | logger.info('返回监控配置'); 123 | res.json({ 124 | notificationType: config.monitoringConfig.notificationType || 'wechat', 125 | webhookUrl: config.monitoringConfig.webhookUrl || '', 126 | telegramToken: config.monitoringConfig.telegramToken || '', 127 | telegramChatId: config.monitoringConfig.telegramChatId || '', 128 | monitorInterval: config.monitoringConfig.monitorInterval || 60, 129 | isEnabled: config.monitoringConfig.isEnabled || false 130 | }); 131 | } catch (error) { 132 | logger.error('获取监控配置失败:', error); 133 | res.status(500).json({ error: '获取监控配置失败', details: error.message }); 134 | } 135 | }); 136 | 137 | // 保存监控配置 138 | router.post('/monitoring-config', requireLogin, async (req, res) => { 139 | try { 140 | const { 141 | notificationType, 142 | webhookUrl, 143 | telegramToken, 144 | telegramChatId, 145 | monitorInterval, 146 | isEnabled 147 | } = req.body; 148 | 149 | // 验证必填字段 150 | if (!notificationType) { 151 | return res.status(400).json({ error: '通知类型不能为空' }); 152 | } 153 | 154 | // 根据通知类型验证对应的字段 155 | if (notificationType === 'wechat' && !webhookUrl) { 156 | return res.status(400).json({ error: '企业微信 Webhook URL 不能为空' }); 157 | } 158 | 159 | if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) { 160 | return res.status(400).json({ error: 'Telegram Token 和 Chat ID 不能为空' }); 161 | } 162 | 163 | // 保存配置 164 | const config = await configService.getConfig(); 165 | config.monitoringConfig = { 166 | notificationType, 167 | webhookUrl: webhookUrl || '', 168 | telegramToken: telegramToken || '', 169 | telegramChatId: telegramChatId || '', 170 | monitorInterval: parseInt(monitorInterval) || 60, 171 | isEnabled: !!isEnabled 172 | }; 173 | 174 | await configService.saveConfig(config); 175 | logger.info('监控配置已更新'); 176 | 177 | res.json({ success: true, message: '监控配置已保存' }); 178 | } catch (error) { 179 | logger.error('保存监控配置失败:', error); 180 | res.status(500).json({ error: '保存监控配置失败', details: error.message }); 181 | } 182 | }); 183 | 184 | // 测试通知 185 | router.post('/test-notification', requireLogin, async (req, res) => { 186 | try { 187 | const { notificationType, webhookUrl, telegramToken, telegramChatId } = req.body; 188 | 189 | // 验证参数 190 | if (!notificationType) { 191 | return res.status(400).json({ error: '通知类型不能为空' }); 192 | } 193 | 194 | if (notificationType === 'wechat' && !webhookUrl) { 195 | return res.status(400).json({ error: '企业微信 Webhook URL 不能为空' }); 196 | } 197 | 198 | if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) { 199 | return res.status(400).json({ error: 'Telegram Token 和 Chat ID 不能为空' }); 200 | } 201 | 202 | // 构造测试消息 203 | const testMessage = { 204 | title: '测试通知', 205 | content: `这是一条测试通知消息,发送时间: ${new Date().toLocaleString('zh-CN')}`, 206 | type: 'info' 207 | }; 208 | 209 | // 模拟发送通知 210 | logger.info('发送测试通知:', testMessage); 211 | 212 | // TODO: 实际发送通知的逻辑 213 | // 这里仅做模拟,实际应用中需要实现真正的通知发送逻辑 214 | 215 | // 返回成功 216 | res.json({ success: true, message: '测试通知已发送' }); 217 | } catch (error) { 218 | logger.error('发送测试通知失败:', error); 219 | res.status(500).json({ error: '发送测试通知失败', details: error.message }); 220 | } 221 | }); 222 | 223 | // 导出路由 224 | module.exports = router; -------------------------------------------------------------------------------- /hubcmdui/routes/docker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Docker容器管理路由 3 | */ 4 | const express = require('express'); 5 | const router = express.Router(); 6 | const WebSocket = require('ws'); 7 | const http = require('http'); 8 | const dockerService = require('../services/dockerService'); 9 | const logger = require('../logger'); 10 | const { requireLogin } = require('../middleware/auth'); 11 | 12 | // 获取Docker状态 13 | router.get('/status', requireLogin, async (req, res) => { 14 | try { 15 | const containerStatus = await dockerService.getContainersStatus(); 16 | res.json(containerStatus); 17 | } catch (error) { 18 | logger.error('获取 Docker 状态时出错:', error); 19 | res.status(500).json({ error: '获取 Docker 状态失败', details: error.message }); 20 | } 21 | }); 22 | 23 | // 获取单个容器状态 24 | router.get('/status/:id', requireLogin, async (req, res) => { 25 | try { 26 | const containerInfo = await dockerService.getContainerStatus(req.params.id); 27 | res.json(containerInfo); 28 | } catch (error) { 29 | logger.error('获取容器状态失败:', error); 30 | res.status(500).json({ error: '获取容器状态失败', details: error.message }); 31 | } 32 | }); 33 | 34 | // 重启容器 35 | router.post('/restart/:id', requireLogin, async (req, res) => { 36 | try { 37 | await dockerService.restartContainer(req.params.id); 38 | res.json({ success: true }); 39 | } catch (error) { 40 | logger.error('重启容器失败:', error); 41 | res.status(500).json({ error: '重启容器失败', details: error.message }); 42 | } 43 | }); 44 | 45 | // 停止容器 46 | router.post('/stop/:id', requireLogin, async (req, res) => { 47 | try { 48 | await dockerService.stopContainer(req.params.id); 49 | res.json({ success: true }); 50 | } catch (error) { 51 | logger.error('停止容器失败:', error); 52 | res.status(500).json({ error: '停止容器失败', details: error.message }); 53 | } 54 | }); 55 | 56 | // 删除容器 57 | router.post('/delete/:id', requireLogin, async (req, res) => { 58 | try { 59 | await dockerService.deleteContainer(req.params.id); 60 | res.json({ success: true, message: '容器已成功删除' }); 61 | } catch (error) { 62 | logger.error('删除容器失败:', error); 63 | res.status(500).json({ error: '删除容器失败', details: error.message }); 64 | } 65 | }); 66 | 67 | // 更新容器 68 | router.post('/update/:id', requireLogin, async (req, res) => { 69 | try { 70 | const { tag } = req.body; 71 | await dockerService.updateContainer(req.params.id, tag); 72 | res.json({ success: true, message: '容器更新成功' }); 73 | } catch (error) { 74 | logger.error('更新容器失败:', error); 75 | res.status(500).json({ error: '更新容器失败', details: error.message, stack: error.stack }); 76 | } 77 | }); 78 | 79 | // 获取已停止容器 80 | router.get('/stopped', requireLogin, async (req, res) => { 81 | try { 82 | const stoppedContainers = await dockerService.getStoppedContainers(); 83 | res.json(stoppedContainers); 84 | } catch (error) { 85 | logger.error('获取已停止容器列表失败:', error); 86 | res.status(500).json({ error: '获取已停止容器列表失败', details: error.message }); 87 | } 88 | }); 89 | 90 | // 获取容器日志(HTTP轮询) 91 | router.get('/logs-poll/:id', async (req, res) => { 92 | const { id } = req.params; 93 | try { 94 | const logs = await dockerService.getContainerLogs(id); 95 | res.send(logs); 96 | } catch (error) { 97 | logger.error('获取容器日志失败:', error); 98 | res.status(500).send('获取日志失败'); 99 | } 100 | }); 101 | 102 | // 设置WebSocket路由,用于实时日志流 103 | function setupLogWebsocket(server) { 104 | const wss = new WebSocket.Server({ server }); 105 | 106 | wss.on('connection', async (ws, req) => { 107 | try { 108 | const containerId = req.url.split('/').pop(); 109 | const docker = await dockerService.getDockerConnection(); 110 | 111 | if (!docker) { 112 | ws.send('Error: 无法连接到 Docker 守护进程'); 113 | return; 114 | } 115 | 116 | const container = docker.getContainer(containerId); 117 | const stream = await container.logs({ 118 | follow: true, 119 | stdout: true, 120 | stderr: true, 121 | tail: 100 122 | }); 123 | 124 | stream.on('data', (chunk) => { 125 | const cleanedChunk = chunk.toString('utf8').replace(/\x1B\[[0-9;]*[JKmsu]/g, ''); 126 | // 移除不可打印字符 127 | const printableChunk = cleanedChunk.replace(/[^\x20-\x7E\x0A\x0D]/g, ''); 128 | ws.send(printableChunk); 129 | }); 130 | 131 | ws.on('close', () => { 132 | stream.destroy(); 133 | }); 134 | 135 | stream.on('error', (err) => { 136 | ws.send('Error: ' + err.message); 137 | }); 138 | } catch (err) { 139 | ws.send('Error: ' + err.message); 140 | } 141 | }); 142 | } 143 | 144 | // 直接导出 router 实例,并添加 setupLogWebsocket 作为静态属性 145 | router.setupLogWebsocket = setupLogWebsocket; 146 | module.exports = router; 147 | -------------------------------------------------------------------------------- /hubcmdui/routes/dockerhub.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Docker Hub 代理路由 3 | */ 4 | const express = require('express'); 5 | const router = express.Router(); 6 | const axios = require('axios'); 7 | const logger = require('../logger'); 8 | 9 | // Docker Hub API 基础 URL 10 | const DOCKER_HUB_API = 'https://hub.docker.com/v2'; 11 | 12 | // 搜索镜像 13 | router.get('/search', async (req, res) => { 14 | try { 15 | const { term, page = 1, limit = 25 } = req.query; 16 | 17 | // 确保有搜索关键字 18 | if (!term) { 19 | return res.status(400).json({ error: '缺少搜索关键字(term)' }); 20 | } 21 | 22 | logger.info(`搜索 Docker Hub: 关键字="${term}", 页码=${page}`); 23 | 24 | const response = await axios.get(`${DOCKER_HUB_API}/search/repositories`, { 25 | params: { 26 | query: term, 27 | page, 28 | page_size: limit 29 | }, 30 | timeout: 10000 31 | }); 32 | 33 | res.json(response.data); 34 | } catch (err) { 35 | logger.error('Docker Hub 搜索失败:', err.message); 36 | res.status(500).json({ error: 'Docker Hub 搜索失败', details: err.message }); 37 | } 38 | }); 39 | 40 | // 获取镜像标签 41 | router.get('/tags/:owner/:repo', async (req, res) => { 42 | try { 43 | const { owner, repo } = req.params; 44 | const { page = 1, limit = 25 } = req.query; 45 | 46 | logger.info(`获取镜像标签: ${owner}/${repo}, 页码=${page}`); 47 | 48 | const response = await axios.get( 49 | `${DOCKER_HUB_API}/repositories/${owner}/${repo}/tags`, { 50 | params: { 51 | page, 52 | page_size: limit 53 | }, 54 | timeout: 10000 55 | }); 56 | 57 | res.json(response.data); 58 | } catch (err) { 59 | logger.error('获取 Docker 镜像标签失败:', err.message); 60 | res.status(500).json({ error: '获取镜像标签失败', details: err.message }); 61 | } 62 | }); 63 | 64 | // 直接导出路由实例 65 | module.exports = router; 66 | -------------------------------------------------------------------------------- /hubcmdui/routes/health.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 健康检查路由 3 | */ 4 | const express = require('express'); 5 | const router = express.Router(); 6 | const os = require('os'); 7 | const path = require('path'); 8 | const { version } = require('../package.json'); 9 | 10 | // 简单健康检查 11 | router.get('/', (req, res) => { 12 | res.json({ 13 | status: 'ok', 14 | uptime: process.uptime(), 15 | timestamp: Date.now(), 16 | version 17 | }); 18 | }); 19 | 20 | // 详细系统信息 21 | router.get('/system', (req, res) => { 22 | try { 23 | res.json({ 24 | status: 'ok', 25 | system: { 26 | platform: os.platform(), 27 | release: os.release(), 28 | hostname: os.hostname(), 29 | uptime: os.uptime(), 30 | totalMem: os.totalmem(), 31 | freeMem: os.freemem(), 32 | cpus: os.cpus().length, 33 | loadavg: os.loadavg() 34 | }, 35 | process: { 36 | pid: process.pid, 37 | uptime: process.uptime(), 38 | memoryUsage: process.memoryUsage(), 39 | nodeVersion: process.version, 40 | env: process.env.NODE_ENV || 'development' 41 | } 42 | }); 43 | } catch (err) { 44 | res.status(500).json({ error: '获取系统信息失败', details: err.message }); 45 | } 46 | }); 47 | 48 | module.exports = router; 49 | -------------------------------------------------------------------------------- /hubcmdui/routes/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 路由注册器 3 | * 负责注册所有API路由 4 | */ 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const logger = require('../logger'); 8 | 9 | // 检查文件是否是路由模块 10 | function isRouteModule(file) { 11 | return file.endsWith('.js') && 12 | file !== 'index.js' && 13 | file !== 'routeLoader.js' && 14 | !file.startsWith('_'); 15 | } 16 | 17 | /** 18 | * 注册所有路由 19 | * @param {Express} app - Express应用实例 20 | */ 21 | function registerRoutes(app) { 22 | const routeDir = __dirname; 23 | 24 | try { 25 | const files = fs.readdirSync(routeDir); 26 | const routeFiles = files.filter(isRouteModule); 27 | 28 | logger.info(`发现 ${routeFiles.length} 个路由文件待加载`); 29 | 30 | routeFiles.forEach(file => { 31 | const routeName = path.basename(file, '.js'); 32 | try { 33 | const routePath = path.join(routeDir, file); 34 | const routeExport = require(routePath); // 加载导出的模块 35 | 36 | // 优先处理 { router: routerInstance, ... } 格式 37 | if (routeExport && typeof routeExport === 'object' && routeExport.router && typeof routeExport.router === 'function' && routeExport.router.stack) { 38 | app.use(`/api/${routeName}`, routeExport.router); 39 | logger.info(`✓ 挂载路由对象: /api/${routeName}`); 40 | } 41 | // 处理直接导出 routerInstance 的情况 (更严格的检查) 42 | else if (typeof routeExport === 'function' && routeExport.handle && routeExport.stack) { 43 | app.use(`/api/${routeName}`, routeExport); 44 | logger.info(`✓ 挂载路由: /api/${routeName}`); 45 | } 46 | // 处理导出 { setup: setupFunction } 的情况 47 | else if (routeExport && typeof routeExport === 'object' && routeExport.setup && typeof routeExport.setup === 'function') { 48 | routeExport.setup(app); 49 | logger.info(`✓ 初始化路由: ${routeName}`); 50 | } 51 | // 处理导出 setup 函数 (app) => {} 的情况 52 | else if (typeof routeExport === 'function') { 53 | // 检查是否是 Express Router 实例 (避免重复判断 Case 3) 54 | if (!(routeExport.handle && routeExport.stack)) { 55 | routeExport(app); 56 | logger.info(`✓ 注册路由函数: ${routeName}`); 57 | } else { 58 | logger.warn(`× 路由 ${file} 格式疑似 Router 实例但未被 Case 3 处理,已跳过`); 59 | } 60 | } 61 | // 其他无法识别的格式 62 | else { 63 | logger.warn(`× 路由 ${file} 导出格式无法识别 (${typeof routeExport}),已跳过`); 64 | } 65 | } catch (error) { 66 | logger.error(`× 加载路由 ${file} 失败: ${error.message}`); 67 | // 可以在这里添加更详细的错误堆栈日志 68 | logger.debug(error.stack); 69 | } 70 | }); 71 | 72 | logger.info('所有路由注册完成'); 73 | } catch (error) { 74 | logger.error(`路由注册失败: ${error.message}`); 75 | } 76 | } 77 | 78 | module.exports = registerRoutes; 79 | -------------------------------------------------------------------------------- /hubcmdui/routes/login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 登录路由 3 | */ 4 | const express = require('express'); 5 | const router = express.Router(); 6 | const fs = require('fs').promises; 7 | const path = require('path'); 8 | const crypto = require('crypto'); 9 | const logger = require('../logger'); 10 | 11 | // 生成随机验证码 12 | function generateCaptcha() { 13 | return Math.floor(1000 + Math.random() * 9000).toString(); 14 | } 15 | 16 | // 获取验证码 17 | router.get('/captcha', (req, res) => { 18 | const captcha = generateCaptcha(); 19 | req.session.captcha = captcha; 20 | res.json({ captcha }); 21 | }); 22 | 23 | // 处理登录 24 | router.post('/login', async (req, res) => { 25 | try { 26 | const { username, password, captcha } = req.body; 27 | 28 | // 验证码检查 29 | if (!req.session.captcha || req.session.captcha !== captcha) { 30 | return res.status(401).json({ error: '验证码错误' }); 31 | } 32 | 33 | // 读取用户文件 34 | const userFilePath = path.join(__dirname, '../config/users.json'); 35 | let users; 36 | 37 | try { 38 | const data = await fs.readFile(userFilePath, 'utf8'); 39 | users = JSON.parse(data); 40 | } catch (err) { 41 | logger.error('读取用户文件失败:', err); 42 | return res.status(500).json({ error: '内部服务器错误' }); 43 | } 44 | 45 | // 查找用户 46 | const user = users.find(u => u.username === username); 47 | if (!user) { 48 | return res.status(401).json({ error: '用户名或密码错误' }); 49 | } 50 | 51 | // 验证密码 52 | const hashedPassword = crypto 53 | .createHash('sha256') 54 | .update(password + user.salt) 55 | .digest('hex'); 56 | 57 | if (hashedPassword !== user.password) { 58 | return res.status(401).json({ error: '用户名或密码错误' }); 59 | } 60 | 61 | // 登录成功 62 | req.session.user = { 63 | id: user.id, 64 | username: user.username, 65 | role: user.role, 66 | loginCount: (user.loginCount || 0) + 1, 67 | lastLogin: new Date().toISOString() 68 | }; 69 | 70 | // 更新登录信息 71 | user.loginCount = (user.loginCount || 0) + 1; 72 | user.lastLogin = new Date().toISOString(); 73 | 74 | await fs.writeFile(userFilePath, JSON.stringify(users, null, 2), 'utf8'); 75 | 76 | res.json({ 77 | success: true, 78 | user: { 79 | username: user.username, 80 | role: user.role 81 | } 82 | }); 83 | } catch (err) { 84 | logger.error('登录处理错误:', err); 85 | res.status(500).json({ error: '登录处理失败' }); 86 | } 87 | }); 88 | 89 | logger.success('✓ 登录路由已加载'); 90 | 91 | // 导出路由 92 | module.exports = router; 93 | -------------------------------------------------------------------------------- /hubcmdui/routes/monitoring.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 监控配置路由 3 | */ 4 | const express = require('express'); 5 | const router = express.Router(); 6 | const fs = require('fs').promises; 7 | const path = require('path'); 8 | const { requireLogin } = require('../middleware/auth'); 9 | const logger = require('../logger'); 10 | 11 | // 监控配置文件路径 12 | const CONFIG_FILE = path.join(__dirname, '../config/monitoring.json'); 13 | 14 | // 确保配置文件存在 15 | async function ensureConfigFile() { 16 | try { 17 | await fs.access(CONFIG_FILE); 18 | } catch (err) { 19 | // 文件不存在,创建默认配置 20 | const defaultConfig = { 21 | isEnabled: false, 22 | notificationType: 'wechat', 23 | webhookUrl: '', 24 | telegramToken: '', 25 | telegramChatId: '', 26 | monitorInterval: 60 27 | }; 28 | 29 | await fs.mkdir(path.dirname(CONFIG_FILE), { recursive: true }); 30 | await fs.writeFile(CONFIG_FILE, JSON.stringify(defaultConfig, null, 2), 'utf8'); 31 | return defaultConfig; 32 | } 33 | 34 | // 文件存在,读取配置 35 | const data = await fs.readFile(CONFIG_FILE, 'utf8'); 36 | return JSON.parse(data); 37 | } 38 | 39 | // 获取监控配置 40 | router.get('/monitoring-config', requireLogin, async (req, res) => { 41 | try { 42 | const config = await ensureConfigFile(); 43 | res.json(config); 44 | } catch (err) { 45 | logger.error('获取监控配置失败:', err); 46 | res.status(500).json({ error: '获取监控配置失败' }); 47 | } 48 | }); 49 | 50 | // 保存监控配置 51 | router.post('/monitoring-config', requireLogin, async (req, res) => { 52 | try { 53 | const { 54 | notificationType, 55 | webhookUrl, 56 | telegramToken, 57 | telegramChatId, 58 | monitorInterval, 59 | isEnabled 60 | } = req.body; 61 | 62 | // 简单验证 63 | if (notificationType === 'wechat' && !webhookUrl) { 64 | return res.status(400).json({ error: '企业微信通知需要设置 webhook URL' }); 65 | } 66 | 67 | if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) { 68 | return res.status(400).json({ error: 'Telegram 通知需要设置 Token 和 Chat ID' }); 69 | } 70 | 71 | const config = await ensureConfigFile(); 72 | 73 | // 更新配置 74 | const updatedConfig = { 75 | ...config, 76 | notificationType, 77 | webhookUrl: webhookUrl || '', 78 | telegramToken: telegramToken || '', 79 | telegramChatId: telegramChatId || '', 80 | monitorInterval: parseInt(monitorInterval, 10) || 60, 81 | isEnabled: isEnabled !== undefined ? isEnabled : config.isEnabled 82 | }; 83 | 84 | await fs.writeFile(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2), 'utf8'); 85 | 86 | res.json({ success: true, message: '监控配置已保存' }); 87 | 88 | // 通知监控服务重新加载配置 89 | if (global.monitoringService && typeof global.monitoringService.reload === 'function') { 90 | global.monitoringService.reload(); 91 | } 92 | } catch (err) { 93 | logger.error('保存监控配置失败:', err); 94 | res.status(500).json({ error: '保存监控配置失败' }); 95 | } 96 | }); 97 | 98 | // 切换监控状态 99 | router.post('/toggle-monitoring', requireLogin, async (req, res) => { 100 | try { 101 | const { isEnabled } = req.body; 102 | const config = await ensureConfigFile(); 103 | 104 | config.isEnabled = !!isEnabled; 105 | await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8'); 106 | 107 | res.json({ 108 | success: true, 109 | message: `监控已${isEnabled ? '启用' : '禁用'}` 110 | }); 111 | 112 | // 通知监控服务重新加载配置 113 | if (global.monitoringService && typeof global.monitoringService.reload === 'function') { 114 | global.monitoringService.reload(); 115 | } 116 | } catch (err) { 117 | logger.error('切换监控状态失败:', err); 118 | res.status(500).json({ error: '切换监控状态失败' }); 119 | } 120 | }); 121 | 122 | // 测试通知 123 | router.post('/test-notification', requireLogin, async (req, res) => { 124 | try { 125 | const { 126 | notificationType, 127 | webhookUrl, 128 | telegramToken, 129 | telegramChatId 130 | } = req.body; 131 | 132 | // 简单验证 133 | if (notificationType === 'wechat' && !webhookUrl) { 134 | return res.status(400).json({ error: '企业微信通知需要设置 webhook URL' }); 135 | } 136 | 137 | if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) { 138 | return res.status(400).json({ error: 'Telegram 通知需要设置 Token 和 Chat ID' }); 139 | } 140 | 141 | // 发送测试通知 142 | const notifier = require('../services/notificationService'); 143 | const testMessage = { 144 | title: '测试通知', 145 | content: '这是一条测试通知,如果您收到这条消息,说明您的通知配置工作正常。', 146 | time: new Date().toLocaleString() 147 | }; 148 | 149 | await notifier.sendNotification(testMessage, { 150 | type: notificationType, 151 | webhookUrl, 152 | telegramToken, 153 | telegramChatId 154 | }); 155 | 156 | res.json({ success: true, message: '测试通知已发送' }); 157 | } catch (err) { 158 | logger.error('发送测试通知失败:', err); 159 | res.status(500).json({ error: '发送测试通知失败: ' + err.message }); 160 | } 161 | }); 162 | 163 | // 获取已停止的容器 164 | router.get('/stopped-containers', async (req, res) => { 165 | try { 166 | const dockerService = require('../services/dockerService'); 167 | const containers = await dockerService.getStoppedContainers(); 168 | 169 | res.json(containers); 170 | } catch (err) { 171 | logger.error('获取已停止容器失败:', err); 172 | res.status(500).json({ error: '获取已停止容器失败', details: err.message }); 173 | } 174 | }); 175 | 176 | logger.success('✓ 监控配置路由已加载'); 177 | 178 | // 导出路由 179 | module.exports = router; 180 | -------------------------------------------------------------------------------- /hubcmdui/routes/routeLoader.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const express = require('express'); 4 | const { executeOnce } = require('../lib/initScheduler'); 5 | 6 | // 引入logger 7 | const logger = require('../logger'); 8 | 9 | // 改进路由加载器,确保每个路由只被加载一次 10 | async function loadRoutes(app, customLogger) { 11 | // 使用传入的logger或默认logger 12 | const log = customLogger || logger; 13 | 14 | const routesDir = path.join(__dirname); 15 | const routeFiles = fs.readdirSync(routesDir).filter(file => 16 | file.endsWith('.js') && !file.includes('routeLoader') && !file.includes('index') 17 | ); 18 | 19 | log.info(`发现 ${routeFiles.length} 个路由文件待加载`); 20 | 21 | for (const file of routeFiles) { 22 | const routeName = path.basename(file, '.js'); 23 | 24 | try { 25 | await executeOnce(`loadRoute_${routeName}`, async () => { 26 | const routePath = path.join(routesDir, file); 27 | 28 | // 添加错误处理来避免路由加载失败时导致应用崩溃 29 | try { 30 | const route = require(routePath); 31 | 32 | if (typeof route === 'function') { 33 | route(app); 34 | log.success(`✓ 注册路由: ${routeName}`); 35 | } else if (route && typeof route.router === 'function') { 36 | route.router(app); 37 | log.success(`✓ 注册路由对象: ${routeName}`); 38 | } else { 39 | log.error(`× 路由格式错误: ${file} (应该导出一个函数或router方法)`); 40 | } 41 | } catch (routeError) { 42 | log.error(`× 加载路由 ${file} 失败: ${routeError.message}`); 43 | // 继续加载其他路由,不中断流程 44 | } 45 | }, log); 46 | } catch (error) { 47 | log.error(`× 路由加载流程出错: ${error.message}`); 48 | // 继续处理下一个路由 49 | } 50 | } 51 | 52 | log.info('所有路由注册完成'); 53 | } 54 | 55 | module.exports = loadRoutes; 56 | -------------------------------------------------------------------------------- /hubcmdui/routes/systemStatus.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const os = require('os'); 4 | const logger = require('../logger'); 5 | 6 | // 获取系统状态 7 | router.get('/', (req, res) => { 8 | try { 9 | // 收集系统信息 10 | const cpuLoad = os.loadavg()[0] / os.cpus().length * 100; 11 | const totalMem = os.totalmem(); 12 | const freeMem = os.freemem(); 13 | const usedMem = totalMem - freeMem; 14 | const memoryUsage = `${Math.round(usedMem / totalMem * 100)}%`; 15 | 16 | // 组合结果 17 | const systemStatus = { 18 | dockerAvailable: true, 19 | containerCount: 0, 20 | cpuLoad: `${cpuLoad.toFixed(1)}%`, 21 | memoryUsage: memoryUsage, 22 | diskSpace: '未知', 23 | recentActivities: [] 24 | }; 25 | 26 | res.json(systemStatus); 27 | } catch (error) { 28 | logger.error('获取系统状态失败:', error); 29 | res.status(500).json({ 30 | error: '获取系统状态失败', 31 | details: error.message 32 | }); 33 | } 34 | }); 35 | 36 | // 获取系统资源详情 37 | router.get('/system-resource-details', (req, res) => { 38 | try { 39 | const { type } = req.query; 40 | let data = {}; 41 | 42 | switch(type) { 43 | case 'memory': 44 | const totalMem = os.totalmem(); 45 | const freeMem = os.freemem(); 46 | const usedMem = totalMem - freeMem; 47 | 48 | data = { 49 | totalMemory: formatBytes(totalMem), 50 | usedMemory: formatBytes(usedMem), 51 | freeMemory: formatBytes(freeMem), 52 | memoryUsage: `${Math.round(usedMem / totalMem * 100)}%` 53 | }; 54 | break; 55 | 56 | case 'cpu': 57 | const cpus = os.cpus(); 58 | const loadAvg = os.loadavg(); 59 | 60 | data = { 61 | cpuCores: cpus.length, 62 | cpuModel: cpus[0].model, 63 | cpuLoad: `${(loadAvg[0] / cpus.length * 100).toFixed(1)}%`, 64 | loadAvg1: loadAvg[0].toFixed(2), 65 | loadAvg5: loadAvg[1].toFixed(2), 66 | loadAvg15: loadAvg[2].toFixed(2) 67 | }; 68 | break; 69 | 70 | case 'disk': 71 | // 简单的硬编码数据,在实际环境中应该调用系统命令获取 72 | data = { 73 | totalSpace: '100 GB', 74 | usedSpace: '30 GB', 75 | freeSpace: '70 GB', 76 | diskUsage: '30%' 77 | }; 78 | break; 79 | 80 | default: 81 | return res.status(400).json({ error: '无效的资源类型' }); 82 | } 83 | 84 | res.json(data); 85 | } catch (error) { 86 | logger.error('获取系统资源详情失败:', error); 87 | res.status(500).json({ error: '获取系统资源详情失败', details: error.message }); 88 | } 89 | }); 90 | 91 | // 格式化字节数为可读格式 92 | function formatBytes(bytes, decimals = 2) { 93 | if (bytes === 0) return '0 Bytes'; 94 | 95 | const k = 1024; 96 | const dm = decimals < 0 ? 0 : decimals; 97 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 98 | 99 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 100 | 101 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; 102 | } 103 | 104 | module.exports = router; 105 | -------------------------------------------------------------------------------- /hubcmdui/scripts/diagnostics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 系统诊断工具 - 帮助找出可能存在的问题 3 | */ 4 | const fs = require('fs').promises; 5 | const path = require('path'); 6 | const { execSync } = require('child_process'); 7 | const logger = require('../logger'); 8 | 9 | // 检查所有必要的文件和目录是否存在 10 | async function checkFilesAndDirectories() { 11 | logger.info('开始检查必要的文件和目录...'); 12 | 13 | // 检查必要的目录 14 | const requiredDirs = [ 15 | { path: 'logs', critical: true }, 16 | { path: 'documentation', critical: true }, 17 | { path: 'web/images', critical: true }, 18 | { path: 'routes', critical: true }, 19 | { path: 'services', critical: true }, 20 | { path: 'middleware', critical: true }, 21 | { path: 'scripts', critical: false } 22 | ]; 23 | 24 | const dirsStatus = {}; 25 | for (const dir of requiredDirs) { 26 | const fullPath = path.join(__dirname, '..', dir.path); 27 | try { 28 | await fs.access(fullPath); 29 | dirsStatus[dir.path] = { exists: true, critical: dir.critical }; 30 | logger.info(`目录存在: ${dir.path}`); 31 | } catch (error) { 32 | dirsStatus[dir.path] = { exists: false, critical: dir.critical }; 33 | logger.error(`目录不存在: ${dir.path} (${dir.critical ? '关键' : '非关键'})`); 34 | } 35 | } 36 | 37 | // 检查必要的文件 38 | const requiredFiles = [ 39 | { path: 'server.js', critical: true }, 40 | { path: 'app.js', critical: false }, 41 | { path: 'config.js', critical: true }, 42 | { path: 'logger.js', critical: true }, 43 | { path: 'init-dirs.js', critical: true }, 44 | { path: 'download-images.js', critical: true }, 45 | { path: 'cleanup.js', critical: true }, 46 | { path: 'package.json', critical: true }, 47 | { path: 'web/index.html', critical: true }, 48 | { path: 'web/admin.html', critical: true } 49 | ]; 50 | 51 | const filesStatus = {}; 52 | for (const file of requiredFiles) { 53 | const fullPath = path.join(__dirname, '..', file.path); 54 | try { 55 | await fs.access(fullPath); 56 | filesStatus[file.path] = { exists: true, critical: file.critical }; 57 | logger.info(`文件存在: ${file.path}`); 58 | } catch (error) { 59 | filesStatus[file.path] = { exists: false, critical: file.critical }; 60 | logger.error(`文件不存在: ${file.path} (${file.critical ? '关键' : '非关键'})`); 61 | } 62 | } 63 | 64 | return { directories: dirsStatus, files: filesStatus }; 65 | } 66 | 67 | // 检查Node.js模块依赖 68 | function checkNodeDependencies() { 69 | logger.info('开始检查Node.js依赖...'); 70 | 71 | try { 72 | // 执行npm list --depth=0来检查已安装的依赖 73 | const npmListOutput = execSync('npm list --depth=0', { encoding: 'utf8' }); 74 | logger.info('已安装的依赖:\n' + npmListOutput); 75 | 76 | return { success: true, output: npmListOutput }; 77 | } catch (error) { 78 | logger.error('检查依赖时出错:', error.message); 79 | return { success: false, error: error.message }; 80 | } 81 | } 82 | 83 | // 检查系统环境 84 | async function checkSystemEnvironment() { 85 | logger.info('开始检查系统环境...'); 86 | 87 | const checks = { 88 | node: process.version, 89 | platform: process.platform, 90 | arch: process.arch, 91 | docker: null 92 | }; 93 | 94 | try { 95 | // 检查Docker是否可用 96 | const dockerVersion = execSync('docker --version', { encoding: 'utf8' }); 97 | checks.docker = dockerVersion.trim(); 98 | logger.info(`Docker版本: ${dockerVersion.trim()}`); 99 | } catch (error) { 100 | checks.docker = false; 101 | logger.warn('Docker未安装或不可用'); 102 | } 103 | 104 | return checks; 105 | } 106 | 107 | // 运行诊断 108 | async function runDiagnostics() { 109 | logger.info('======= 开始系统诊断 ======='); 110 | 111 | const results = { 112 | filesAndDirs: await checkFilesAndDirectories(), 113 | dependencies: checkNodeDependencies(), 114 | environment: await checkSystemEnvironment() 115 | }; 116 | 117 | // 检查关键错误 118 | const criticalErrors = []; 119 | 120 | // 检查关键目录 121 | Object.entries(results.filesAndDirs.directories).forEach(([dir, status]) => { 122 | if (status.critical && !status.exists) { 123 | criticalErrors.push(`关键目录丢失: ${dir}`); 124 | } 125 | }); 126 | 127 | // 检查关键文件 128 | Object.entries(results.filesAndDirs.files).forEach(([file, status]) => { 129 | if (status.critical && !status.exists) { 130 | criticalErrors.push(`关键文件丢失: ${file}`); 131 | } 132 | }); 133 | 134 | // 检查依赖 135 | if (!results.dependencies.success) { 136 | criticalErrors.push('依赖检查失败'); 137 | } 138 | 139 | // 总结 140 | logger.info('======= 诊断完成 ======='); 141 | if (criticalErrors.length > 0) { 142 | logger.error('发现关键错误:'); 143 | criticalErrors.forEach(err => logger.error(`- ${err}`)); 144 | logger.error('请解决以上问题后重试'); 145 | } else { 146 | logger.success('未发现关键错误,系统应该可以正常运行'); 147 | } 148 | 149 | return { results, criticalErrors }; 150 | } 151 | 152 | // 直接运行脚本时启动诊断 153 | if (require.main === module) { 154 | runDiagnostics() 155 | .then(() => { 156 | logger.info('诊断完成'); 157 | }) 158 | .catch(error => { 159 | logger.fatal('诊断过程中发生错误:', error); 160 | }); 161 | } 162 | 163 | module.exports = { runDiagnostics }; 164 | -------------------------------------------------------------------------------- /hubcmdui/scripts/init-menu.js: -------------------------------------------------------------------------------- 1 | const MenuItem = require('../models/MenuItem'); 2 | 3 | async function initMenuItems() { 4 | const count = await MenuItem.countDocuments(); 5 | if (count === 0) { 6 | await MenuItem.insertMany([ 7 | { 8 | text: '首页', 9 | link: '/', 10 | icon: 'fa-home', 11 | order: 1 12 | }, 13 | { 14 | text: '文档', 15 | link: '/docs', 16 | icon: 'fa-book', 17 | order: 2 18 | } 19 | // 添加更多默认菜单项... 20 | ]); 21 | console.log('默认菜单项已初始化'); 22 | } 23 | } 24 | 25 | module.exports = initMenuItems; -------------------------------------------------------------------------------- /hubcmdui/scripts/init-system.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 系统初始化脚本 - 首次运行时执行 3 | */ 4 | const fs = require('fs').promises; 5 | const path = require('path'); 6 | const bcrypt = require('bcrypt'); 7 | const { execSync } = require('child_process'); 8 | const logger = require('../logger'); 9 | const { ensureDirectoriesExist } = require('../init-dirs'); 10 | const { downloadImages } = require('../download-images'); 11 | const configService = require('../services/configService'); 12 | 13 | // 用户文件路径 14 | const USERS_FILE = path.join(__dirname, '..', 'users.json'); 15 | 16 | /** 17 | * 创建管理员用户 18 | * @param {string} username 用户名 19 | * @param {string} password 密码 20 | */ 21 | async function createAdminUser(username = 'root', password = 'admin') { 22 | try { 23 | // 检查用户文件是否已存在 24 | try { 25 | await fs.access(USERS_FILE); 26 | logger.info('用户文件已存在,跳过创建管理员用户'); 27 | return; 28 | } catch (err) { 29 | if (err.code !== 'ENOENT') throw err; 30 | } 31 | 32 | // 创建默认管理员用户 33 | const defaultUser = { 34 | username, 35 | password: bcrypt.hashSync(password, 10), 36 | createdAt: new Date().toISOString(), 37 | loginCount: 0, 38 | lastLogin: null 39 | }; 40 | 41 | await fs.writeFile(USERS_FILE, JSON.stringify({ users: [defaultUser] }, null, 2)); 42 | logger.success(`创建默认管理员用户: ${username}/${password}`); 43 | logger.warn('请在首次登录后立即修改默认密码'); 44 | } catch (error) { 45 | logger.error('创建管理员用户失败:', error); 46 | throw error; 47 | } 48 | } 49 | 50 | /** 51 | * 创建默认配置 52 | */ 53 | async function createDefaultConfig() { 54 | try { 55 | // 检查配置是否已存在 56 | const config = await configService.getConfig(); 57 | 58 | // 如果菜单项为空,添加默认菜单项 59 | if (!config.menuItems || config.menuItems.length === 0) { 60 | config.menuItems = [ 61 | { 62 | text: "控制台", 63 | link: "/admin", 64 | newTab: false 65 | }, 66 | { 67 | text: "镜像搜索", 68 | link: "/", 69 | newTab: false 70 | }, 71 | { 72 | text: "文档", 73 | link: "/docs", 74 | newTab: false 75 | }, 76 | { 77 | text: "GitHub", 78 | link: "https://github.com/dqzboy/hubcmdui", 79 | newTab: true 80 | } 81 | ]; 82 | 83 | await configService.saveConfig(config); 84 | logger.success('创建默认菜单配置'); 85 | } 86 | 87 | return config; 88 | } catch (error) { 89 | logger.error('初始化配置失败:', error); 90 | throw error; 91 | } 92 | } 93 | 94 | /** 95 | * 创建示例文档 - 现已禁用 96 | */ 97 | async function createSampleDocumentation() { 98 | logger.info('示例文档创建功能已禁用'); 99 | return; // 不再创建默认文档 100 | 101 | /* 旧代码保留注释,已禁用 102 | const docService = require('../services/documentationService'); 103 | 104 | try { 105 | await docService.ensureDocumentationDir(); 106 | 107 | // 检查是否有现有文档 108 | const docs = await docService.getDocumentationList(); 109 | if (docs && docs.length > 0) { 110 | logger.info('文档已存在,跳过创建示例文档'); 111 | return; 112 | } 113 | 114 | // 创建示例文档 115 | const welcomeDoc = { 116 | title: "欢迎使用 Docker 镜像代理加速系统", 117 | content: `# 欢迎使用 Docker 镜像代理加速系统 118 | 119 | ## 系统简介 120 | 121 | Docker 镜像代理加速系统是一个帮助用户快速搜索、拉取 Docker 镜像的工具。本系统提供了以下功能: 122 | 123 | - 快速搜索 Docker Hub 上的镜像 124 | - 查看镜像的详细信息和标签 125 | - 管理本地 Docker 容器 126 | - 监控容器状态并发送通知 127 | 128 | ## 快速开始 129 | 130 | 1. 在首页搜索框中输入要查找的镜像名称 131 | 2. 点击搜索结果查看详细信息 132 | 3. 使用提供的命令拉取镜像 133 | 134 | ## 管理功能 135 | 136 | 管理员可以通过控制面板管理系统: 137 | 138 | - 查看所有容器状态 139 | - 启动/停止/重启容器 140 | - 更新容器镜像 141 | - 配置监控告警 142 | 143 | 祝您使用愉快! 144 | `, 145 | published: true 146 | }; 147 | 148 | const aboutDoc = { 149 | title: "关于系统", 150 | content: `# 关于 Docker 镜像代理加速系统 151 | 152 | ## 系统版本 153 | 154 | 当前版本: v1.0.0 155 | 156 | ## 技术栈 157 | 158 | - 前端: HTML, CSS, JavaScript 159 | - 后端: Node.js, Express 160 | - 容器: Docker, Dockerode 161 | - 数据存储: 文件系统 162 | 163 | ## 联系方式 164 | 165 | 如有问题,请通过以下方式联系我们: 166 | 167 | - GitHub Issues 168 | - 电子邮件: example@example.com 169 | 170 | ## 许可证 171 | 172 | 本项目采用 MIT 许可证 173 | `, 174 | published: true 175 | }; 176 | 177 | await docService.saveDocument(Date.now().toString(), welcomeDoc.title, welcomeDoc.content); 178 | await docService.saveDocument((Date.now() + 1000).toString(), aboutDoc.title, aboutDoc.content); 179 | 180 | logger.success('创建示例文档成功'); 181 | } catch (error) { 182 | logger.error('创建示例文档失败:', error); 183 | } 184 | */ 185 | } 186 | 187 | /** 188 | * 检查必要依赖 189 | */ 190 | async function checkDependencies() { 191 | try { 192 | logger.info('正在检查系统依赖...'); 193 | 194 | // 检查 Node.js 版本 195 | const nodeVersion = process.version; 196 | const minNodeVersion = 'v14.0.0'; 197 | if (compareVersions(nodeVersion, minNodeVersion) < 0) { 198 | logger.warn(`当前 Node.js 版本 ${nodeVersion} 低于推荐的最低版本 ${minNodeVersion}`); 199 | } else { 200 | logger.success(`Node.js 版本 ${nodeVersion} 满足要求`); 201 | } 202 | 203 | // 检查必要的 npm 包 204 | try { 205 | const packageJson = require('../package.json'); 206 | const requiredDeps = Object.keys(packageJson.dependencies); 207 | 208 | logger.info(`系统依赖共 ${requiredDeps.length} 个包`); 209 | 210 | // 检查是否有 node_modules 目录 211 | try { 212 | await fs.access(path.join(__dirname, '..', 'node_modules')); 213 | } catch (err) { 214 | if (err.code === 'ENOENT') { 215 | logger.warn('未找到 node_modules 目录,请运行 npm install 安装依赖'); 216 | return false; 217 | } 218 | } 219 | } catch (err) { 220 | logger.warn('无法读取 package.json:', err.message); 221 | } 222 | 223 | // 检查 Docker 224 | try { 225 | execSync('docker --version', { stdio: ['ignore', 'ignore', 'ignore'] }); 226 | logger.success('Docker 已安装'); 227 | } catch (err) { 228 | logger.warn('未检测到 Docker,部分功能可能无法正常使用'); 229 | } 230 | 231 | return true; 232 | } catch (error) { 233 | logger.error('依赖检查失败:', error); 234 | return false; 235 | } 236 | } 237 | 238 | /** 239 | * 比较版本号 240 | */ 241 | function compareVersions(v1, v2) { 242 | const v1parts = v1.replace('v', '').split('.'); 243 | const v2parts = v2.replace('v', '').split('.'); 244 | 245 | for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) { 246 | const v1part = parseInt(v1parts[i] || 0); 247 | const v2part = parseInt(v2parts[i] || 0); 248 | 249 | if (v1part > v2part) return 1; 250 | if (v1part < v2part) return -1; 251 | } 252 | 253 | return 0; 254 | } 255 | 256 | /** 257 | * 主初始化函数 258 | */ 259 | async function initialize() { 260 | logger.info('开始系统初始化...'); 261 | 262 | try { 263 | // 1. 检查系统依赖 264 | await checkDependencies(); 265 | 266 | // 2. 确保目录结构存在 267 | await ensureDirectoriesExist(); 268 | logger.success('目录结构初始化完成'); 269 | 270 | // 3. 下载必要图片 271 | await downloadImages(); 272 | 273 | // 4. 创建默认用户 274 | await createAdminUser(); 275 | 276 | // 5. 创建默认配置 277 | await createDefaultConfig(); 278 | 279 | // 6. 创建示例文档 280 | await createSampleDocumentation(); 281 | 282 | logger.success('系统初始化完成!'); 283 | // 移除敏感的账户信息日志 284 | logger.warn('首次登录后请立即修改默认密码!'); 285 | 286 | return { success: true }; 287 | } catch (error) { 288 | logger.error('系统初始化失败:', error); 289 | return { success: false, error: error.message }; 290 | } 291 | } 292 | 293 | // 如果直接运行此脚本 294 | if (require.main === module) { 295 | initialize() 296 | .then((result) => { 297 | if (result.success) { 298 | process.exit(0); 299 | } else { 300 | process.exit(1); 301 | } 302 | }) 303 | .catch((error) => { 304 | logger.fatal('初始化过程中发生错误:', error); 305 | process.exit(1); 306 | }); 307 | } 308 | 309 | module.exports = { 310 | initialize, 311 | createAdminUser, 312 | createDefaultConfig, 313 | createSampleDocumentation, 314 | checkDependencies 315 | }; 316 | -------------------------------------------------------------------------------- /hubcmdui/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Docker 镜像代理加速系统 - 服务器入口点 3 | */ 4 | const express = require('express'); 5 | const fs = require('fs').promises; 6 | const path = require('path'); 7 | const bodyParser = require('body-parser'); 8 | const session = require('express-session'); 9 | const cors = require('cors'); 10 | const http = require('http'); 11 | const logger = require('./logger'); 12 | const { ensureDirectoriesExist } = require('./init-dirs'); 13 | const { downloadImages } = require('./download-images'); 14 | const { gracefulShutdown } = require('./cleanup'); 15 | const os = require('os'); 16 | const { requireLogin } = require('./middleware/auth'); 17 | const compatibilityLayer = require('./compatibility-layer'); 18 | const initSystem = require('./scripts/init-system'); 19 | 20 | // 设置日志级别 (默认INFO, 可通过环境变量设置) 21 | const logLevel = process.env.LOG_LEVEL || 'WARN'; 22 | logger.setLogLevel(logLevel); 23 | logger.info(`日志级别已设置为: ${logLevel}`); 24 | 25 | // 导入配置 26 | const config = require('./config'); 27 | 28 | // 导入中间件 29 | const { sessionActivity, sanitizeRequestBody, securityHeaders } = require('./middleware/auth'); 30 | 31 | // 导入初始化调度器 32 | const { executeOnce } = require('./lib/initScheduler'); 33 | 34 | // 初始化Express应用 35 | const app = express(); 36 | const server = http.createServer(app); 37 | 38 | // 配置中间件 39 | app.use(cors()); 40 | app.use(express.json()); 41 | app.use(express.static('web')); 42 | app.use(bodyParser.urlencoded({ extended: true })); 43 | app.use(session({ 44 | secret: config.sessionSecret || 'OhTq3faqSKoxbV%NJV', 45 | resave: true, 46 | saveUninitialized: true, 47 | cookie: { 48 | secure: config.secureSession || false, 49 | maxAge: 7 * 24 * 60 * 60 * 1000 // 7天(一周) 50 | } 51 | })); 52 | 53 | // 自定义中间件 54 | app.use(sessionActivity); 55 | app.use(sanitizeRequestBody); 56 | app.use(securityHeaders); 57 | 58 | // 请求日志中间件 59 | app.use((req, res, next) => { 60 | const start = Date.now(); 61 | 62 | // 在响应完成后记录日志 63 | res.on('finish', () => { 64 | const duration = Date.now() - start; 65 | 66 | // 增强过滤条件 67 | const isSuccessfulGet = req.method === 'GET' && (res.statusCode === 200 || res.statusCode === 304); 68 | const isStaticResource = req.url.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i); 69 | const isCommonApiRequest = req.url.startsWith('/api/') && 70 | (req.url.includes('/check-session') || 71 | req.url.includes('/system-resources') || 72 | req.url.includes('/docker/status')); 73 | const isErrorResponse = res.statusCode >= 400; 74 | 75 | // 只记录关键API请求和错误响应,过滤普通的API请求和静态资源 76 | if ((isErrorResponse || 77 | (req.url.startsWith('/api/') && !isCommonApiRequest)) && 78 | !isStaticResource && 79 | !(isSuccessfulGet && isCommonApiRequest)) { 80 | 81 | // 记录简化的请求信息 82 | req.skipDetailedLogging = !isErrorResponse; // 非错误请求跳过详细日志 83 | logger.request(req, res, duration); 84 | } 85 | }); 86 | 87 | next(); 88 | }); 89 | 90 | // 使用我们的路由注册函数加载所有路由 91 | logger.info('注册所有应用路由...'); 92 | const registerRoutes = require('./routes'); 93 | registerRoutes(app); 94 | 95 | // 提供兼容层以确保旧接口继续工作 96 | require('./compatibility-layer')(app); 97 | 98 | // 确保登录路由可用 99 | try { 100 | const loginRouter = require('./routes/login'); 101 | app.use('/api', loginRouter); 102 | logger.success('✓ 已添加备用登录路由'); 103 | } catch (loginError) { 104 | logger.error('无法加载备用登录路由:', loginError); 105 | } 106 | 107 | // 页面路由 108 | app.get('/', (req, res) => { 109 | res.sendFile(path.join(__dirname, 'web', 'index.html')); 110 | }); 111 | 112 | app.get('/admin', (req, res) => { 113 | res.sendFile(path.join(__dirname, 'web', 'admin.html')); 114 | }); 115 | 116 | app.get('/docs', (req, res) => { 117 | res.sendFile(path.join(__dirname, 'web', 'docs.html')); 118 | }); 119 | 120 | // 废弃的登录页面路由 - 该路由未使用且导致404错误,现已移除 121 | // app.get('/login', (req, res) => { 122 | // // 检查用户是否已登录 123 | // if (req.session && req.session.user) { 124 | // return res.redirect('/admin'); // 已登录用户重定向到管理页面 125 | // } 126 | // 127 | // res.sendFile(path.join(__dirname, 'web', 'login.html')); 128 | // }); 129 | 130 | // 404处理 131 | app.use((req, res) => { 132 | res.status(404).json({ error: '请求的资源不存在' }); 133 | }); 134 | 135 | // 错误处理中间件 136 | app.use((err, req, res, next) => { 137 | logger.error('应用错误:', err); 138 | res.status(500).json({ error: '服务器内部错误', details: err.message }); 139 | }); 140 | 141 | // 启动服务器 142 | const PORT = process.env.PORT || 3000; 143 | 144 | async function startServer() { 145 | server.listen(PORT, async () => { 146 | logger.info(`服务器已启动并监听端口 ${PORT}`); 147 | 148 | try { 149 | // 确保目录存在 150 | await ensureDirectoriesExist(); 151 | logger.success('系统目录初始化完成'); 152 | 153 | // 下载必要资源 154 | await downloadImages(); 155 | logger.success('资源下载完成'); 156 | 157 | // 初始化系统 158 | try { 159 | const { initialize } = require('./scripts/init-system'); 160 | await initialize(); 161 | logger.success('系统初始化完成'); 162 | } catch (initError) { 163 | logger.warn('系统初始化遇到问题:', initError.message); 164 | logger.warn('某些功能可能无法正常工作'); 165 | } 166 | 167 | // 尝试启动监控 168 | try { 169 | const monitoringService = require('./services/monitoringService'); 170 | await monitoringService.startMonitoring(); 171 | logger.success('监控服务已启动'); 172 | } catch (monitoringError) { 173 | logger.warn('监控服务启动失败:', monitoringError.message); 174 | logger.warn('监控功能可能不可用'); 175 | } 176 | 177 | // 尝试设置WebSocket 178 | try { 179 | const dockerRouter = require('./routes/docker'); 180 | if (typeof dockerRouter.setupLogWebsocket === 'function') { 181 | dockerRouter.setupLogWebsocket(server); 182 | logger.success('WebSocket服务已启动'); 183 | } 184 | } catch (wsError) { 185 | logger.warn('WebSocket服务启动失败:', wsError.message); 186 | logger.warn('容器日志实时流可能不可用'); 187 | } 188 | 189 | logger.success('服务器初始化完成,系统已准备就绪'); 190 | } catch (error) { 191 | logger.error('系统初始化失败,但服务仍将继续运行:', error); 192 | } 193 | }); 194 | } 195 | 196 | startServer(); 197 | 198 | // 处理进程终止信号 199 | process.on('SIGINT', gracefulShutdown); 200 | process.on('SIGTERM', gracefulShutdown); 201 | 202 | // 捕获未处理的Promise拒绝和未捕获的异常 203 | process.on('unhandledRejection', (reason, promise) => { 204 | logger.error('未处理的Promise拒绝:', reason); 205 | if (reason instanceof Error) { 206 | logger.debug('拒绝原因堆栈:', reason.stack); 207 | } 208 | }); 209 | 210 | process.on('uncaughtException', (error) => { 211 | logger.error('未捕获的异常:', error); 212 | logger.error('错误堆栈:', error.stack); 213 | // 给日志一些时间写入后退出 214 | setTimeout(() => { 215 | logger.fatal('由于未捕获的异常,系统将在3秒后退出'); 216 | setTimeout(() => process.exit(1), 3000); 217 | }, 1000); 218 | }); 219 | 220 | // 导出服务器对象以供测试使用 221 | module.exports = server; -------------------------------------------------------------------------------- /hubcmdui/services/configService.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const path = require('path'); 3 | const logger = require('../logger'); 4 | 5 | const CONFIG_FILE = path.join(__dirname, '../config.json'); 6 | const DEFAULT_CONFIG = { 7 | theme: 'light', 8 | language: 'zh_CN', 9 | notifications: true, 10 | autoRefresh: true, 11 | refreshInterval: 30000, 12 | dockerHost: 'localhost', 13 | dockerPort: 2375, 14 | useHttps: false 15 | }; 16 | 17 | async function ensureConfigFile() { 18 | try { 19 | await fs.access(CONFIG_FILE); 20 | } catch (error) { 21 | if (error.code === 'ENOENT') { 22 | await fs.writeFile(CONFIG_FILE, JSON.stringify(DEFAULT_CONFIG, null, 2)); 23 | } else { 24 | throw error; 25 | } 26 | } 27 | } 28 | 29 | async function getConfig() { 30 | try { 31 | await ensureConfigFile(); 32 | const data = await fs.readFile(CONFIG_FILE, 'utf8'); 33 | return JSON.parse(data); 34 | } catch (error) { 35 | logger.error('读取配置文件失败:', error); 36 | return { ...DEFAULT_CONFIG, error: true }; 37 | } 38 | } 39 | 40 | module.exports = { 41 | getConfig, 42 | saveConfig: async (config) => { 43 | await ensureConfigFile(); 44 | await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2)); 45 | }, 46 | DEFAULT_CONFIG 47 | }; 48 | -------------------------------------------------------------------------------- /hubcmdui/services/dockerHubService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Docker Hub 服务模块 3 | */ 4 | const axios = require('axios'); 5 | const logger = require('../logger'); 6 | const pLimit = require('p-limit'); 7 | const axiosRetry = require('axios-retry'); 8 | 9 | // 配置并发限制,最多5个并发请求 10 | const limit = pLimit(5); 11 | 12 | // 优化HTTP请求配置 13 | const httpOptions = { 14 | timeout: 15000, // 15秒超时 15 | headers: { 16 | 'User-Agent': 'DockerHubSearchClient/1.0', 17 | 'Accept': 'application/json' 18 | } 19 | }; 20 | 21 | // 配置Axios重试 22 | axiosRetry(axios, { 23 | retries: 3, // 最多重试3次 24 | retryDelay: (retryCount) => { 25 | console.log(`[INFO] 重试 Docker Hub 请求 (${retryCount}/3)`); 26 | return retryCount * 1000; // 重试延迟,每次递增1秒 27 | }, 28 | retryCondition: (error) => { 29 | // 只在网络错误或5xx响应时重试 30 | return axiosRetry.isNetworkOrIdempotentRequestError(error) || 31 | (error.response && error.response.status >= 500); 32 | } 33 | }); 34 | 35 | // 搜索仓库 36 | async function searchRepositories(term, page = 1, requestCache = null) { 37 | const cacheKey = `search_${term}_${page}`; 38 | let cachedResult = null; 39 | 40 | // 安全地检查缓存 41 | if (requestCache && typeof requestCache.get === 'function') { 42 | cachedResult = requestCache.get(cacheKey); 43 | } 44 | 45 | if (cachedResult) { 46 | console.log(`[INFO] 返回缓存的搜索结果: ${term} (页码: ${page})`); 47 | return cachedResult; 48 | } 49 | 50 | console.log(`[INFO] 搜索Docker Hub: ${term} (页码: ${page})`); 51 | 52 | try { 53 | // 使用更安全的直接请求方式,避免pLimit可能的问题 54 | const url = `https://hub.docker.com/v2/search/repositories/?query=${encodeURIComponent(term)}&page=${page}&page_size=25`; 55 | const response = await axios.get(url, httpOptions); 56 | const result = response.data; 57 | 58 | // 将结果缓存(如果缓存对象可用) 59 | if (requestCache && typeof requestCache.set === 'function') { 60 | requestCache.set(cacheKey, result); 61 | } 62 | 63 | return result; 64 | } catch (error) { 65 | logger.error('搜索Docker Hub失败:', error.message); 66 | // 重新抛出错误以便上层处理 67 | throw new Error(error.message || '搜索Docker Hub失败'); 68 | } 69 | } 70 | 71 | // 获取所有标签 72 | async function getAllTags(imageName, isOfficial) { 73 | const fullImageName = isOfficial ? `library/${imageName}` : imageName; 74 | logger.info(`获取所有镜像标签: ${fullImageName}`); 75 | 76 | // 为所有标签请求设置超时限制 77 | const allTagsPromise = fetchAllTags(fullImageName); 78 | const timeoutPromise = new Promise((_, reject) => 79 | setTimeout(() => reject(new Error('获取所有标签超时')), 30000) 80 | ); 81 | 82 | try { 83 | // 使用Promise.race确保请求不会无限等待 84 | const allTags = await Promise.race([allTagsPromise, timeoutPromise]); 85 | 86 | // 过滤掉无效平台信息 87 | const cleanedTags = allTags.map(tag => { 88 | if (tag.images && Array.isArray(tag.images)) { 89 | tag.images = tag.images.filter(img => !(img.os === 'unknown' && img.architecture === 'unknown')); 90 | } 91 | return tag; 92 | }); 93 | 94 | return { 95 | count: cleanedTags.length, 96 | results: cleanedTags, 97 | all_pages_loaded: true 98 | }; 99 | } catch (error) { 100 | logger.error(`获取所有标签失败: ${error.message}`); 101 | throw error; 102 | } 103 | } 104 | 105 | // 获取特定页的标签 106 | async function getTagsByPage(imageName, isOfficial, page, pageSize) { 107 | const fullImageName = isOfficial ? `library/${imageName}` : imageName; 108 | logger.info(`获取镜像标签: ${fullImageName}, 页码: ${page}, 页面大小: ${pageSize}`); 109 | 110 | const tagsUrl = `https://hub.docker.com/v2/repositories/${fullImageName}/tags?page=${page}&page_size=${pageSize}`; 111 | 112 | try { 113 | const tagsResponse = await axios.get(tagsUrl, { 114 | timeout: 15000, 115 | headers: { 116 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36' 117 | } 118 | }); 119 | 120 | // 检查响应数据有效性 121 | if (!tagsResponse.data || typeof tagsResponse.data !== 'object') { 122 | logger.warn(`镜像 ${fullImageName} 返回的数据格式不正确`); 123 | return { count: 0, results: [] }; 124 | } 125 | 126 | if (!tagsResponse.data.results || !Array.isArray(tagsResponse.data.results)) { 127 | logger.warn(`镜像 ${fullImageName} 没有返回有效的标签数据`); 128 | return { count: 0, results: [] }; 129 | } 130 | 131 | // 过滤掉无效平台信息 132 | const cleanedResults = tagsResponse.data.results.map(tag => { 133 | if (tag.images && Array.isArray(tag.images)) { 134 | tag.images = tag.images.filter(img => !(img.os === 'unknown' && img.architecture === 'unknown')); 135 | } 136 | return tag; 137 | }); 138 | 139 | return { 140 | ...tagsResponse.data, 141 | results: cleanedResults 142 | }; 143 | } catch (error) { 144 | logger.error(`获取标签列表失败: ${error.message}`, { 145 | url: tagsUrl, 146 | status: error.response?.status, 147 | statusText: error.response?.statusText 148 | }); 149 | throw error; 150 | } 151 | } 152 | 153 | // 获取标签数量 154 | async function getTagCount(name, isOfficial, requestCache) { 155 | const cacheKey = `tag_count_${name}_${isOfficial}`; 156 | const cachedResult = requestCache?.get(cacheKey); 157 | 158 | if (cachedResult) { 159 | console.log(`[INFO] 返回缓存的标签计数: ${name}`); 160 | return cachedResult; 161 | } 162 | 163 | const fullImageName = isOfficial ? `library/${name}` : name; 164 | const apiUrl = `https://hub.docker.com/v2/repositories/${fullImageName}/tags/?page_size=1`; 165 | 166 | try { 167 | const result = await limit(async () => { 168 | const response = await axios.get(apiUrl, httpOptions); 169 | return { 170 | count: response.data.count, 171 | recommended_mode: response.data.count > 500 ? 'paginated' : 'full' 172 | }; 173 | }); 174 | 175 | if (requestCache) { 176 | requestCache.set(cacheKey, result); 177 | } 178 | 179 | return result; 180 | } catch (error) { 181 | throw error; 182 | } 183 | } 184 | 185 | // 递归获取所有标签 186 | async function fetchAllTags(fullImageName, page = 1, allTags = [], maxPages = 10) { 187 | if (page > maxPages) { 188 | logger.warn(`达到最大页数限制 (${maxPages}),停止获取更多标签`); 189 | return allTags; 190 | } 191 | 192 | const pageSize = 100; // 使用最大页面大小 193 | const url = `https://hub.docker.com/v2/repositories/${fullImageName}/tags?page=${page}&page_size=${pageSize}`; 194 | 195 | try { 196 | logger.info(`获取标签页 ${page}/${maxPages}...`); 197 | 198 | const response = await axios.get(url, { 199 | timeout: 10000, 200 | headers: { 201 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36' 202 | } 203 | }); 204 | 205 | if (!response.data.results || !Array.isArray(response.data.results)) { 206 | logger.warn(`页 ${page} 没有有效的标签数据`); 207 | return allTags; 208 | } 209 | 210 | allTags.push(...response.data.results); 211 | logger.info(`已获取 ${allTags.length}/${response.data.count || 'unknown'} 个标签`); 212 | 213 | // 检查是否有下一页 214 | if (response.data.next && allTags.length < response.data.count) { 215 | // 添加一些延迟以避免请求过快 216 | await new Promise(resolve => setTimeout(resolve, 500)); 217 | return fetchAllTags(fullImageName, page + 1, allTags, maxPages); 218 | } 219 | 220 | logger.success(`成功获取所有 ${allTags.length} 个标签`); 221 | return allTags; 222 | } catch (error) { 223 | logger.error(`递归获取标签失败 (页码 ${page}): ${error.message}`); 224 | 225 | // 如果已经获取了一些标签,返回这些标签而不是抛出错误 226 | if (allTags.length > 0) { 227 | return allTags; 228 | } 229 | 230 | // 如果没有获取到任何标签,则抛出错误 231 | throw error; 232 | } 233 | } 234 | 235 | // 统一的错误处理函数 236 | function handleAxiosError(error, res, message) { 237 | let errorDetails = ''; 238 | 239 | if (error.response) { 240 | // 服务器响应错误的错误处理函数 241 | const status = error.response.status; 242 | errorDetails = `状态码: ${status}`; 243 | 244 | if (error.response.data && error.response.data.message) { 245 | errorDetails += `, 信息: ${error.response.data.message}`; 246 | } 247 | 248 | console.error(`[ERROR] ${message}: ${errorDetails}`); 249 | 250 | res.status(status).json({ 251 | error: `${message} (${errorDetails})`, 252 | details: error.response.data 253 | }); 254 | } else if (error.request) { 255 | // 请求已发送但没有收到响应 256 | if (error.code === 'ECONNRESET') { 257 | errorDetails = '连接被重置,这可能是由于网络不稳定或服务端断开连接'; 258 | } else if (error.code === 'ECONNABORTED') { 259 | errorDetails = '请求超时,服务器响应时间过长'; 260 | } else { 261 | errorDetails = `${error.code || '未知错误代码'}: ${error.message}`; 262 | } 263 | 264 | console.error(`[ERROR] ${message}: ${errorDetails}`); 265 | 266 | res.status(503).json({ 267 | error: `${message} (${errorDetails})`, 268 | retryable: true 269 | }); 270 | } else { 271 | // 其他错误 272 | errorDetails = error.message; 273 | console.error(`[ERROR] ${message}: ${errorDetails}`); 274 | console.error(`[ERROR] 错误堆栈: ${error.stack}`); 275 | 276 | res.status(500).json({ 277 | error: `${message} (${errorDetails})`, 278 | retryable: true 279 | }); 280 | } 281 | } 282 | 283 | module.exports = { 284 | searchRepositories, 285 | getAllTags, 286 | getTagsByPage, 287 | getTagCount, 288 | fetchAllTags, 289 | handleAxiosError 290 | }; 291 | -------------------------------------------------------------------------------- /hubcmdui/services/networkService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 网络服务 - 提供网络诊断功能 3 | */ 4 | const { exec } = require('child_process'); 5 | const { promisify } = require('util'); 6 | const logger = require('../logger'); 7 | 8 | const execAsync = promisify(exec); 9 | 10 | /** 11 | * 执行网络测试 12 | * @param {string} type 测试类型 ('ping' 或 'traceroute') 13 | * @param {string} domain 目标域名 14 | * @returns {Promise} 测试结果 15 | */ 16 | async function performNetworkTest(type, domain) { 17 | // 验证输入 18 | if (!domain || !domain.match(/^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)) { 19 | throw new Error('无效的域名格式'); 20 | } 21 | 22 | if (!type || !['ping', 'traceroute'].includes(type)) { 23 | throw new Error('无效的测试类型'); 24 | } 25 | 26 | try { 27 | // 根据测试类型构建命令 28 | const command = type === 'ping' 29 | ? `ping -c 4 ${domain}` 30 | : `traceroute -m 10 ${domain}`; 31 | 32 | logger.info(`执行网络测试: ${command}`); 33 | 34 | // 执行命令并获取结果 35 | const { stdout, stderr } = await execAsync(command, { timeout: 30000 }); 36 | return stdout || stderr; 37 | } catch (error) { 38 | logger.error(`网络测试失败: ${error.message}`); 39 | 40 | // 如果命令被终止,表示超时 41 | if (error.killed) { 42 | throw new Error('测试超时'); 43 | } 44 | 45 | // 其他错误 46 | throw error; 47 | } 48 | } 49 | 50 | module.exports = { 51 | performNetworkTest 52 | }; 53 | -------------------------------------------------------------------------------- /hubcmdui/services/notificationService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 通知服务 3 | * 用于发送各种类型的通知 4 | */ 5 | const axios = require('axios'); 6 | const logger = require('../logger'); 7 | 8 | /** 9 | * 发送通知 10 | * @param {Object} message - 消息对象,包含标题、内容等 11 | * @param {Object} config - 配置对象,包含通知类型和相关配置 12 | * @returns {Promise} 13 | */ 14 | async function sendNotification(message, config) { 15 | const { type } = config; 16 | 17 | switch (type) { 18 | case 'wechat': 19 | return sendWechatNotification(message, config); 20 | case 'telegram': 21 | return sendTelegramNotification(message, config); 22 | default: 23 | throw new Error(`不支持的通知类型: ${type}`); 24 | } 25 | } 26 | 27 | /** 28 | * 发送企业微信通知 29 | * @param {Object} message - 消息对象 30 | * @param {Object} config - 配置对象 31 | * @returns {Promise} 32 | */ 33 | async function sendWechatNotification(message, config) { 34 | const { webhookUrl } = config; 35 | 36 | if (!webhookUrl) { 37 | throw new Error('企业微信 Webhook URL 未配置'); 38 | } 39 | 40 | const payload = { 41 | msgtype: 'markdown', 42 | markdown: { 43 | content: `## ${message.title}\n${message.content}\n> ${message.time}` 44 | } 45 | }; 46 | 47 | try { 48 | const response = await axios.post(webhookUrl, payload, { 49 | headers: { 'Content-Type': 'application/json' }, 50 | timeout: 5000 51 | }); 52 | 53 | if (response.status !== 200 || response.data.errcode !== 0) { 54 | throw new Error(`企业微信返回错误: ${response.data.errmsg || '未知错误'}`); 55 | } 56 | 57 | logger.info('企业微信通知发送成功'); 58 | } catch (error) { 59 | logger.error('企业微信通知发送失败:', error); 60 | throw new Error(`企业微信通知发送失败: ${error.message}`); 61 | } 62 | } 63 | 64 | /** 65 | * 发送Telegram通知 66 | * @param {Object} message - 消息对象 67 | * @param {Object} config - 配置对象 68 | * @returns {Promise} 69 | */ 70 | async function sendTelegramNotification(message, config) { 71 | const { telegramToken, telegramChatId } = config; 72 | 73 | if (!telegramToken || !telegramChatId) { 74 | throw new Error('Telegram Token 或 Chat ID 未配置'); 75 | } 76 | 77 | const text = `*${message.title}*\n\n${message.content}\n\n_${message.time}_`; 78 | const url = `https://api.telegram.org/bot${telegramToken}/sendMessage`; 79 | 80 | try { 81 | const response = await axios.post(url, { 82 | chat_id: telegramChatId, 83 | text: text, 84 | parse_mode: 'Markdown' 85 | }, { 86 | headers: { 'Content-Type': 'application/json' }, 87 | timeout: 5000 88 | }); 89 | 90 | if (response.status !== 200 || !response.data.ok) { 91 | throw new Error(`Telegram 返回错误: ${response.data.description || '未知错误'}`); 92 | } 93 | 94 | logger.info('Telegram 通知发送成功'); 95 | } catch (error) { 96 | logger.error('Telegram 通知发送失败:', error); 97 | throw new Error(`Telegram 通知发送失败: ${error.message}`); 98 | } 99 | } 100 | 101 | module.exports = { 102 | sendNotification 103 | }; 104 | -------------------------------------------------------------------------------- /hubcmdui/services/systemService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 系统服务模块 - 处理系统级信息获取 3 | * 使用 systeminformation 库来提供跨平台的系统数据 4 | */ 5 | const si = require('systeminformation'); 6 | const logger = require('../logger'); 7 | const os = require('os'); // os模块仍可用于某些特定情况或日志记录 8 | 9 | // Helper function to format bytes into a more readable format 10 | function formatBytes(bytes, decimals = 2) { 11 | if (bytes === null || bytes === undefined || isNaN(bytes) || bytes === 0) return '0 Bytes'; 12 | const k = 1024; 13 | const dm = decimals < 0 ? 0 : decimals; 14 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 15 | try { 16 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 17 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; 18 | } catch (e) { 19 | return 'N/A'; // In case of Math.log error with very small numbers etc. 20 | } 21 | } 22 | 23 | // 获取核心系统资源信息 (CPU, Memory, Disk) 24 | async function getSystemResources() { 25 | try { 26 | logger.info('Fetching system resources using systeminformation...'); 27 | 28 | // 并行获取数据以提高效率 29 | const [cpuInfo, memInfo, fsInfo, cpuLoadInfo, osInfo] = await Promise.all([ 30 | si.cpu(), // For CPU model, cores, speed 31 | si.mem(), // For memory details 32 | si.fsSize(), // For filesystem details 33 | si.currentLoad(), // For current CPU load percentage and per-core load 34 | si.osInfo() // For OS type, mainly for specific disk selection if needed 35 | ]); 36 | 37 | // --- CPU 信息处理 --- 38 | let cpuUsage = parseFloat(cpuLoadInfo.currentLoad.toFixed(1)); 39 | // Fallback if currentLoad is not a number (very unlikely with systeminformation) 40 | if (isNaN(cpuUsage) && Array.isArray(cpuLoadInfo.cpus) && cpuLoadInfo.cpus.length > 0) { 41 | // Calculate average from per-core loads if overall isn't good 42 | const totalLoad = cpuLoadInfo.cpus.reduce((acc, core) => acc + core.load, 0); 43 | cpuUsage = parseFloat((totalLoad / cpuLoadInfo.cpus.length).toFixed(1)); 44 | } 45 | if (isNaN(cpuUsage)) cpuUsage = null; // Final fallback to null if still NaN 46 | 47 | const cpuData = { 48 | cores: cpuInfo.cores, 49 | physicalCores: cpuInfo.physicalCores, 50 | model: cpuInfo.manufacturer + ' ' + cpuInfo.brand, 51 | speed: cpuInfo.speed, // in GHz 52 | usage: cpuUsage, // Overall CPU usage percentage 53 | loadAvg: osInfo.platform !== 'win32' ? os.loadavg().map(load => parseFloat(load.toFixed(1))) : null // os.loadavg() is not for Windows 54 | }; 55 | 56 | // --- 内存信息处理 --- 57 | // systeminformation already provides these in bytes 58 | const memData = { 59 | total: memInfo.total, 60 | free: memInfo.free, // Truly free 61 | used: memInfo.used, // total - free (includes buff/cache on Linux, PhysMem used on macOS) 62 | active: memInfo.active, // More representative of app-used memory 63 | available: memInfo.available, // Memory available to applications (often free + reclaimable buff/cache) 64 | wired: memInfo.wired, // macOS specific: memory that cannot be paged out 65 | // compressed: memInfo.compressed, // macOS specific, if systeminformation lib provides it directly 66 | buffcache: memInfo.buffcache // Linux specific: buffer and cache 67 | }; 68 | 69 | // --- 磁盘信息处理 --- 70 | // Find the primary disk (e.g., mounted on '/' for Linux/macOS, or C: for Windows) 71 | let mainDiskInfo = null; 72 | if (osInfo.platform === 'win32') { 73 | mainDiskInfo = fsInfo.find(d => d.fs.startsWith('C:')); 74 | } else { 75 | mainDiskInfo = fsInfo.find(d => d.mount === '/'); 76 | } 77 | if (!mainDiskInfo && fsInfo.length > 0) { 78 | // Fallback to the first disk if the standard one isn't found 79 | mainDiskInfo = fsInfo[0]; 80 | } 81 | 82 | const diskData = mainDiskInfo ? { 83 | mount: mainDiskInfo.mount, 84 | size: formatBytes(mainDiskInfo.size), 85 | used: formatBytes(mainDiskInfo.used), 86 | available: formatBytes(mainDiskInfo.available), // systeminformation provides 'available' 87 | percent: mainDiskInfo.use !== null && mainDiskInfo.use !== undefined ? mainDiskInfo.use.toFixed(0) + '%' : 'N/A' 88 | } : { 89 | mount: 'N/A', 90 | size: 'N/A', 91 | used: 'N/A', 92 | available: 'N/A', 93 | percent: 'N/A' 94 | }; 95 | 96 | const resources = { 97 | osType: osInfo.platform, // e.g., 'darwin', 'linux', 'win32' 98 | osDistro: osInfo.distro, 99 | cpu: cpuData, 100 | memory: memData, 101 | disk: diskData 102 | }; 103 | logger.info('Successfully fetched system resources:', /* JSON.stringify(resources, null, 2) */ resources.osType); 104 | return resources; 105 | 106 | } catch (error) { 107 | logger.error('获取系统资源失败 (services/systemService.js):', error); 108 | // Return a structured error object or rethrow, 109 | // so the API route can send an appropriate HTTP error 110 | throw new Error(`Failed to get system resources: ${error.message}`); 111 | } 112 | } 113 | 114 | module.exports = { 115 | getSystemResources 116 | // getDiskSpace, if it was previously exported and used by another route, can be kept 117 | // or removed if getSystemResources now covers all disk info needs for /api/system-resources 118 | }; 119 | -------------------------------------------------------------------------------- /hubcmdui/services/userService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 用户服务模块 3 | */ 4 | const fs = require('fs').promises; 5 | const path = require('path'); 6 | const bcrypt = require('bcrypt'); 7 | const logger = require('../logger'); 8 | 9 | const USERS_FILE = path.join(__dirname, '..', 'users.json'); 10 | 11 | // 获取所有用户 12 | async function getUsers() { 13 | try { 14 | const data = await fs.readFile(USERS_FILE, 'utf8'); 15 | return JSON.parse(data); 16 | } catch (error) { 17 | if (error.code === 'ENOENT') { 18 | logger.warn('Users file does not exist, creating default user'); 19 | const defaultUser = { 20 | username: 'root', 21 | password: bcrypt.hashSync('admin', 10), 22 | createdAt: new Date().toISOString(), 23 | loginCount: 0, 24 | lastLogin: null 25 | }; 26 | await saveUsers([defaultUser]); 27 | return { users: [defaultUser] }; 28 | } 29 | throw error; 30 | } 31 | } 32 | 33 | // 保存用户 34 | async function saveUsers(users) { 35 | await fs.writeFile(USERS_FILE, JSON.stringify({ users }, null, 2), 'utf8'); 36 | } 37 | 38 | // 更新用户登录信息 39 | async function updateUserLoginInfo(username) { 40 | try { 41 | const { users } = await getUsers(); 42 | const user = users.find(u => u.username === username); 43 | 44 | if (user) { 45 | user.loginCount = (user.loginCount || 0) + 1; 46 | user.lastLogin = new Date().toISOString(); 47 | await saveUsers(users); 48 | } 49 | } catch (error) { 50 | logger.error('更新用户登录信息失败:', error); 51 | } 52 | } 53 | 54 | // 获取用户统计信息 55 | async function getUserStats(username) { 56 | try { 57 | const { users } = await getUsers(); 58 | const user = users.find(u => u.username === username); 59 | 60 | if (!user) { 61 | return { loginCount: '0', lastLogin: '未知', accountAge: '0' }; 62 | } 63 | 64 | // 计算账户年龄(如果有创建日期) 65 | let accountAge = '0'; 66 | if (user.createdAt) { 67 | const createdDate = new Date(user.createdAt); 68 | const currentDate = new Date(); 69 | const diffTime = Math.abs(currentDate - createdDate); 70 | const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); 71 | accountAge = diffDays.toString(); 72 | } 73 | 74 | // 格式化最后登录时间 75 | let lastLogin = '未知'; 76 | if (user.lastLogin) { 77 | const lastLoginDate = new Date(user.lastLogin); 78 | const now = new Date(); 79 | const isToday = lastLoginDate.toDateString() === now.toDateString(); 80 | 81 | if (isToday) { 82 | lastLogin = '今天 ' + lastLoginDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 83 | } else { 84 | lastLogin = lastLoginDate.toLocaleDateString() + ' ' + lastLoginDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 85 | } 86 | } 87 | 88 | return { 89 | username: user.username, 90 | loginCount: (user.loginCount || 0).toString(), 91 | lastLogin, 92 | accountAge 93 | }; 94 | } catch (error) { 95 | logger.error('获取用户统计信息失败:', error); 96 | return { loginCount: '0', lastLogin: '未知', accountAge: '0' }; 97 | } 98 | } 99 | 100 | // 创建新用户 101 | async function createUser(username, password) { 102 | try { 103 | const { users } = await getUsers(); 104 | 105 | // 检查用户是否已存在 106 | if (users.some(u => u.username === username)) { 107 | throw new Error('用户名已存在'); 108 | } 109 | 110 | const hashedPassword = bcrypt.hashSync(password, 10); 111 | const newUser = { 112 | username, 113 | password: hashedPassword, 114 | createdAt: new Date().toISOString(), 115 | loginCount: 0, 116 | lastLogin: null 117 | }; 118 | 119 | users.push(newUser); 120 | await saveUsers(users); 121 | 122 | return { success: true, username }; 123 | } catch (error) { 124 | logger.error('创建用户失败:', error); 125 | throw error; 126 | } 127 | } 128 | 129 | // 修改用户密码 130 | async function changePassword(username, currentPassword, newPassword) { 131 | try { 132 | const { users } = await getUsers(); 133 | const user = users.find(u => u.username === username); 134 | 135 | if (!user) { 136 | throw new Error('用户不存在'); 137 | } 138 | 139 | // 验证当前密码 140 | const isMatch = await bcrypt.compare(currentPassword, user.password); 141 | if (!isMatch) { 142 | throw new Error('当前密码不正确'); 143 | } 144 | 145 | // 验证新密码复杂度(虽然前端做了,后端再做一层保险) 146 | if (!isPasswordComplex(newPassword)) { 147 | throw new Error('新密码不符合复杂度要求'); 148 | } 149 | 150 | // 更新密码 151 | user.password = await bcrypt.hash(newPassword, 10); 152 | await saveUsers(users); 153 | 154 | logger.info(`用户 ${username} 密码已成功修改`); 155 | } catch (error) { 156 | logger.error('修改密码失败:', error); 157 | throw error; 158 | } 159 | } 160 | 161 | // 验证密码复杂度 (从 userCenter.js 复制过来并调整) 162 | function isPasswordComplex(password) { 163 | // 至少包含1个字母、1个数字和1个特殊字符,长度在8-16位之间 164 | const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/; 165 | return passwordRegex.test(password); 166 | } 167 | 168 | module.exports = { 169 | getUsers, 170 | saveUsers, 171 | updateUserLoginInfo, 172 | getUserStats, 173 | createUser, 174 | changePassword 175 | }; 176 | -------------------------------------------------------------------------------- /hubcmdui/start-diagnostic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 诊断启动脚本 - 运行诊断并安全启动服务器 3 | */ 4 | const { spawn } = require('child_process'); 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const { runDiagnostics } = require('./scripts/diagnostics'); 8 | 9 | // 确保必要的模块存在 10 | try { 11 | require('./logger'); 12 | } catch (error) { 13 | console.error('无法加载logger模块,请确保该模块存在:', error.message); 14 | process.exit(1); 15 | } 16 | 17 | const logger = require('./logger'); 18 | 19 | async function startWithDiagnostics() { 20 | logger.info('正在运行系统诊断...'); 21 | 22 | try { 23 | // 运行诊断 24 | const { criticalErrors } = await runDiagnostics(); 25 | 26 | if (criticalErrors.length > 0) { 27 | logger.error('发现严重问题,无法启动系统。请修复问题后重试。'); 28 | process.exit(1); 29 | } 30 | 31 | logger.success('诊断通过,正在启动系统...'); 32 | 33 | // 启动服务器 34 | const serverProcess = spawn('node', ['server.js'], { 35 | stdio: 'inherit', 36 | cwd: __dirname 37 | }); 38 | 39 | serverProcess.on('close', (code) => { 40 | if (code !== 0) { 41 | logger.error(`服务器进程异常退出,退出码: ${code}`); 42 | process.exit(code); 43 | } 44 | }); 45 | 46 | serverProcess.on('error', (err) => { 47 | logger.error('启动服务器进程时出错:', err); 48 | process.exit(1); 49 | }); 50 | 51 | } catch (error) { 52 | logger.fatal('诊断过程中发生错误:', error); 53 | process.exit(1); 54 | } 55 | } 56 | 57 | // 启动服务 58 | startWithDiagnostics(); 59 | -------------------------------------------------------------------------------- /hubcmdui/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "username": "root", 5 | "password": "$2b$10$HBYJPwEB1gdRxcc6Bm1mKukxCC8eyJOZC7sGJN5meghvsBfoQjKtW", 6 | "loginCount": 0, 7 | "lastLogin": "2025-05-10T11:37:31.774Z" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /hubcmdui/web/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dqzboy/Docker-Proxy/04cfa607b87f46acd6f8904f6e0be96ab6057b45/hubcmdui/web/.DS_Store -------------------------------------------------------------------------------- /hubcmdui/web/compatibility-layer.js: -------------------------------------------------------------------------------- 1 | // ... existing code ... 2 | // 获取文档列表 3 | app.get('/api/documentation', requireLogin, async (req, res) => { 4 | try { 5 | const docList = await getDocumentList(); 6 | res.json(docList); 7 | } catch (error) { 8 | console.error('获取文档列表失败:', error); 9 | res.status(500).json({ error: '获取文档列表失败', details: error.message }); 10 | } 11 | }); 12 | 13 | // 获取单个文档内容 14 | app.get('/api/documentation/:id', requireLogin, async (req, res) => { 15 | const docId = req.params.id; 16 | console.log(`获取文档内容请求,ID: ${docId}`); 17 | 18 | try { 19 | // 获取文档列表 20 | const docList = await getDocumentList(); 21 | 22 | // 查找指定ID的文档 23 | const doc = docList.find(doc => doc.id === docId || doc._id === docId); 24 | 25 | if (!doc) { 26 | return res.status(404).json({ error: '文档不存在', docId }); 27 | } 28 | 29 | // 如果文档未发布且用户不是管理员,则拒绝访问 30 | if (!doc.published && !isAdmin(req.user)) { 31 | return res.status(403).json({ error: '无权访问未发布的文档' }); 32 | } 33 | 34 | // 获取文档完整内容 35 | const docContent = await getDocumentContent(docId); 36 | 37 | // 合并文档信息和内容 38 | const fullDoc = { 39 | ...doc, 40 | content: docContent 41 | }; 42 | 43 | res.json(fullDoc); 44 | } catch (error) { 45 | console.error(`获取文档内容失败,ID: ${docId}`, error); 46 | res.status(500).json({ 47 | error: '获取文档内容失败', 48 | details: error.message, 49 | docId 50 | }); 51 | } 52 | }); 53 | // ... existing code ... -------------------------------------------------------------------------------- /hubcmdui/web/css/admin.css: -------------------------------------------------------------------------------- 1 | /* 今天登录的高亮样式 */ 2 | .today-login { 3 | background-color: #e6f7ff; 4 | color: #1890ff; 5 | padding: 2px 8px; 6 | border-radius: 4px; 7 | font-weight: 500; 8 | display: inline-block; 9 | } 10 | 11 | /* 用户信息显示优化 */ 12 | .account-info-item { 13 | margin-bottom: 15px; 14 | display: flex; 15 | align-items: center; 16 | } 17 | 18 | .account-info-item .label { 19 | font-weight: 500; 20 | width: 120px; 21 | flex-shrink: 0; 22 | color: #555; 23 | } 24 | 25 | .account-info-item .value { 26 | color: #1f2937; 27 | font-weight: 400; 28 | } 29 | 30 | /* 加载中占位符样式 */ 31 | .loading-placeholder { 32 | display: inline-block; 33 | width: 80px; 34 | height: 14px; 35 | background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); 36 | background-size: 200% 100%; 37 | animation: loading 1.5s infinite; 38 | border-radius: 4px; 39 | } 40 | 41 | @keyframes loading { 42 | 0% { 43 | background-position: 200% 0; 44 | } 45 | 100% { 46 | background-position: -200% 0; 47 | } 48 | } 49 | 50 | /* 菜单编辑弹窗样式 */ 51 | .menu-edit-popup { 52 | border-radius: 12px; 53 | padding: 24px; 54 | max-width: 500px; 55 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); 56 | } 57 | 58 | .menu-edit-title { 59 | font-size: 1.5em; 60 | color: #1f2937; 61 | margin-bottom: 24px; 62 | padding-bottom: 16px; 63 | border-bottom: 2px solid #f3f4f6; 64 | font-weight: 600; 65 | } 66 | 67 | .menu-edit-container { 68 | text-align: left; 69 | } 70 | 71 | .menu-edit-container .form-group { 72 | margin-bottom: 24px; 73 | } 74 | 75 | .menu-edit-container label { 76 | display: block; 77 | margin-bottom: 8px; 78 | color: #4b5563; 79 | font-weight: 500; 80 | font-size: 0.95em; 81 | } 82 | 83 | .menu-edit-container .swal2-input, 84 | .menu-edit-container .swal2-select { 85 | width: 100%; 86 | padding: 10px 14px; 87 | border: 2px solid #e5e7eb; 88 | border-radius: 8px; 89 | font-size: 14px; 90 | transition: all 0.3s ease; 91 | background-color: #f9fafb; 92 | } 93 | 94 | .menu-edit-container .swal2-input:hover, 95 | .menu-edit-container .swal2-select:hover { 96 | border-color: #d1d5db; 97 | background-color: #ffffff; 98 | } 99 | 100 | .menu-edit-container .swal2-input:focus, 101 | .menu-edit-container .swal2-select:focus { 102 | border-color: #4CAF50; 103 | outline: none; 104 | box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1); 105 | background-color: #ffffff; 106 | } 107 | 108 | .menu-edit-confirm { 109 | background-color: #4CAF50 !important; 110 | padding: 10px 24px !important; 111 | border-radius: 8px !important; 112 | font-weight: 500 !important; 113 | transition: all 0.3s ease !important; 114 | font-size: 14px !important; 115 | display: inline-flex !important; 116 | align-items: center !important; 117 | gap: 8px !important; 118 | } 119 | 120 | .menu-edit-confirm:hover { 121 | background-color: #45a049 !important; 122 | transform: translateY(-1px); 123 | box-shadow: 0 2px 8px rgba(76, 175, 80, 0.2); 124 | } 125 | 126 | .menu-edit-cancel { 127 | background-color: #f3f4f6 !important; 128 | color: #4b5563 !important; 129 | padding: 10px 24px !important; 130 | border-radius: 8px !important; 131 | font-weight: 500 !important; 132 | transition: all 0.3s ease !important; 133 | font-size: 14px !important; 134 | display: inline-flex !important; 135 | align-items: center !important; 136 | gap: 8px !important; 137 | } 138 | 139 | .menu-edit-cancel:hover { 140 | background-color: #e5e7eb !important; 141 | transform: translateY(-1px); 142 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 143 | } 144 | 145 | /* 操作按钮样式优化 */ 146 | .action-buttons { 147 | display: flex; 148 | gap: 8px; 149 | justify-content: flex-end; 150 | } 151 | 152 | .action-btn { 153 | width: 32px; 154 | height: 32px; 155 | border-radius: 6px; 156 | border: none; 157 | background-color: #f3f4f6; 158 | color: #4b5563; 159 | cursor: pointer; 160 | display: flex; 161 | align-items: center; 162 | justify-content: center; 163 | transition: all 0.2s ease; 164 | } 165 | 166 | .action-btn:hover { 167 | background-color: #e5e7eb; 168 | transform: translateY(-1px); 169 | } 170 | 171 | .action-btn i { 172 | font-size: 14px; 173 | } 174 | 175 | .action-btn.edit-btn { 176 | color: #3b82f6; 177 | } 178 | 179 | .action-btn.edit-btn:hover { 180 | background-color: #dbeafe; 181 | } 182 | 183 | .action-btn.delete-btn { 184 | color: #ef4444; 185 | } 186 | 187 | .action-btn.delete-btn:hover { 188 | background-color: #fee2e2; 189 | } 190 | 191 | .action-btn.log-btn { 192 | color: #10b981; 193 | } 194 | 195 | .action-btn.log-btn:hover { 196 | background-color: #d1fae5; 197 | } 198 | 199 | .action-btn.start-btn { 200 | color: #10b981; 201 | } 202 | 203 | .action-btn.start-btn:hover { 204 | background-color: #d1fae5; 205 | } 206 | 207 | .action-btn.stop-btn { 208 | color: #ef4444; 209 | } 210 | 211 | .action-btn.stop-btn:hover { 212 | background-color: #fee2e2; 213 | } 214 | 215 | .action-btn.restart-btn { 216 | color: #f59e0b; 217 | } 218 | 219 | .action-btn.restart-btn:hover { 220 | background-color: #fef3c7; 221 | } 222 | 223 | /* Docker 状态指示器样式 */ 224 | .status-indicator { 225 | display: inline-flex; 226 | align-items: center; 227 | padding: 6px 12px; 228 | border-radius: 4px; 229 | font-size: 14px; 230 | font-weight: 500; 231 | transition: all 0.3s; 232 | } 233 | 234 | .status-indicator.running { 235 | background-color: rgba(76, 175, 80, 0.1); 236 | color: #4CAF50; 237 | } 238 | 239 | .status-indicator.stopped { 240 | background-color: rgba(244, 67, 54, 0.1); 241 | color: #F44336; 242 | } 243 | 244 | .status-indicator i { 245 | margin-right: 6px; 246 | font-size: 16px; 247 | } 248 | 249 | /* 文档操作按钮样式优化 */ 250 | .view-btn { 251 | background-color: #f0f9ff !important; 252 | color: #0ea5e9 !important; 253 | } 254 | 255 | .view-btn:hover { 256 | background-color: #e0f2fe !important; 257 | color: #0284c7 !important; 258 | } 259 | 260 | .edit-btn { 261 | background-color: #f0fdf4 !important; 262 | color: #22c55e !important; 263 | } 264 | 265 | .edit-btn:hover { 266 | background-color: #dcfce7 !important; 267 | color: #16a34a !important; 268 | } 269 | 270 | .delete-btn { 271 | background-color: #fef2f2 !important; 272 | color: #ef4444 !important; 273 | } 274 | 275 | .delete-btn:hover { 276 | background-color: #fee2e2 !important; 277 | color: #dc2626 !important; 278 | } 279 | 280 | .publish-btn { 281 | background-color: #f0fdfa !important; 282 | color: #14b8a6 !important; 283 | } 284 | 285 | .publish-btn:hover { 286 | background-color: #ccfbf1 !important; 287 | color: #0d9488 !important; 288 | } 289 | 290 | .unpublish-btn { 291 | background-color: #fffbeb !important; 292 | color: #f59e0b !important; 293 | } 294 | 295 | .unpublish-btn:hover { 296 | background-color: #fef3c7 !important; 297 | color: #d97706 !important; 298 | } 299 | 300 | /* 刷新按钮交互反馈 */ 301 | .refresh-btn { 302 | background-color: #f9fafb; 303 | color: #4b5563; 304 | border: 1px solid #e5e7eb; 305 | padding: 6px 12px; 306 | border-radius: 4px; 307 | display: inline-flex; 308 | align-items: center; 309 | gap: 6px; 310 | font-size: 14px; 311 | cursor: pointer; 312 | transition: all 0.2s ease; 313 | } 314 | 315 | .refresh-btn:hover { 316 | background-color: #f3f4f6; 317 | color: #374151; 318 | } 319 | 320 | .refresh-btn.loading { 321 | pointer-events: none; 322 | opacity: 0.7; 323 | } 324 | 325 | .refresh-btn.loading i { 326 | animation: spin 1s linear infinite; 327 | } 328 | 329 | @keyframes spin { 330 | 0% { transform: rotate(0deg); } 331 | 100% { transform: rotate(360deg); } 332 | } 333 | 334 | /* Docker未运行友好提示 */ 335 | .docker-offline-container { 336 | background-color: #f9fafb; 337 | border: 1px solid #e5e7eb; 338 | border-radius: 8px; 339 | padding: 24px; 340 | margin: 20px 0; 341 | text-align: center; 342 | } 343 | 344 | .docker-offline-icon { 345 | font-size: 40px; 346 | color: #9ca3af; 347 | margin-bottom: 16px; 348 | } 349 | 350 | .docker-offline-title { 351 | font-size: 20px; 352 | font-weight: 600; 353 | color: #4b5563; 354 | margin-bottom: 8px; 355 | } 356 | 357 | .docker-offline-message { 358 | color: #6b7280; 359 | margin-bottom: 20px; 360 | } 361 | 362 | .docker-offline-actions { 363 | display: flex; 364 | justify-content: center; 365 | gap: 12px; 366 | } 367 | 368 | .docker-offline-btn { 369 | padding: 8px 16px; 370 | border-radius: 4px; 371 | border: none; 372 | font-weight: 500; 373 | cursor: pointer; 374 | display: inline-flex; 375 | align-items: center; 376 | gap: 8px; 377 | transition: all 0.2s ease; 378 | } 379 | 380 | .docker-offline-btn.primary { 381 | background-color: #4f46e5; 382 | color: white; 383 | } 384 | 385 | .docker-offline-btn.primary:hover { 386 | background-color: #4338ca; 387 | } 388 | 389 | .docker-offline-btn.secondary { 390 | background-color: #f3f4f6; 391 | color: #4b5563; 392 | } 393 | 394 | .docker-offline-btn.secondary:hover { 395 | background-color: #e5e7eb; 396 | } 397 | -------------------------------------------------------------------------------- /hubcmdui/web/data/documentation/index.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /hubcmdui/web/images/login-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dqzboy/Docker-Proxy/04cfa607b87f46acd6f8904f6e0be96ab6057b45/hubcmdui/web/images/login-bg.jpg -------------------------------------------------------------------------------- /hubcmdui/web/js/auth.js: -------------------------------------------------------------------------------- 1 | // 用户认证相关功能 2 | 3 | // 登录函数 4 | async function login() { 5 | const username = document.getElementById('username').value; 6 | const password = document.getElementById('password').value; 7 | const captcha = document.getElementById('captcha').value; 8 | 9 | try { 10 | core.showLoading(); 11 | const response = await fetch('/api/login', { 12 | method: 'POST', 13 | headers: { 'Content-Type': 'application/json' }, 14 | body: JSON.stringify({ username, password, captcha }) 15 | }); 16 | 17 | if (response.ok) { 18 | const data = await response.json(); 19 | 20 | window.isLoggedIn = true; 21 | localStorage.setItem('isLoggedIn', 'true'); 22 | persistSession(); 23 | document.getElementById('currentUsername').textContent = username; 24 | document.getElementById('welcomeUsername').textContent = username; 25 | document.getElementById('loginModal').style.display = 'none'; 26 | document.getElementById('adminContainer').style.display = 'flex'; 27 | 28 | // 确保加载完成后初始化事件监听器 29 | await core.loadSystemConfig(); 30 | core.initEventListeners(); 31 | core.showSection('dashboard'); 32 | userCenter.getUserInfo(); 33 | systemStatus.refreshSystemStatus(); 34 | } else { 35 | const errorData = await response.json(); 36 | core.showAlert(errorData.error || '登录失败', 'error'); 37 | refreshCaptcha(); 38 | } 39 | } catch (error) { 40 | core.showAlert('登录失败: ' + error.message, 'error'); 41 | refreshCaptcha(); 42 | } finally { 43 | core.hideLoading(); 44 | } 45 | } 46 | 47 | // 注销函数 48 | async function logout() { 49 | // console.log("注销操作被触发"); 50 | try { 51 | core.showLoading(); 52 | const response = await fetch('/api/logout', { method: 'POST' }); 53 | if (response.ok) { 54 | // 清除所有登录状态 55 | localStorage.removeItem('isLoggedIn'); 56 | sessionStorage.removeItem('sessionActive'); 57 | window.isLoggedIn = false; 58 | // 清除cookie 59 | document.cookie = 'connect.sid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; 60 | window.location.reload(); 61 | } else { 62 | throw new Error('退出登录失败'); 63 | } 64 | } catch (error) { 65 | // console.error('退出登录失败:', error); 66 | core.showAlert('退出登录失败: ' + error.message, 'error'); 67 | // 即使API失败也清除本地状态 68 | localStorage.removeItem('isLoggedIn'); 69 | sessionStorage.removeItem('sessionActive'); 70 | window.isLoggedIn = false; 71 | window.location.reload(); 72 | } finally { 73 | core.hideLoading(); 74 | } 75 | } 76 | 77 | // 验证码刷新函数 78 | async function refreshCaptcha() { 79 | try { 80 | const response = await fetch('/api/captcha'); 81 | if (!response.ok) { 82 | throw new Error(`验证码获取失败: ${response.status}`); 83 | } 84 | const data = await response.json(); 85 | document.getElementById('captchaText').textContent = data.captcha; 86 | } catch (error) { 87 | // console.error('刷新验证码失败:', error); 88 | document.getElementById('captchaText').textContent = '验证码加载失败,点击重试'; 89 | } 90 | } 91 | 92 | // 持久化会话 93 | function persistSession() { 94 | if (document.cookie.includes('connect.sid')) { 95 | sessionStorage.setItem('sessionActive', 'true'); 96 | } 97 | } 98 | 99 | // 显示登录模态框 100 | function showLoginModal() { 101 | // 确保先隐藏加载指示器 102 | if (core && typeof core.hideLoadingIndicator === 'function') { 103 | core.hideLoadingIndicator(); 104 | } 105 | 106 | document.getElementById('loginModal').style.display = 'flex'; 107 | refreshCaptcha(); 108 | } 109 | 110 | // 导出模块 111 | const auth = { 112 | init: function() { 113 | // console.log('初始化认证模块...'); 114 | // 在这里可以添加认证模块初始化的相关代码 115 | return Promise.resolve(); // 返回一个已解决的 Promise,保持与其他模块一致 116 | }, 117 | login, 118 | logout, 119 | refreshCaptcha, 120 | showLoginModal 121 | }; 122 | 123 | // 全局公开认证模块 124 | window.auth = auth; 125 | -------------------------------------------------------------------------------- /hubcmdui/web/js/error-handler.js: -------------------------------------------------------------------------------- 1 | // 客户端错误收集器 2 | 3 | (function() { 4 | // 保存原始控制台方法 5 | const originalConsoleError = console.error; 6 | 7 | // 重写console.error以捕获错误 8 | console.error = function(...args) { 9 | // 调用原始方法 10 | originalConsoleError.apply(console, args); 11 | 12 | // 提取错误信息 13 | const errorMessage = args.map(arg => { 14 | if (arg instanceof Error) { 15 | return arg.stack || arg.message; 16 | } else if (typeof arg === 'object') { 17 | try { 18 | return JSON.stringify(arg); 19 | } catch (e) { 20 | return String(arg); 21 | } 22 | } else { 23 | return String(arg); 24 | } 25 | }).join(' '); 26 | 27 | // 向服务器报告错误 28 | reportErrorToServer({ 29 | message: errorMessage, 30 | source: 'console.error', 31 | type: 'console' 32 | }); 33 | }; 34 | 35 | // 全局错误处理 36 | window.addEventListener('error', function(event) { 37 | reportErrorToServer({ 38 | message: event.message, 39 | source: event.filename, 40 | lineno: event.lineno, 41 | colno: event.colno, 42 | stack: event.error ? event.error.stack : null, 43 | type: 'uncaught' 44 | }); 45 | }); 46 | 47 | // Promise错误处理 48 | window.addEventListener('unhandledrejection', function(event) { 49 | const message = event.reason instanceof Error 50 | ? event.reason.message 51 | : String(event.reason); 52 | 53 | const stack = event.reason instanceof Error 54 | ? event.reason.stack 55 | : null; 56 | 57 | reportErrorToServer({ 58 | message: message, 59 | stack: stack, 60 | type: 'promise' 61 | }); 62 | }); 63 | 64 | // 向服务器发送错误报告 65 | function reportErrorToServer(errorData) { 66 | // 添加额外信息 67 | const data = { 68 | ...errorData, 69 | userAgent: navigator.userAgent, 70 | page: window.location.href, 71 | timestamp: new Date().toISOString() 72 | }; 73 | 74 | // 发送错误报告到服务器 75 | fetch('/api/client-error', { 76 | method: 'POST', 77 | headers: { 'Content-Type': 'application/json' }, 78 | body: JSON.stringify(data), 79 | // 使用keepalive以确保在页面卸载时仍能发送 80 | keepalive: true 81 | }).catch(err => { 82 | // 不记录这个错误,避免无限循环 83 | }); 84 | } 85 | })(); 86 | -------------------------------------------------------------------------------- /hubcmdui/web/js/nav-menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 导航菜单加载模块 3 | * 负责从config加载并渲染前端导航菜单 4 | */ 5 | 6 | // 用于跟踪菜单是否已加载 7 | let menuLoaded = false; 8 | 9 | // 立即执行初始化函数 10 | (function() { 11 | // console.log('[菜单模块] 初始化开始'); 12 | try { 13 | // 页面加载完成后执行,但不在这里调用加载函数 14 | document.addEventListener('DOMContentLoaded', function() { 15 | // console.log('[菜单模块] DOM内容加载完成,等待loadMenu或loadNavMenu调用'); 16 | // 不在这里调用,避免重复加载 17 | }); 18 | // console.log('[菜单模块] 初始化完成,等待调用'); 19 | } catch (error) { 20 | console.error('[菜单模块] 初始化失败:', error); 21 | } 22 | })(); 23 | 24 | // 加载导航菜单 25 | async function loadNavMenu() { 26 | if (menuLoaded) { 27 | // console.log('[菜单模块] 菜单已加载,跳过'); 28 | return; 29 | } 30 | 31 | // console.log('[菜单模块] loadNavMenu() 函数被调用'); 32 | const navMenu = document.getElementById('navMenu'); 33 | if (!navMenu) { 34 | console.error('[菜单模块] 无法找到id为navMenu的元素,菜单加载失败'); 35 | return; 36 | } 37 | 38 | try { 39 | // console.log('[菜单模块] 正在从/api/config获取菜单配置...'); 40 | 41 | // 从API获取配置 42 | const response = await fetch('/api/config'); 43 | // console.log('[菜单模块] API响应状态:', response.status, response.statusText); 44 | 45 | if (!response.ok) { 46 | throw new Error(`获取配置失败: ${response.status} ${response.statusText}`); 47 | } 48 | 49 | const config = await response.json(); 50 | // console.log('[菜单模块] 成功获取配置:', config); 51 | 52 | // 确保menuItems存在且是数组 53 | if (!config.menuItems || !Array.isArray(config.menuItems) || config.menuItems.length === 0) { 54 | console.warn('[菜单模块] 配置中没有菜单项或格式不正确', config); 55 | navMenu.innerHTML = '未设置菜单'; 56 | menuLoaded = true; 57 | return; 58 | } 59 | 60 | // 渲染菜单 61 | renderNavMenu(navMenu, config.menuItems); 62 | menuLoaded = true; 63 | 64 | } catch (error) { 65 | console.error('[菜单模块] 加载导航菜单失败:', error); 66 | navMenu.innerHTML = `菜单加载失败: ${error.message}`; 67 | } 68 | } 69 | 70 | // 渲染导航菜单 71 | function renderNavMenu(navMenuElement, menuItems) { 72 | try { 73 | // console.log('[菜单模块] 开始渲染导航菜单,菜单项数量:', menuItems.length); 74 | 75 | // 清空现有内容 76 | navMenuElement.innerHTML = ''; 77 | 78 | // 移动设备菜单切换按钮 79 | const menuToggle = document.createElement('div'); 80 | menuToggle.id = 'menuToggle'; 81 | menuToggle.className = 'menu-toggle'; 82 | menuToggle.innerHTML = ''; 83 | menuToggle.style.color = '#333'; // 设置为深色 84 | menuToggle.style.fontSize = '28px'; // 增大菜单图标 85 | navMenuElement.appendChild(menuToggle); 86 | 87 | // 菜单项容器 88 | const menuList = document.createElement('ul'); 89 | menuList.className = 'nav-list'; 90 | menuList.style.display = 'flex'; 91 | menuList.style.listStyle = 'none'; 92 | menuList.style.margin = '0'; 93 | menuList.style.padding = '0'; 94 | 95 | // 添加菜单项 96 | menuItems.forEach((item, index) => { 97 | // console.log(`[菜单模块] 渲染菜单项 #${index+1}:`, item); 98 | const menuItem = document.createElement('li'); 99 | menuItem.style.marginLeft = '25px'; // 增加间距 100 | 101 | const link = document.createElement('a'); 102 | link.href = item.link || '#'; 103 | link.textContent = item.text || '未命名菜单'; 104 | 105 | // 使用内联样式确保文字颜色可见,并增大字体 106 | link.style.color = '#333'; // 黑色文字 107 | link.style.textDecoration = 'none'; 108 | link.style.fontSize = '16px'; // 从14px增大到16px 109 | link.style.fontWeight = 'bold'; 110 | link.style.padding = '8px 15px'; // 增大内边距 111 | link.style.borderRadius = '4px'; 112 | link.style.transition = 'background-color 0.3s, color 0.3s'; 113 | 114 | // 添加鼠标悬停效果 115 | link.addEventListener('mouseover', function() { 116 | this.style.backgroundColor = '#3d7cf4'; // 蓝色背景 117 | this.style.color = '#fff'; // 白色文字 118 | }); 119 | 120 | // 鼠标移出时恢复原样 121 | link.addEventListener('mouseout', function() { 122 | this.style.backgroundColor = 'transparent'; 123 | this.style.color = '#333'; 124 | }); 125 | 126 | if (item.newTab) { 127 | link.target = '_blank'; 128 | link.rel = 'noopener noreferrer'; 129 | } 130 | 131 | menuItem.appendChild(link); 132 | menuList.appendChild(menuItem); 133 | }); 134 | 135 | navMenuElement.appendChild(menuList); 136 | 137 | // 绑定移动端菜单切换事件 138 | menuToggle.addEventListener('click', () => { 139 | // console.log('[菜单模块] 菜单切换按钮被点击'); 140 | navMenuElement.classList.toggle('active'); 141 | }); 142 | 143 | // console.log(`[菜单模块] 成功渲染了 ${menuItems.length} 个导航菜单项`); 144 | } catch (error) { 145 | console.error('[菜单模块] 渲染导航菜单失败:', error); 146 | navMenuElement.innerHTML = `菜单渲染失败: ${error.message}`; 147 | } 148 | } 149 | 150 | // 添加loadMenu函数,作为loadNavMenu的别名,确保与index.html中的调用匹配 151 | function loadMenu() { 152 | // console.log('[菜单模块] 调用loadMenu() - 转发到loadNavMenu()'); 153 | loadNavMenu(); 154 | } -------------------------------------------------------------------------------- /hubcmdui/web/js/networkTest.js: -------------------------------------------------------------------------------- 1 | // 网络测试相关功能 2 | 3 | // 创建networkTest对象 4 | const networkTest = { 5 | // 初始化函数 6 | init: function() { 7 | // console.log('初始化网络测试模块...'); 8 | this.initNetworkTestControls(); // Renamed for clarity 9 | this.displayInitialResultsMessage(); 10 | return Promise.resolve(); // Keep if other inits expect a Promise 11 | }, 12 | 13 | displayInitialResultsMessage: function() { 14 | const resultsDiv = document.getElementById('testResults'); 15 | if (resultsDiv) { 16 | resultsDiv.innerHTML = '

请选择参数并开始测试。

'; 17 | } 18 | }, 19 | 20 | // 初始化网络测试界面控件和事件 21 | initNetworkTestControls: function() { 22 | const domainSelect = document.getElementById('domainSelect'); 23 | const testTypeSelect = document.getElementById('testType'); // Corrected ID reference 24 | const startTestButton = document.getElementById('startTestBtn'); // Use ID 25 | const clearResultsButton = document.getElementById('clearTestResultsBtn'); 26 | const resultsDiv = document.getElementById('testResults'); 27 | 28 | // 填充域名选择器 29 | if (domainSelect) { 30 | domainSelect.innerHTML = ` 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | `; 44 | 45 | // 添加选择变化事件,显示/隐藏自定义域名输入框 46 | domainSelect.addEventListener('change', () => { 47 | const customDomainContainer = document.getElementById('customDomainContainer'); 48 | if (customDomainContainer) { 49 | customDomainContainer.style.display = domainSelect.value === 'custom' ? 'block' : 'none'; 50 | } 51 | }); 52 | } 53 | 54 | // 填充测试类型选择器 55 | if (testTypeSelect) { 56 | testTypeSelect.innerHTML = ` 57 | 58 | 59 | `; 60 | } 61 | 62 | // 绑定开始测试按钮点击事件 63 | if (startTestButton) { 64 | // Bind the function correctly, preserving 'this' context if necessary, 65 | // or ensure runNetworkTest doesn't rely on 'this' from the event handler. 66 | // Using an arrow function or .bind(this) if runNetworkTest uses 'this.someOtherMethod' 67 | startTestButton.addEventListener('click', () => this.runNetworkTest()); 68 | } else { 69 | // console.error('未找到开始测试按钮 (ID: startTestBtn)'); 70 | } 71 | 72 | // 绑定清空结果按钮点击事件 73 | if (clearResultsButton && resultsDiv) { 74 | clearResultsButton.addEventListener('click', () => { 75 | resultsDiv.innerHTML = '

结果已清空。

'; 76 | // Optionally, remove loading class if it was somehow stuck 77 | resultsDiv.classList.remove('loading'); 78 | }); 79 | } else { 80 | if (!clearResultsButton) { /* console.error('未找到清空结果按钮 (ID: clearTestResultsBtn)'); */ } 81 | if (!resultsDiv) { /* console.error('未找到测试结果区域 (ID: testResults)'); */ } 82 | } 83 | }, 84 | 85 | // 运行网络测试 86 | runNetworkTest: async function() { // Changed to async for await 87 | let domain = document.getElementById('domainSelect').value; 88 | const testType = document.getElementById('testType').value; 89 | const resultsDiv = document.getElementById('testResults'); 90 | const startTestButton = document.getElementById('startTestBtn'); 91 | 92 | // 处理自定义域名 93 | if (domain === 'custom') { 94 | const customDomain = document.getElementById('customDomain')?.value?.trim(); 95 | if (!customDomain) { 96 | core.showAlert('请输入自定义域名进行测试。', 'warning'); 97 | return; 98 | } 99 | domain = customDomain; 100 | } else if (!domain) { 101 | core.showAlert('请选择目标域名进行测试。', 'warning'); 102 | return; 103 | } 104 | 105 | if (!testType) { 106 | core.showAlert('请选择测试类型。', 'warning'); 107 | return; 108 | } 109 | 110 | resultsDiv.innerHTML = ''; // Clear previous content before adding loading class 111 | resultsDiv.classList.add('loading'); 112 | if(startTestButton) startTestButton.disabled = true; // Disable button during test 113 | 114 | const controller = new AbortController(); 115 | const timeoutId = setTimeout(() => { 116 | controller.abort(); 117 | // logger.warn('Network test aborted due to timeout.'); 118 | }, 60000); // 60秒超时 119 | 120 | try { 121 | const response = await fetch('/api/network-test', { 122 | method: 'POST', 123 | headers: { 124 | 'Content-Type': 'application/json', 125 | // Add any necessary auth headers if your API requires them 126 | // 'Authorization': 'Bearer ' + localStorage.getItem('authToken'), 127 | }, 128 | body: JSON.stringify({ domain, type: testType }), 129 | signal: controller.signal 130 | }); 131 | 132 | clearTimeout(timeoutId); 133 | 134 | if (!response.ok) { 135 | const errorText = await response.text(); 136 | let detail = errorText; 137 | try { 138 | const errorJson = JSON.parse(errorText); 139 | detail = errorJson.message || errorJson.error || errorText; 140 | } catch (e) { /* ignore parsing error, use raw text */ } 141 | throw new Error(`网络连接正常,但测试执行失败 (状态: ${response.status}): ${detail}`); 142 | } 143 | const result = await response.text(); 144 | // Format the plain text result in a
 tag for better display
145 |             resultsDiv.innerHTML = `
${result}
`; 146 | } catch (error) { 147 | clearTimeout(timeoutId); // Ensure timeout is cleared on any error 148 | // console.error('网络测试出错:', error); 149 | let errorMessage = '测试失败: ' + error.message; 150 | if (error.name === 'AbortError') { 151 | errorMessage = '测试请求超时 (60秒)。请检查网络连接或目标主机状态。'; 152 | } 153 | resultsDiv.innerHTML = `
${errorMessage}
`; 154 | } finally { 155 | resultsDiv.classList.remove('loading'); 156 | if(startTestButton) startTestButton.disabled = false; // Re-enable button 157 | } 158 | } 159 | }; 160 | 161 | // 全局公开网络测试模块 (或者 integrate with app.js module system if you have one) 162 | // Ensure this is called after the DOM is ready, e.g., in app.js or a DOMContentLoaded listener 163 | // For now, let's assume app.js handles calling networkTest.init() 164 | window.networkTest = networkTest; 165 | -------------------------------------------------------------------------------- /hubcmdui/web/services/documentationService.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { v4: uuidv4 } = require('uuid'); 4 | 5 | // 文档存储目录 6 | const DOCUMENTATION_DIR = path.join(__dirname, '..', 'data', 'documentation'); 7 | 8 | /** 9 | * 确保文档目录存在 10 | */ 11 | async function ensureDocumentationDirExists() { 12 | if (!fs.existsSync(DOCUMENTATION_DIR)) { 13 | await fs.promises.mkdir(DOCUMENTATION_DIR, { recursive: true }); 14 | console.log(`创建文档目录: ${DOCUMENTATION_DIR}`); 15 | } 16 | } 17 | 18 | /** 19 | * 获取文档列表 20 | * @returns {Promise} 文档列表 21 | */ 22 | async function getDocumentList() { 23 | try { 24 | await ensureDocumentationDirExists(); 25 | 26 | // 检查索引文件是否存在 27 | const indexPath = path.join(DOCUMENTATION_DIR, 'index.json'); 28 | if (!fs.existsSync(indexPath)) { 29 | // 创建空索引,不再添加默认文档 30 | await fs.promises.writeFile(indexPath, JSON.stringify([]), 'utf8'); 31 | console.log('创建了空的文档索引文件'); 32 | return []; 33 | } 34 | 35 | // 读取索引文件 36 | const data = await fs.promises.readFile(indexPath, 'utf8'); 37 | return JSON.parse(data || '[]'); 38 | } catch (error) { 39 | console.error('获取文档列表失败:', error); 40 | return []; 41 | } 42 | } 43 | 44 | /** 45 | * 保存文档列表 46 | * @param {Array} docList 文档列表 47 | */ 48 | async function saveDocumentList(docList) { 49 | try { 50 | await ensureDocumentationDirExists(); 51 | 52 | const indexPath = path.join(DOCUMENTATION_DIR, 'index.json'); 53 | await fs.promises.writeFile(indexPath, JSON.stringify(docList, null, 2), 'utf8'); 54 | console.log('文档列表已更新'); 55 | } catch (error) { 56 | console.error('保存文档列表失败:', error); 57 | throw error; 58 | } 59 | } 60 | 61 | /** 62 | * 获取单个文档的内容 63 | * @param {string} docId 文档ID 64 | * @returns {Promise} 文档内容 65 | */ 66 | async function getDocumentContent(docId) { 67 | try { 68 | console.log(`尝试获取文档内容,ID: ${docId}`); 69 | 70 | // 确保文档目录存在 71 | await ensureDocumentationDirExists(); 72 | 73 | // 获取文档列表 74 | const docList = await getDocumentList(); 75 | const doc = docList.find(doc => doc.id === docId || doc._id === docId); 76 | 77 | if (!doc) { 78 | throw new Error(`文档不存在,ID: ${docId}`); 79 | } 80 | 81 | // 构建文档路径 82 | const docPath = path.join(DOCUMENTATION_DIR, `${docId}.md`); 83 | 84 | // 检查文件是否存在 85 | if (!fs.existsSync(docPath)) { 86 | return ''; // 文件不存在,返回空内容 87 | } 88 | 89 | // 读取文档内容 90 | const content = await fs.promises.readFile(docPath, 'utf8'); 91 | console.log(`成功读取文档内容,ID: ${docId}, 内容长度: ${content.length}`); 92 | 93 | return content; 94 | } catch (error) { 95 | console.error(`获取文档内容失败,ID: ${docId}`, error); 96 | throw error; 97 | } 98 | } 99 | 100 | /** 101 | * 创建或更新文档 102 | * @param {Object} doc 文档对象 103 | * @param {string} content 文档内容 104 | * @returns {Promise} 保存后的文档 105 | */ 106 | async function saveDocument(doc, content) { 107 | try { 108 | await ensureDocumentationDirExists(); 109 | 110 | // 获取现有文档列表 111 | const docList = await getDocumentList(); 112 | 113 | // 为新文档生成ID 114 | if (!doc.id) { 115 | doc.id = uuidv4(); 116 | } 117 | 118 | // 更新文档元数据 119 | doc.lastUpdated = new Date().toISOString(); 120 | 121 | // 查找现有文档索引 122 | const existingIndex = docList.findIndex(item => item.id === doc.id); 123 | 124 | if (existingIndex >= 0) { 125 | // 更新现有文档 126 | docList[existingIndex] = { ...docList[existingIndex], ...doc }; 127 | } else { 128 | // 添加新文档 129 | docList.push(doc); 130 | } 131 | 132 | // 保存文档内容 133 | const docPath = path.join(DOCUMENTATION_DIR, `${doc.id}.md`); 134 | await fs.promises.writeFile(docPath, content || '', 'utf8'); 135 | 136 | // 更新文档列表 137 | await saveDocumentList(docList); 138 | 139 | return doc; 140 | } catch (error) { 141 | console.error('保存文档失败:', error); 142 | throw error; 143 | } 144 | } 145 | 146 | /** 147 | * 删除文档 148 | * @param {string} docId 文档ID 149 | * @returns {Promise} 删除结果 150 | */ 151 | async function deleteDocument(docId) { 152 | try { 153 | await ensureDocumentationDirExists(); 154 | 155 | // 获取现有文档列表 156 | const docList = await getDocumentList(); 157 | 158 | // 查找文档索引 159 | const existingIndex = docList.findIndex(doc => doc.id === docId); 160 | 161 | if (existingIndex === -1) { 162 | return false; // 文档不存在 163 | } 164 | 165 | // 从列表中移除 166 | docList.splice(existingIndex, 1); 167 | 168 | // 删除文档文件 169 | const docPath = path.join(DOCUMENTATION_DIR, `${docId}.md`); 170 | if (fs.existsSync(docPath)) { 171 | await fs.promises.unlink(docPath); 172 | } 173 | 174 | // 更新文档列表 175 | await saveDocumentList(docList); 176 | 177 | return true; 178 | } catch (error) { 179 | console.error('删除文档失败:', error); 180 | throw error; 181 | } 182 | } 183 | 184 | /** 185 | * 发布或取消发布文档 186 | * @param {string} docId 文档ID 187 | * @param {boolean} publishState 发布状态 188 | * @returns {Promise} 更新后的文档 189 | */ 190 | async function togglePublishState(docId, publishState) { 191 | try { 192 | // 获取现有文档列表 193 | const docList = await getDocumentList(); 194 | 195 | // 查找文档索引 196 | const existingIndex = docList.findIndex(doc => doc.id === docId); 197 | 198 | if (existingIndex === -1) { 199 | throw new Error('文档不存在'); 200 | } 201 | 202 | // 更新发布状态 203 | docList[existingIndex].published = !!publishState; 204 | docList[existingIndex].lastUpdated = new Date().toISOString(); 205 | 206 | // 更新文档列表 207 | await saveDocumentList(docList); 208 | 209 | return docList[existingIndex]; 210 | } catch (error) { 211 | console.error('更新文档发布状态失败:', error); 212 | throw error; 213 | } 214 | } 215 | 216 | module.exports = { 217 | ensureDocumentationDirExists, 218 | getDocumentList, 219 | saveDocumentList, 220 | getDocumentContent, 221 | saveDocument, 222 | deleteDocument, 223 | togglePublishState 224 | }; --------------------------------------------------------------------------------