Bladeren bron

修复bug

moki 1 week geleden
bovenliggende
commit
b3eb2aed4f
8 gewijzigde bestanden met toevoegingen van 232 en 76 verwijderingen
  1. 3 3
      README.md
  2. 15 9
      cmd/monitor/main.go
  3. 86 25
      docs/api.md
  4. 5 0
      go.mod
  5. 5 10
      go.sum
  6. 6 16
      internal/api/api.go
  7. 8 13
      internal/database/database.go
  8. 104 0
      internal/discovery/discovery_darwin.go

+ 3 - 3
README.md

@@ -189,9 +189,9 @@ BLE 中继已嵌入 Go 二进制(使用 `make build-with-ble` 构建时),
 ./opencode-monitor config ble set [选项]
 
 选项:
-  --device          蓝牙设备名称 (默认 "AI-Light")
-  --service-uuid    BLE 服务 UUID (默认 "b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001")
-  --char-uuid       BLE 特征 UUID (默认 "b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001")
+  --device          蓝牙设备名称 (必填)
+  --service-uuid    BLE 服务 UUID (必填)
+  --char-uuid       BLE 特征 UUID (必填)
   --enabled         是否启用 (true/false, 默认 true)
 ```
 

+ 15 - 9
cmd/monitor/main.go

@@ -399,9 +399,9 @@ func printConfigUsage() {
 	fmt.Println("  --enabled         是否启用 (true/false)")
 	fmt.Println("")
 	fmt.Println("BLE 选项:")
-	fmt.Println("  --device          蓝牙设备名称 (默认: AI-Light)")
-	fmt.Println("  --service-uuid    BLE 服务 UUID")
-	fmt.Println("  --char-uuid       BLE 特征 UUID")
+	fmt.Println("  --device          蓝牙设备名称 (必填)")
+	fmt.Println("  --service-uuid    BLE 服务 UUID (必填)")
+	fmt.Println("  --char-uuid       BLE 特征 UUID (必填)")
 	fmt.Println("  --enabled         是否启用 (true/false)")
 	fmt.Println("")
 	fmt.Println("全局选项:")
@@ -499,12 +499,18 @@ func runBleConfig(db *database.DB, args []string) {
 
 	case "set":
 		fs := flag.NewFlagSet("config ble set", flag.ExitOnError)
-		deviceName := fs.String("device", "AI-Light", "蓝牙设备名称")
-		serviceUUID := fs.String("service-uuid", "b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001", "BLE 服务 UUID")
-		charUUID := fs.String("char-uuid", "b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001", "BLE 特征 UUID")
+		deviceName := fs.String("device", "", "蓝牙设备名称 (必填)")
+		serviceUUID := fs.String("service-uuid", "", "BLE 服务 UUID (必填)")
+		charUUID := fs.String("char-uuid", "", "BLE 特征 UUID (必填)")
 		enabled := fs.Bool("enabled", true, "是否启用")
 		fs.Parse(args[1:])
 
+		if *deviceName == "" || *serviceUUID == "" || *charUUID == "" {
+			fmt.Println("错误: --device, --service-uuid, --char-uuid 为必填参数")
+			fs.Usage()
+			return
+		}
+
 		cfg := &database.BLEConfig{
 			DeviceName:  *deviceName,
 			ServiceUUID: *serviceUUID,
@@ -553,9 +559,9 @@ func printBleConfigUsage() {
 	fmt.Println("  delete <id>       删除 BLE 配置")
 	fmt.Println("")
 	fmt.Println("选项:")
-	fmt.Println("  --device          蓝牙设备名称 (默认: AI-Light)")
-	fmt.Println("  --service-uuid    BLE 服务 UUID")
-	fmt.Println("  --char-uuid       BLE 特征 UUID")
+	fmt.Println("  --device          蓝牙设备名称 (必填)")
+	fmt.Println("  --service-uuid    BLE 服务 UUID (必填)")
+	fmt.Println("  --char-uuid       BLE 特征 UUID (必填)")
 	fmt.Println("  --enabled         是否启用 (true/false)")
 }
 

+ 86 - 25
docs/api.md

@@ -22,7 +22,29 @@ GET /api/health
 }
 ```
 
-### 2. 获取所有 MQTT 配置
+### 2. 获取所有客户端状态
+
+```
+GET /api/clients
+```
+
+**响应示例:**
+```json
+{
+  "code": 0,
+  "message": "success",
+  "data": [
+    {
+      "port": 4096,
+      "status": "工作中",
+      "code": "busy",
+      "timestamp": "2026-06-03T14:30:00Z"
+    }
+  ]
+}
+```
+
+### 3. 获取所有 MQTT 配置
 
 ```
 GET /api/mqtt
@@ -47,7 +69,7 @@ GET /api/mqtt
 }
 ```
 
