@lancewu602/mihomo-dash 0.0.0-stage → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -2
  3. package/bin/mihomo-dash.js +4 -0
  4. package/package.json +63 -4
  5. package/server/CHANGELOG.md +51 -0
  6. package/server/package.json +13 -0
  7. package/server/src/cli/index.js +75 -0
  8. package/server/src/cli/service.js +167 -0
  9. package/server/src/cli/status.js +245 -0
  10. package/server/src/http/config-files.js +148 -0
  11. package/server/src/http/config-order.js +186 -0
  12. package/server/src/http/config-sections.js +278 -0
  13. package/server/src/http/default-config.js +69 -0
  14. package/server/src/http/groups.js +386 -0
  15. package/server/src/http/index.js +260 -0
  16. package/server/src/http/kernel-admin.js +522 -0
  17. package/server/src/http/kernel-proxy.js +43 -0
  18. package/server/src/http/providers.js +251 -0
  19. package/server/src/http/rules.js +509 -0
  20. package/server/src/http/settings.js +409 -0
  21. package/server/src/http/system-proxy.js +86 -0
  22. package/server/src/index.js +26 -0
  23. package/server/src/kernel/archive.js +181 -0
  24. package/server/src/kernel/binary.js +33 -0
  25. package/server/src/kernel/client.js +138 -0
  26. package/server/src/kernel/install.js +150 -0
  27. package/server/src/kernel/lifecycle.js +339 -0
  28. package/server/src/kernel/run.js +11 -0
  29. package/server/src/shared/args.js +57 -0
  30. package/server/src/shared/atomic-file.js +41 -0
  31. package/server/src/shared/exec.js +44 -0
  32. package/server/src/shared/kernel-config.js +27 -0
  33. package/server/src/shared/log-file.js +92 -0
  34. package/server/src/shared/panel.js +58 -0
  35. package/server/src/shared/paths.js +72 -0
  36. package/server/src/shared/service.js +552 -0
  37. package/server/src/shared/system-proxy.js +375 -0
  38. package/server/src/shared/text.js +11 -0
  39. package/server/tests/cli-service.test.js +153 -0
  40. package/server/tests/config-dir.test.js +133 -0
  41. package/server/tests/config-order.test.js +148 -0
  42. package/server/tests/groups.test.js +558 -0
  43. package/server/tests/kernel-admin.test.js +647 -0
  44. package/server/tests/kernel.test.js +250 -0
  45. package/server/tests/log-file.test.js +110 -0
  46. package/server/tests/providers.test.js +259 -0
  47. package/server/tests/rules.test.js +463 -0
  48. package/server/tests/service.test.js +470 -0
  49. package/server/tests/settings.test.js +456 -0
  50. package/server/tests/status.test.js +269 -0
  51. package/server/tests/system-proxy.test.js +497 -0
  52. package/server/tsconfig.json +17 -0
  53. package/web/dist/assets/index-BpDzYhuI.css +1 -0
  54. package/web/dist/assets/index-WmujD2EM.js +515 -0
  55. package/web/dist/favicon.svg +5 -0
  56. package/web/dist/index.html +14 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lancewu602
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,100 @@
1
- # Temporary Holding Version
1
+ # mihomo-dash
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 管理本机 [mihomo](https://github.com/MetaCubeX/mihomo)(Clash.Meta)内核的一体化工具:**CLI + 守护进程 + Web 面板**,一个 npm 包搞定订阅、策略组、规则、日志与配置文件。
4
+
5
+ > npm 包名是 **`@lancewu602/mihomo-dash`**(scoped),命令名仍是 **`mihomo-dash`**(装上后直接敲它)。
6
+
7
+ - 运行时依赖只有 `express` 与 `js-yaml`,后端纯 JS + JSDoc、**零构建**直跑
8
+ - 前端 React 18 + TypeScript + Vite + Ant Design 5,面板默认只监听 `127.0.0.1:8981`
9
+ - 改配置走「校验 → 写盘 → 内核热重载」,文本级改写保留 YAML 注释(不留 `.bak`,重载失败只报错不改回文件)
10
+ - 纯 JS 包,任何装了 Node ≥20 的平台都能跑;不发布原生二进制
11
+
12
+ ## 快速开始
13
+
14
+ ```bash
15
+ npm i -g @lancewu602/mihomo-dash # 或 npx @lancewu602/mihomo-dash
16
+
17
+ mihomo-dash # 无参数 = 总览(内核 / 面板 / 系统代理 / 开机自启)
18
+ mihomo-dash service install # 装成开机自启(macOS launchd / Debian systemd --user)
19
+ mihomo-dash service status # 查看自启状态
20
+ ```
21
+
22
+ ## 命令
23
+
24
+ ```bash
25
+ mihomo-dash # 总览:内核 / 面板 / 系统代理 / 开机自启(= status)
26
+ sudo mihomo-dash service install # Debian:system 单元(默认),开机即起
27
+ mihomo-dash service install # macOS:用户级 LaunchAgent,不需要 root
28
+ mihomo-dash service status # 自启状态(作用域 / 定义目录 / 是否在跑)
29
+ sudo mihomo-dash service uninstall # 卸载(不停进程)
30
+ mihomo-dash service print # 打印服务定义(plist / systemd unit),不写文件
31
+ ```
32
+
33
+ 面板的启动交给系统服务管理器,装好后由它拉起 `server/src/index.js`(面板)与
34
+ `server/src/kernel/run.js`(内核)。**只开机拉起一次、不保活**(崩了不重拉,停了就是真停);
35
+ 安装 / 卸载都**不动正在跑的进程**。
36
+
37
+ 作用域(Debian):
38
+
39
+ | 作用域 | 落点 | 要 root | 开机即起 |
40
+ | ---------------------- | -------------------------------------------------------- | ------- | -------------------------------------- |
41
+ | `--system`(**默认**) | `/etc/systemd/system/mihomo-dash-{panel,kernel}.service` | 是 | 是(不依赖 linger) |
42
+ | `--user` | `~/.config/systemd/user/…` | 否 | 需 `sudo loginctl enable-linger $USER` |
43
+
44
+ macOS 只有用户级 LaunchAgent(`~/Library/LaunchAgents`,`gui/<uid>` 域),没有 system 域。
45
+ 日志:macOS 在 `~/.config/mihomo-dash/{daemon,kernel}.log`,Debian 在 `journalctl`(`--user` 视作用域)。
46
+
47
+ ## 面板功能
48
+
49
+ | 路由 | 页面 | 能力 |
50
+ | -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
51
+ | `/` | 概览 | 内核信息条、代理概览(订阅/策略组/节点/可用节点)、累计流量 / 活动连接 / 内存占用读数、上下行速率 60s 趋势 |
52
+ | `/proxies` | 节点 | 策略组切换与增删改、节点测速、订阅增删/更新/健康检查、每条订阅的「拉取出口」(走哪个策略组 / 节点去拉,直连常被墙) |
53
+ | `/connections` | 连接 | 连接表、单个/全部关闭、host 搜索 |
54
+ | `/rules` | 规则 | 两张卡片:自定义规则(`DOMAIN-SUFFIX` 增删)+ 系统规则(固定 7 条 GEOSITE/MATCH 启停与选目标) |
55
+ | `/logs` | 日志 | 实时日志流(关键字 / 级别过滤、暂停、自动滚动) |
56
+ | `/config` | 配置 | 快捷设置(allow-lan / log-level / unified-delay / tcp-concurrent / find-process-mode)、GEO 数据库展示与配置、系统代理开关 |
57
+ | `/config-file` | 配置文件 | CodeMirror 直接编辑 `config.yaml` 源文件 |
58
+ | `/kernel` | 内核 | 面板自有内核:上传安装包(.gz / .zip)安装与升级、卸载、日志(进程启停 / 重启在侧栏底部) |
59
+
60
+ 设计要点:
61
+
62
+ - **REST 走后端转发**:`/api/kernel/*` 透明转发内核控制接口,并从 `config.yaml` 读 `secret` 自动附加 `Authorization`,内核错误统一转 502。
63
+ - **内核的装 / 升 / 卸只在面板**,且**面板不联网**:安装包要自己从官方 release 页面下好再上传。面板只认自己的落点 `bin/mihomo`,不去探测(也不会去碰)系统上别的位置的内核。
64
+ - **WebSocket 直连内核**:前端直连内核 `ws://<内核>` 的 `/traffic` `/memory` `/logs` `/connections`,不经后端中转。
65
+ - **配置写入只在后端**:订阅、策略组、快捷设置、自定义规则、`default-selected` 均文本级改写对应段落/键以保留注释;GEO 项不在内核 `PATCH /configs` schema 内,改完走整包热重载。
66
+ - **顶层键按固定顺序落盘**:面板每次写 `config.yaml`(含「配置文件」页保存)都会把顶层键排一遍 —— 基础运行 → 控制接口 → GEO → 内核行为 → 代理与分组 → 规则;只搬整块、不动内容与注释,面板不认识的键排在 `rules` 前面。
67
+ - **进程启停是进程级**:「启动 / 重启 / 停止内核」(侧栏底部)都 detached 拉起 / 停掉内核子进程,PID 会变;内核自带的 `POST /restart`(exec 重启、PID 不变)面板前端不再调用。
68
+ - **开机自启在 CLI(`mihomo-dash service …`)**:只开机拉起一次、不保活(崩了不重拉,停了就是真停);**安装 / 卸载都不动正在跑的进程**(否则会把正在服务这个页面的面板自己杀掉),已装载的服务只更新定义、下次登录生效。要停进程仍是侧栏的「停止内核」/ 服务管理器(`systemctl --user stop …` 或 launchd)。
69
+
70
+ ## 环境变量
71
+
72
+ | 变量 | 说明 |
73
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
74
+ | `MIHOMO_DIR` | 内核配置目录的显式覆盖(不设就用 `~/.config/mihomo-dash/data`;面板不再自动扫别处的 mihomo 目录) |
75
+ | `MIHOMO_DASH_DIR` | 状态目录(默认 `~/.config/mihomo-dash`):内核管理文件(`bin/`、`data/`、`kernels.json`、`kernel.pid`、`kernel.log`)与自启服务的日志 |
76
+
77
+ ## 开发
78
+
79
+ ```bash
80
+ npm install # 根:后端运行时 + 工具链
81
+ npm --prefix web install # 前端依赖(React / antd / vite / vitest)
82
+ npm run setup # 上面两条的合体
83
+ npm run dev # vite(5173) + 面板(8981),vite 已代理 /api
84
+ npm run build # 构建前端 → web/dist
85
+ npm test # node --test(桩内核,不依赖真实 mihomo)
86
+ npm run lint
87
+ npm run format
88
+ ```
89
+
90
+ 架构与 API 细节见 [AGENTS.md](./AGENTS.md)。
91
+
92
+ ## 安全边界
93
+
94
+ - 面板默认只监听 `127.0.0.1`,**没有任何鉴权**(token 鉴权未实现,也没有对应环境变量),因此仅适合本机使用;要暴露到局域网得自己在前面加一层带鉴权的反代。
95
+ - **不校验 `Host` / `Origin`**(2026-11 明示决定:不加鉴权,也不加 Host 白名单)。这意味着本机上的任意网页都可能借 **DNS 重绑定**直接调这些 API —— 装开机自启、上传并执行内核二进制、开关系统代理都在其中。所以别在开着面板时停留/操作不可信页面;这台机器上但凡有人能让你打开一个网页,就能操作这个面板。
96
+ - 文件读写限定在内核配置目录内(`MIHOMO_DIR` 覆盖 > `~/.config/mihomo-dash/data`),路径 resolve 白名单防目录穿越。
97
+
98
+ ## License
99
+
100
+ [MIT](./LICENSE)
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../server/src/cli/index.js';
3
+
4
+ main(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,65 @@
1
1
  {
2
2
  "name": "@lancewu602/mihomo-dash",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.6.1",
4
+ "description": "mihomo 管理工具:CLI + 守护进程 + Web 面板",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/lancewu602/mihomo-dash.git"
10
+ },
11
+ "homepage": "https://github.com/lancewu602/mihomo-dash#readme",
12
+ "bugs": "https://github.com/lancewu602/mihomo-dash/issues",
13
+ "keywords": [
14
+ "mihomo",
15
+ "clash",
16
+ "clash-meta",
17
+ "proxy",
18
+ "cli",
19
+ "panel"
20
+ ],
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "bin": {
25
+ "mihomo-dash": "bin/mihomo-dash.js"
26
+ },
27
+ "files": [
28
+ "bin",
29
+ "server",
30
+ "web/dist"
31
+ ],
32
+ "scripts": {
33
+ "setup": "npm install && npm --prefix web install",
34
+ "dev": "npm --prefix server run dev & npm --prefix web run dev & wait",
35
+ "api": "npm --prefix server run dev",
36
+ "web": "npm --prefix web run dev",
37
+ "build": "npm --prefix web run build",
38
+ "serve": "npm run build && node bin/mihomo-dash-runner.js panel",
39
+ "test": "npm --prefix server test && npm --prefix web test",
40
+ "typecheck": "npm --prefix server run typecheck && npm --prefix web run typecheck",
41
+ "lint": "eslint . --max-warnings 0",
42
+ "format": "prettier --write .",
43
+ "format:check": "prettier --check .",
44
+ "check": "npm run format:check && npm run typecheck && npm run lint && npm test",
45
+ "prepack": "npm run build",
46
+ "prepublishOnly": "npm run check"
47
+ },
48
+ "dependencies": {
49
+ "express": "^4.21.2",
50
+ "js-yaml": "^4.1.0"
51
+ },
52
+ "devDependencies": {
53
+ "@eslint/js": "^9.18.0",
54
+ "@types/js-yaml": "^4.0.9",
55
+ "@types/node": "^22.20.5",
56
+ "eslint": "^9.18.0",
57
+ "eslint-config-prettier": "^10.0.1",
58
+ "eslint-plugin-react-hooks": "^5.1.0",
59
+ "eslint-plugin-react-refresh": "^0.4.18",
60
+ "globals": "^15.14.0",
61
+ "prettier": "^3.4.2",
62
+ "typescript": "^5.7.3",
63
+ "typescript-eslint": "^8.21.0"
64
+ }
65
+ }
@@ -0,0 +1,51 @@
1
+ # Changelog — mihomo-dash-server
2
+
3
+ 本文件记录后端(CLI / HTTP / 内核管理)能力。跨端条目两端都写;前端见 `../web/CHANGELOG.md`。
4
+ 只维护一个 `## [Unreleased]` 段,每条一行,用现在时描述系统具备的能力。
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.6.1] - 2026-10-31
9
+
10
+ ### 修复
11
+
12
+ - CLI 与生成的服务定义里不再提「自启」页:`status` 的提示改为 `mihomo-dash service install`,
13
+ unit 头注释改为「由 `mihomo-dash service install` 生成」;`--help` 里 Debian 作用域不再只写 `--user`
14
+
15
+ ## [0.6.0] - 2026-10-31
16
+
17
+ ### 新功能
18
+
19
+ - `/api/service` 下线:开机自启的装/卸/状态只在 CLI(`mihomo-dash service …`),面板不再提供「自启」页
20
+
21
+ - 面板进程不再由自带守护进程管理:交给系统服务管理器(macOS launchd / Linux systemd --user),
22
+ 服务定义直接指向两个入口(`server/src/index.js` 与 `server/src/kernel/run.js`),
23
+ 在面板「自启」页一键安装;**升级后已装的自启服务要重装一次**
24
+ - `GET /api/health` 返回进程事实(版本 / pid / uptime / 启动时间),供 `status`、`doctor`、
25
+ `open` 与面板「自启」页探活
26
+ - CLI 面收敛成 `status` + `service`:新增 `service print|install|uninstall|status`;
27
+ **Debian 默认装 system 单元**(`/etc/systemd/system`,要 root,开机即起),`--user` 走
28
+ `~/.config/systemd/user`(不需要 root,开机即起靠 linger);macOS 固定用户级 LaunchAgent;
29
+ 安装 / 卸载都不停正在跑的进程;
30
+ `daemon` / `serve` / `doctor` / `open` 子命令全部移除;不带参数仍是 `status`
31
+ - `status` 与 `doctor` 的面板那一行改探 `/api/health`:不再读 pid 文件,也就没有
32
+ 「进程被 kill -9 后残留 pid」与「pid 复用误判」两种假象
33
+ - `server/src/index.js` 成为后端入口(`runPanel()`):手动起与服务管理器拉起走同一条代码路径
34
+
35
+ ### 工程
36
+
37
+ - npm 包名改成 `@lancewu602/mihomo-dash`(scoped;命令名仍是 `mihomo-dash`):workflow 的 publish 带
38
+ `--access public`;这是一个新包名,需要重新本地占名并给新包名配可信发布者
39
+
40
+ - 目录按 deck 对齐:`bin/` + `server/src/{cli,http,kernel,shared}` + `server/tests/`;
41
+ `server/` 与 `web/` 各有一份 `package.json`(`private`,各自锁文件),根 `package.json` 只留
42
+ `express` / `js-yaml` 与工具链
43
+ - 删除 `server/src/daemon/`(pid / meta / detached 拉起 / 日志那一层),内核启停保留在 `server/src/kernel/`
44
+ - 删除 `bin/mihomo-dash-runner.js` 与 `server/src/cli/runner.js`:服务定义直接指向真实入口,
45
+ `bin/` 只剩 CLI(与 deck 一致)
46
+ - 测试搬到 `server/tests/`,内核起停的等待间隔可用 `MIHOMO_DASH_READY_INTERVAL_MS` /
47
+ `MIHOMO_DASH_STOP_INTERVAL_MS` 调小(默认行为不变),装/卸内核的用例从等固定 100ms 改成轮询
48
+ - 工具配置收在根一份:`eslint.config.mjs`(React 规则按 `files` 限定到 `web/**`)、
49
+ `.prettierrc.json` / `.prettierignore` / `.editorconfig`;`check` 脚本串起
50
+ 格式检查 + 类型检查 + lint + 测试
51
+ - `server/tsconfig.json`:JS + JSDoc 的编辑器解析配置(`checkJs` 关着,打开会有 900+ 条历史错误)
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "mihomo-dash-server",
3
+ "version": "0.6.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "mihomo-dash 后端源码(作为 mihomo-dash 的一部分发布,不单独发包)",
7
+ "scripts": {
8
+ "start": "node src/index.js",
9
+ "dev": "node --watch src/index.js",
10
+ "test": "node --test tests/*.test.js",
11
+ "typecheck": "tsc --noEmit"
12
+ }
13
+ }
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ /** mihomo-dash 入口:子命令分发与异常兜底。不带参数 = status。 */
3
+ import { createRequire } from 'node:module';
4
+ import { parseArgs } from '../shared/args.js';
5
+ import { cmdStatus } from './status.js';
6
+ import { cmdService, serviceHelp } from './service.js';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const { version: PKG_VERSION } = require('../../../package.json');
10
+
11
+ /** `--port` / `--host` 指定要探的面板(默认 127.0.0.1:8981) */
12
+ function panelTargetFromFlags(flags) {
13
+ const target = {};
14
+ const port = Number(flags.port);
15
+ if (Number.isFinite(port) && port > 0) target.port = port;
16
+ if (typeof flags.host === 'string') target.host = flags.host;
17
+ return target;
18
+ }
19
+
20
+ const HELP = `mihomo-dash —— mihomo 管理工具:CLI + Web 面板
21
+
22
+ 用法: mihomo-dash [命令]
23
+
24
+ status 总览:内核 / 面板 / 系统代理 / 开机自启(默认命令;内核不通时退出码 1)
25
+ service <子命令> 开机自启:print / install / uninstall / status(macOS launchd · Debian systemd)
26
+
27
+ -V, --version 版本
28
+ -h, --help 帮助
29
+
30
+ 面板进程不再由自带的守护进程管理:交给系统服务管理器,服务定义指向 server/src/index.js(面板)
31
+ 与 server/src/kernel/run.js(内核),开机自启用上面的 service 子命令。
32
+ 内核的装 / 升 / 卸用面板的「内核」页,进程启停在侧栏底部;系统代理开关也不在 CLI(用面板顶栏的
33
+ 胶囊开关或配置页的「系统代理」卡片)。(面板默认 http://127.0.0.1:8981)
34
+
35
+ ${serviceHelp()}`;
36
+
37
+ export async function main(argv) {
38
+ const [cmd, ...rest] = argv;
39
+ const { positionals, flags } = parseArgs(rest);
40
+
41
+ if (cmd === '--version' || cmd === '-V' || flags.version === true) {
42
+ console.log(`mihomo-dash ${PKG_VERSION}`);
43
+ return;
44
+ }
45
+ if (flags.help || cmd === '--help' || cmd === '-h') {
46
+ console.log(HELP);
47
+ return;
48
+ }
49
+
50
+ try {
51
+ switch (cmd) {
52
+ case undefined:
53
+ case 'status':
54
+ await cmdStatus(panelTargetFromFlags(flags));
55
+ break;
56
+ case 'kernel':
57
+ console.error(
58
+ 'kernel 子命令已移除:内核的装 / 升 / 卸改到面板的「内核」页,进程启停改到侧栏底部',
59
+ );
60
+ process.exit(2);
61
+ break;
62
+ case 'service': {
63
+ const code = await cmdService(positionals, flags);
64
+ if (code !== 0) process.exit(code);
65
+ break;
66
+ }
67
+ default:
68
+ console.error(`未知命令: ${cmd}\n\n${HELP}`);
69
+ process.exit(2);
70
+ }
71
+ } catch (err) {
72
+ console.error(`错误: ${err?.message || err}`);
73
+ process.exit(1);
74
+ }
75
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * `mihomo-dash service <print|install|uninstall|status>`:开机自启的命令行入口。
3
+ *
4
+ * 与 deck 的 `deck service …` 思路一致(把「装服务定义」这件事从面板搬到脚本),
5
+ * 但平台面不一样,**不要照抄 deck 的实现**:
6
+ *
7
+ * - deck 只跑 Linux + **system** 单元(`/etc/systemd/system`,要 root);
8
+ * - 这里要同时支持 **macOS(launchd 用户级)与 Debian(systemd system 与 --user 两种)**:
9
+ * Debian 默认 system(要 root、开机即起),--user 是备选;macOS 只有用户级;
10
+ * 两个平台都交给 `createServiceManager()`(`server/src/shared/service.js`),CLI 只管打印结果。
11
+ * - 两个 job:`panel`(server/src/index.js)与 `kernel`(server/src/kernel/run.js)。
12
+ *
13
+ * 行为约定:**装/卸都不停正在跑的进程**(这个面板很可能正是被服务拉起来的),
14
+ * 「只开机拉起一次、不保活」(launchd 不写 KeepAlive、systemd 写 Restart=no)。
15
+ */
16
+ import { ownKernelRunning } from '../kernel/lifecycle.js';
17
+ import { parseArgs, parsePort } from '../shared/args.js';
18
+ import { probePanel } from '../shared/panel.js';
19
+ import { DEFAULT_HOST, DEFAULT_PORT } from '../shared/paths.js';
20
+ import { SERVICE_JOBS, createServiceManager } from '../shared/service.js';
21
+
22
+ const ACTIONS = ['print', 'install', 'uninstall', 'status'];
23
+
24
+ const HELP = `用法: mihomo-dash service <子命令> [--port <端口>] [--host <地址>] [--system|--user]
25
+
26
+ print [panel|kernel] 打印服务定义(macOS 是 launchd plist,Debian 是 systemd 单元)
27
+ install 安装两个服务定义并装载(不会停正在跑的进程)
28
+ uninstall 卸载两个服务定义(不 --now、不 bootout,正在跑的进程不动)
29
+ status 各 job 的定义/装载/运行状态
30
+
31
+ 作用域(只对 Debian 有意义):
32
+ --system 写 /etc/systemd/system,systemctl enable --now,**开机即起、不依赖 linger**
33
+ ——这是默认值,**需要 root**
34
+ --user 写 ~/.config/systemd/user,不需要 root;要开机即起得 sudo loginctl enable-linger
35
+ macOS 只有用户级 LaunchAgent(gui/<uid> 域),没有 system 域。
36
+
37
+ 日志:macOS 写 ~/.config/mihomo-dash/{daemon,kernel}.log · Debian 走 journalctl`;
38
+
39
+ /**
40
+ * @param {string[]} positionals
41
+ * @param {Record<string, string|boolean>} flags
42
+ * 返回值是**退出码**(0 成功 / 1 平台不支持或有错误 / 2 参数不认识),不在这里 `process.exit`:
43
+ * 这样同进程里能测,退出由 `index.js` 统一做。
44
+ * @param {{ manager?: ReturnType<typeof createServiceManager>, out?: (line: string) => void }} [deps]
45
+ * 测试可注入桩 manager 与输出
46
+ * @returns {Promise<number>}
47
+ */
48
+ export async function cmdService(positionals, flags, deps = {}) {
49
+ const [action, ...rest] = positionals;
50
+ const out = deps.out || ((line) => console.log(line));
51
+
52
+ // 作用域:--system / --user 二选一;不传时交给 manager 决定(Debian 是 root 就 system)
53
+ const wantSystem = flags.system === true;
54
+ const wantUser = flags.user === true;
55
+ if (wantSystem && wantUser) {
56
+ console.error('--system 与 --user 只能选一个');
57
+ return 2;
58
+ }
59
+ const scope = wantSystem ? 'system' : wantUser ? 'user' : undefined;
60
+ if (scope === 'system') {
61
+ const isRoot = (process.getuid?.() ?? -1) === 0;
62
+ if (process.platform === 'darwin') {
63
+ console.error('macOS 没有 system 域:开机自启固定用用户级 LaunchAgent(不要传 --system)');
64
+ return 2;
65
+ }
66
+ if (!isRoot) {
67
+ console.error(
68
+ '--system 要 root:sudo mihomo-dash service install --system(或去掉 --system 用用户级)',
69
+ );
70
+ return 1;
71
+ }
72
+ }
73
+
74
+ const manager = deps.manager || createServiceManager({ scope });
75
+
76
+ if (!ACTIONS.includes(action)) {
77
+ console.error(
78
+ `未知的 service 子命令:${action || '(缺省)'};可用:${ACTIONS.join(' / ')}\n\n${HELP}`,
79
+ );
80
+ return 2;
81
+ }
82
+
83
+ const port = parsePort(flags.port, DEFAULT_PORT);
84
+ const host = typeof flags.host === 'string' ? flags.host : DEFAULT_HOST;
85
+
86
+ if (action === 'print') {
87
+ const only = rest[0];
88
+ if (only && !SERVICE_JOBS.includes(only)) {
89
+ console.error(`未知的 job:${only}(只能是 ${SERVICE_JOBS.join(' / ')})`);
90
+ return 2;
91
+ }
92
+ const jobs = only ? [only] : SERVICE_JOBS;
93
+ for (const job of jobs) {
94
+ out(`# ${job}`);
95
+ out(manager.render(job, { port, host, scope: manager.scope }));
96
+ }
97
+ return 0;
98
+ }
99
+
100
+ if (action === 'install') {
101
+ // 正在手跑的 job 要告诉 manager:Linux 侧会只 enable 不 --now,免得留一个注定失败的单元
102
+ const manualRunning = [];
103
+ if ((await probePanel({ host, port })).running) manualRunning.push('panel');
104
+ if (ownKernelRunning().running) manualRunning.push('kernel');
105
+
106
+ const res = await manager.install({ port, host, manualRunning });
107
+ if (!res.supported) {
108
+ out(`本平台不支持:${res.hint}`);
109
+ return 1;
110
+ }
111
+ out(`平台:${manager.platform} · 作用域:${manager.scope} · 定义目录:${manager.dir}`);
112
+ for (const file of res.files) out(`已写入 ${file}`);
113
+ for (const action of res.actions) out(`· ${action}`);
114
+ for (const note of res.notes) out(`注意:${note}`);
115
+ for (const error of res.errors) out(`失败:${error}`);
116
+ if (res.errors.length > 0) return 1;
117
+ out('已安装。只开机拉起一次、不保活;要停止用面板侧栏的「停止内核」/ 服务管理器。');
118
+ return 0;
119
+ }
120
+
121
+ if (action === 'uninstall') {
122
+ const res = await manager.uninstall();
123
+ if (!res.supported) {
124
+ out(`本平台不支持:${res.hint}`);
125
+ return 1;
126
+ }
127
+ for (const file of res.removed) out(`已删除 ${file}`);
128
+ for (const action of res.actions) out(`· ${action}`);
129
+ for (const note of res.notes) out(`注意:${note}`);
130
+ for (const error of res.errors) out(`失败:${error}`);
131
+ return res.errors.length > 0 ? 1 : 0;
132
+ }
133
+
134
+ const res = await manager.status();
135
+ if (!res.supported) {
136
+ out(`本平台不支持:${res.hint}`);
137
+ return 1;
138
+ }
139
+ const statusScope = res.scope || 'user';
140
+ out(`平台:${res.platform} · 作用域:${statusScope} · 定义目录:${res.dir}`);
141
+ for (const job of res.jobs) {
142
+ const state = job.running ? 'running' : job.loaded ? 'loaded' : 'not loaded';
143
+ out(
144
+ `${job.job.padEnd(6)} ${job.installed ? '已安装' : '未安装'} · ${state}` +
145
+ (job.pid ? ` · pid ${job.pid}` : '') +
146
+ ` · ${job.file}`,
147
+ );
148
+ }
149
+ if (res.platform === 'linux' && statusScope === 'user') {
150
+ out(
151
+ res.linger
152
+ ? 'linger 已开启:开机即起(无需登录)'
153
+ : 'linger 未开启:只在登录后启动;要开机即起跑 sudo loginctl enable-linger $USER',
154
+ );
155
+ }
156
+ return 0;
157
+ }
158
+
159
+ /** 供 `--help` 复用 */
160
+ export function serviceHelp() {
161
+ return HELP;
162
+ }
163
+
164
+ /** 解析 `service` 后面的参数(index.js 里只做分发,解析放这里便于单测) */
165
+ export function parseServiceArgs(argv) {
166
+ return parseArgs(argv);
167
+ }