├── .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 | 
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 | [](https://github.com/dqzboy)
16 | [](https://github.com/dqzboy/Docker-Proxy/graphs/contributors)
17 | [](https://github.com/dqzboy/Docker-Proxy/issues)
18 | [](https://github.com/dqzboy/Docker-Proxy)
19 | [](https://github.com/dqzboy/Docker-Proxy)
20 | [](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 | Alipay
150 | WeChat Pay
151 |
152 |
153 |
154 |
155 |
156 |
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 = ``;
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 = ``;
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 | gcr.io
33 | ghcr.io
34 | quay.io
35 | k8s.gcr.io
36 | registry.k8s.io
37 | mcr.microsoft.com
38 | docker.elastic.co
39 | registry-1.docker.io
40 | google.com (测试用)
41 | cloudflare.com (测试用)
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 | Ping (ICMP)
58 | Traceroute
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 | };
--------------------------------------------------------------------------------