-### 3. 创建 MQTT 配置
+### 4. 创建 MQTT 配置
 
 ```
 POST /api/mqtt
@@ -91,7 +113,7 @@ POST /api/mqtt
 }
 ```
 
-### 4. 获取单个 MQTT 配置
+### 5. 获取单个 MQTT 配置
 
 ```
 GET /api/mqtt/:id
@@ -119,7 +141,7 @@ GET /api/mqtt/:id
 }
 ```
 
-### 5. 更新 MQTT 配置
+### 6. 更新 MQTT 配置
 
 ```
 PUT /api/mqtt/:id
@@ -159,7 +181,7 @@ PUT /api/mqtt/:id
 }
 ```
 
-### 6. 删除 MQTT 配置
+### 7. 删除 MQTT 配置
 
 ```
 DELETE /api/mqtt/:id
@@ -178,7 +200,7 @@ DELETE /api/mqtt/:id
 }
 ```
 
-### 7. 获取所有 BLE 配置
+### 8. 获取所有 BLE 配置
 
 ```
 GET /api/ble
@@ -201,7 +223,7 @@ GET /api/ble
 }
 ```
 
-### 8. 创建 BLE 配置
+### 9. 创建 BLE 配置
 
 ```
 POST /api/ble
@@ -217,12 +239,12 @@ POST /api/ble
 }
 ```
 
-| 字段 | 类型 | 必填 | 默认值 | 说明 |
-|------|------|------|--------|------|
-| device_name | string | 否 | AI-Light | BLE 设备名称 |
-| service_uuid | string | 否 | b8b7e001-... | BLE 服务 UUID |
-| char_uuid | string | 否 | b8b7e002-... | BLE 特征 UUID |
-| enabled | boolean | 否 | true | 是否启用 |
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| device_name | string |  | BLE 设备名称 |
+| service_uuid | string |  | BLE 服务 UUID |
+| char_uuid | string |  | BLE 特征 UUID |
+| enabled | boolean | 否 | 是否启用 (默认 true) |
 
 **响应示例:**
 ```json
@@ -239,7 +261,7 @@ POST /api/ble
 }
 ```
 
-### 9. 获取单个 BLE 配置
+### 10. 获取单个 BLE 配置
 
 ```
 GET /api/ble/:id
@@ -250,7 +272,7 @@ GET /api/ble/:id
 |------|------|------|
 | id | integer | 配置 ID |
 
-### 10. 更新 BLE 配置
+### 11. 更新 BLE 配置
 
 ```
 PUT /api/ble/:id
@@ -271,7 +293,7 @@ PUT /api/ble/:id
 }
 ```
 
-### 11. 删除 BLE 配置
+### 12. 删除 BLE 配置
 
 ```
 DELETE /api/ble/:id
@@ -300,8 +322,51 @@ DELETE /api/ble/:id
 
 # 方式2: 监控时同时启动 API 服务
 ./bin/opencode-monitor monitor --ports 4096 --api-addr :8080
+
+# 方式3: 启用 HTTPS (自签名证书)
+./bin/opencode-monitor serve --addr :8080 --tls
 ```
 
+## 子命令选项
+
+### serve 子命令
+
+| 选项 | 默认值 | 说明 |
+|------|--------|------|
+| `--addr` | `:8080` | 监听地址 |
+| `--db` | `./data/config.db` | 数据库路径 |
+| `--tls` | `false` | 启用 HTTPS (使用自签名证书) |
+| `--tls-cert` | `./data/tls/cert.pem` | TLS 证书文件路径 |
+| `--tls-key` | `./data/tls/key.pem` | TLS 私钥文件路径 |
+
+### monitor 子命令
+
+| 选项 | 默认值 | 说明 |
+|------|--------|------|
+| `--host` | `127.0.0.1` | 主机地址 |
+| `--ports` | - | 端口列表,逗号分隔 (如: 4096,4097,4098) |
+| `--scan` | - | 扫描端口范围 (如: 4096-4100) |
+| `--interval` | `1` | 动态扫描间隔(秒) |
+| `--api-addr` | - | API 服务地址 (如: :8080) |
+| `--db` | `./data/config.db` | 数据库路径 |
+| `--tls` | `false` | 启用 HTTPS (使用自签名证书) |
+| `--tls-cert` | `./data/tls/cert.pem` | TLS 证书文件路径 |
+| `--tls-key` | `./data/tls/key.pem` | TLS 私钥文件路径 |
+
+### config 子命令
+
+| 选项 | 默认值 | 说明 |
+|------|--------|------|
+| `--db` | `./data/config.db` | 数据库路径 |
+
+### version 子命令
+
+```bash
+./bin/opencode-monitor version
+```
+
+显示版本信息。
+
 ## 全局选项
 
 以下选项适用于 `monitor`、`serve`、`config` 三个子命令:
@@ -379,15 +444,11 @@ API 已启用 CORS,支持跨域请求。
 # 一键构建(打包 Python 脚本 + 嵌入 Go 二进制)
 make build-with-ble
 
-# 或手动两步
-scripts\build_ble_relay.bat
-go build -tags ble -o bin/opencode-monitor.exe ./cmd/monitor
-```
-
-### Python 依赖(仅开发时需要)
-
-```bash
-pip install -r scripts/requirements.txt
+# 或手动步骤
+python -m PyInstaller --onefile --name ble_relay --distpath bin scripts/ble_relay.py
+cp bin/ble_relay cmd/monitor/ble_relay
+go build -tags ble -o bin/opencode-monitor ./cmd/monitor
+rm -f cmd/monitor/ble_relay
 ```
 
 ### 状态映射

+ 5 - 0
go.mod

@@ -5,15 +5,20 @@ go 1.21
 require (
 	github.com/eclipse/paho.mqtt.golang v1.4.3
 	github.com/gorilla/websocket v1.5.0
+	github.com/muka/go-bluetooth v0.0.0-20240701044517-04c4f09c514e
 	modernc.org/sqlite v1.24.0
 )
 
 require (
 	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/fatih/structs v1.1.0 // indirect
+	github.com/godbus/dbus/v5 v5.0.3 // indirect
 	github.com/google/uuid v1.3.0 // indirect
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
+	github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
 	github.com/mattn/go-isatty v0.0.16 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+	github.com/sirupsen/logrus v1.6.0 // indirect
 	golang.org/x/mod v0.3.0 // indirect
 	golang.org/x/net v0.8.0 // indirect
 	golang.org/x/sync v0.1.0 // indirect

+ 5 - 10
go.sum

@@ -41,20 +41,16 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
 github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
-github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
@@ -80,17 +76,16 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
+golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs=
 golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
-golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
 lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
 modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=

+ 6 - 16
internal/api/api.go

@@ -326,14 +326,9 @@ func (s *Server) createBLEConfig(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if cfg.DeviceName == "" {
-		cfg.DeviceName = "AI-Light"
-	}
-	if cfg.ServiceUUID == "" {
-		cfg.ServiceUUID = "b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001"
-	}
-	if cfg.CharUUID == "" {
-		cfg.CharUUID = "b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001"
+	if cfg.DeviceName == "" || cfg.ServiceUUID == "" || cfg.CharUUID == "" {
+		writeJSON(w, http.StatusBadRequest, Response{Code: -1, Message: "device_name, service_uuid, char_uuid 不能为空"})
+		return
 	}
 
 	if err := s.db.SaveBLEConfig(&cfg); err != nil {
@@ -376,14 +371,9 @@ func (s *Server) updateBLEConfig(w http.ResponseWriter, r *http.Request, id int)
 
 	cfg.ID = id
 
-	if cfg.DeviceName == "" {
-		cfg.DeviceName = "AI-Light"
-	}
-	if cfg.ServiceUUID == "" {
-		cfg.ServiceUUID = "b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001"
-	}
-	if cfg.CharUUID == "" {
-		cfg.CharUUID = "b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001"
+	if cfg.DeviceName == "" || cfg.ServiceUUID == "" || cfg.CharUUID == "" {
+		writeJSON(w, http.StatusBadRequest, Response{Code: -1, Message: "device_name, service_uuid, char_uuid 不能为空"})
+		return
 	}
 
 	if err := s.db.SaveBLEConfig(&cfg); err != nil {

+ 8 - 13
internal/database/database.go

@@ -88,9 +88,9 @@ func (d *DB) init() error {
 	bleQuery := `
 	CREATE TABLE IF NOT EXISTS ble_config (
 		id INTEGER PRIMARY KEY AUTOINCREMENT,
-		device_name TEXT NOT NULL DEFAULT 'AI-Light',
-		service_uuid TEXT NOT NULL DEFAULT 'b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001',
-		char_uuid TEXT NOT NULL DEFAULT 'b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001',
+		device_name TEXT NOT NULL,
+		service_uuid TEXT NOT NULL,
+		char_uuid TEXT NOT NULL,
 		enabled BOOLEAN DEFAULT 1
 	);
 	`
@@ -99,6 +99,11 @@ func (d *DB) init() error {
 		return err
 	}
 
+	for _, col := range []string{"service_uuid", "char_uuid"} {
+		alterQuery := fmt.Sprintf("ALTER TABLE ble_config ADD COLUMN %s TEXT NOT NULL DEFAULT ''", col)
+		d.conn.ExecContext(context.Background(), alterQuery)
+	}
+
 	return nil
 }
 
@@ -197,16 +202,6 @@ func (d *DB) GetBLEConfig() (*BLEConfig, error) {
 }
 
 func (d *DB) SaveBLEConfig(cfg *BLEConfig) error {
-	if cfg.DeviceName == "" {
-		cfg.DeviceName = "AI-Light"
-	}
-	if cfg.ServiceUUID == "" {
-		cfg.ServiceUUID = "b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001"
-	}
-	if cfg.CharUUID == "" {
-		cfg.CharUUID = "b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001"
-	}
-
 	if cfg.ID == 0 {
 		query := "INSERT INTO ble_config (device_name, service_uuid, char_uuid, enabled) VALUES (?, ?, ?, ?)"
 		_, err := d.conn.ExecContext(context.Background(), query, cfg.DeviceName, cfg.ServiceUUID, cfg.CharUUID, cfg.Enabled)

+ 104 - 0
internal/discovery/discovery_darwin.go

@@ -0,0 +1,104 @@
+//go:build darwin
+
+package discovery
+
+import (
+	"os/exec"
+	"regexp"
+	"sort"
+	"strconv"
+	"strings"
+
+	"ai-status-light/internal/logger"
+)
+
+func findByPID() []int {
+	var ports []int
+	pidRegex := regexp.MustCompile(`(?:^|/)opencode(?:\s|$)`)
+
+	out, err := exec.Command("ps", "aux").Output()
+	if err != nil {
+		logger.Debug("执行 ps aux 失败: %v", err)
+		return ports
+	}
+
+	var pids []int
+	for _, line := range strings.Split(string(out), "\n") {
+		if pidRegex.MatchString(line) && !strings.Contains(line, "grep") && !strings.Contains(line, "opencode-monitor") {
+			fields := strings.Fields(line)
+			if len(fields) > 1 {
+				if pid, err := strconv.Atoi(fields[1]); err == nil {
+					pids = append(pids, pid)
+				}
+			}
+		}
+	}
+
+	if len(pids) == 0 {
+		logger.Debug("未找到 opencode 进程")
+		return ports
+	}
+	logger.Debug("发现 opencode 进程 PID: %v", pids)
+
+	for _, pid := range pids {
+		lsofOut, err := exec.Command("lsof", "-p", strconv.Itoa(pid), "-i", "TCP", "-s", "TCP:LISTEN").Output()
+		if err != nil {
+			logger.Debug("执行 lsof 失败: %v", err)
+			continue
+		}
+
+		for _, line := range strings.Split(string(lsofOut), "\n") {
+			if strings.Contains(line, "LISTEN") {
+				// lsof 输出格式: COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
+				// NAME 部分格式: *:port 或 host:port
+				fields := strings.Fields(line)
+				if len(fields) >= 9 {
+					nameField := fields[len(fields)-1]
+					if strings.Contains(nameField, ":") {
+						parts := strings.Split(nameField, ":")
+						if len(parts) >= 2 {
+							if port, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
+								if port > 0 && port < 65535 && !contains(ports, port) {
+									ports = append(ports, port)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+
+	sort.Ints(ports)
+	return ports
+}
+
+func findByCmdline() []int {
+	var ports []int
+
+	out, err := exec.Command("ps", "aux").Output()
+	if err != nil {
+		logger.Debug("执行 ps aux 失败: %v", err)
+		return ports
+	}
+
+	for _, line := range strings.Split(string(out), "\n") {
+		if strings.Contains(line, "opencode") && !strings.Contains(line, "opencode-monitor") && !strings.Contains(line, "grep") {
+			if strings.Contains(line, "--port") {
+				fields := strings.Fields(line)
+				for i, f := range fields {
+					if f == "--port" && i+1 < len(fields) {
+						if port, err := strconv.Atoi(fields[i+1]); err == nil {
+							if !contains(ports, port) {
+								ports = append(ports, port)
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+
+	sort.Ints(ports)
+	return ports
+}