Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

## **API 说明**

| **端点** | **方法** | **功能** | **参数** | **成功返回** | **失败返回** |
| ------------- | ---------- | ---------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **`/screen`** | `GET` | 获取屏幕截图 | - `r`(高斯模糊半径)<br>- `k`(API 密钥) | - `200 OK`,返回 `image/jpeg` 截图 | - `401 Unauthorized`:配置了 `api_key` 且低模糊度密钥错误<br>- `403 Forbidden`:私密模式<br>- `500 Internal Server Error`:截图失败 |
| **`/record`** | `GET` | 获取最近录音 | 无 | - `200 OK`,返回 `audio/wav` 录音文件 | - `403 Forbidden`:私密模式<br>- `500 Internal Server Error`:录音失败 |
| **`/idle`** | `GET` | 获取用户空闲时间 | 无 | - `200 OK`,返回 JSON:`{"idle_seconds": 123.456, "last_input_time": "2026-02-09T23:00:00+08:00"}` | - `403 Forbidden`:私密模式 |
| **`/check`** | `GET/POST` | 检查是否运行 | 无 | - `200 OK` | 无 |
| **端点** | **方法** | **功能** | **参数** | **成功返回** | **失败返回** |
| ------------- | ---------- | ---------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **`/screen`** | `GET` | 获取屏幕截图 | - `r`(高斯模糊半径)<br>- `k`(API 密钥) | - `200 OK`,返回 `image/jpeg` 截图 | - `401 Unauthorized`:配置了 `api_key` 且低模糊度密钥错误<br>- `403 Forbidden`:私密模式<br>- `500 Internal Server Error`:截图失败 |
| **`/record`** | `GET` | 获取最近录音 | 无 | - `200 OK`,返回 `audio/wav` 录音文件 | - `403 Forbidden`:私密模式<br>- `500 Internal Server Error`:录音失败 |
| **`/idle`** | `GET` | 获取用户空闲时间 | 无 | - `200 OK`,返回 JSON:`{"idle_seconds": 123.456, "last_input_time": "..."}` | - `403 Forbidden`:私密模式 |
| **`/info`** | `GET` | 获取设备信息 | 无 | - `200 OK`,返回 JSON:`{"hostname": "PC", "cpu": "Intel...", "gpus": [...]}` | - `403 Forbidden`:私密模式 |
| **`/check`** | `GET/POST` | 检查是否运行 | 无 | - `200 OK` | 无 |

## **使用**

Expand Down Expand Up @@ -50,6 +51,7 @@ uv run pyinstaller peekapi.spec
```toml
[basic]
is_public = true # 程序启动时默认是否为公开模式
device_name = "" # 设备名称,留空则使用系统主机名
api_key = "" # 低模糊度下获取截图的key,留空则不需要key
host = "0.0.0.0" # 监听IP
port = 1920 # 监听端口
Expand All @@ -68,6 +70,7 @@ gain = 20 # 音量增益倍数
| **参数** | **说明** | **默认值** |
| ---------------------- | -------------------------------------------------- | ----------- |
| **`is_public`** | 程序启动时默认是否为公开模式 | `true` |
| **`device_name`** | 设备名称,留空则使用系统主机名 | `""` |
| **`api_key`** | 低模糊度下获取截图的密钥,留空则不需要key | `""` |
| **`host`** | 监听 IP | `"0.0.0.0"` |
| **`port`** | 监听端口 | `1920` |
Expand Down
17 changes: 16 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,13 @@ check:

# ===== 版本管理 =====

# 版本发布(更新版本号、更新 lock 文件)
# 版本发布(更新版本号、更新 lock 文件、自动修正标签
bump:
uv run cz bump
uv lock
git add uv.lock
git commit --amend --no-edit --no-verify
$version = (uv run cz version --project).Trim(); git tag -f "v$version"

# 生成 changelog
changelog:
Expand All @@ -74,3 +77,15 @@ hooks:
# 更新 prek hooks
update:
uv run prek auto-update

# 从 dev 向 main 创建 PR
pr:
gh pr create --base main --head dev --fill
gh pr view --web

# PR 合并后同步 dev 到 main
sync:
git fetch origin
git checkout dev
git reset --hard origin/main
git push origin dev --force-with-lease
5 changes: 5 additions & 0 deletions memory-bank/activeContext.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@

项目处于稳定运行状态,核心功能已完成,CI 检查全部通过。当前关注点:
- CI/CD 配置优化(已完成)
- 添加设备信息端点 (`/info`)(已完成)
- Linux 平台支持(计划中)
- GitHub Release 自动发布(计划中)

## 最近变更

- 2026-02-10: 完成设备信息端点 (`/info`)
- 使用 PowerShell WMI 查询硬件信息
- 返回主机名、电脑型号、主板、CPU、显卡
- 支持 device_name 配置覆盖主机名
- 2026-02-09: 添加用户空闲时间端点 (`/idle`)
- 使用 Windows GetLastInputInfo API 获取最后操作时间
- 返回空闲秒数和最后操作时间 (ISO 格式)
Expand Down
18 changes: 11 additions & 7 deletions memory-bank/tasks/TASK010-github-release-exe.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# [TASK010] - GitHub Release 自动发布 EXE

**Status:** Pending
**Status:** Completed
**Added:** 2026-02-02
**Updated:** 2026-02-02
**Updated:** 2026-02-10

## Original Request

Expand Down Expand Up @@ -220,14 +220,14 @@ jobs:
## Implementation Plan

- [x] 1.1 创建基础 release.yml 工作流文件(含版本校验逻辑)
- [ ] 1.2 集成 TheDoctor0/zip-release 打包
- [ ] 1.3 配置 Release 产出物名称
- [ ] 1.4 可选:添加 SHA256 校验和
- [ ] 1.5 测试完整发布流程
- [x] 1.2 集成 TheDoctor0/zip-release 打包
- [x] 1.3 配置 Release 产出物名称
- [-] 1.4 可选:添加 SHA256 校验和 (已跳过)
- [x] 1.5 测试完整发布流程

## Progress Tracking

**Overall Status:** Pending - 15%
**Overall Status:** Completed - 100%

### Subtasks
| ID | Description | Status | Updated | Notes |
Expand All @@ -250,3 +250,7 @@ jobs:
- 参数: filename, directory, path, type, exclusions 等
- 设计完整的发布方案,采用 TheDoctor0/zip-release 作为压缩方案

### 2026-02-10
- 完成 release.yml 配置,修复版本检查逻辑
- 标记任务完成

89 changes: 89 additions & 0 deletions memory-bank/tasks/TASK014-fix-gettickcount-overflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# [TASK014] - 修复 GetTickCount 溢出问题

**Status:** Completed
**Added:** 2026-02-10
**Updated:** 2026-02-10
**Priority:** High

## Original Request

`GetTickCount` 默认 `restype` 是有符号 `c_int`,在系统运行超过约 24.9 天后会变成负数,或在 49.7 天左右发生回绕,导致 `idle_seconds` 计算错误甚至为负数。

## 问题分析

### 当前代码(修复前)

```python
# idle.py 第 38-40 行
current_tick = ctypes.windll.kernel32.GetTickCount()
idle_ms = current_tick - lii.dwTime
idle_seconds = idle_ms / 1000.0
```

### 问题

1. `ctypes.windll.kernel32.GetTickCount()` 默认返回类型是 `c_int`(有符号 32 位)
2. `GetTickCount` 实际返回 `DWORD`(无符号 32 位),范围 0 ~ 4,294,967,295
3. 当值超过 2,147,483,647(约 24.9 天)时,Python 会将其解释为负数
4. 49.7 天后发生回绕(wrap around),导致计算错误

### 影响

- 系统运行超过 ~25 天后,`idle_seconds` 可能变为负数
- 用户操作时间显示错误
- 多主机选择功能(nonebot-plugin-peek TASK003)依赖此端点,会导致选择错误主机

## Implementation Plan

### 采用方案 A:使用 GetTickCount64

```python
def get_idle_info() -> tuple[float, datetime]:
lii = LASTINPUTINFO()
lii.cbSize = sizeof(LASTINPUTINFO)
ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lii))

# 使用 GetTickCount64 避免溢出(返回 64 位无符号整数)
GetTickCount64 = ctypes.windll.kernel32.GetTickCount64
GetTickCount64.restype = ctypes.c_uint64
current_tick = GetTickCount64()

# dwTime 仍是 32 位,需处理回绕
current_tick_32 = current_tick & 0xFFFFFFFF
if current_tick_32 >= lii.dwTime:
idle_ms = current_tick_32 - lii.dwTime
else:
# 回绕:dwTime 在 current_tick 回绕之前设置
idle_ms = (0xFFFFFFFF - lii.dwTime) + current_tick_32 + 1

idle_seconds = idle_ms / 1000.0
last_input_time = datetime.now(_BEIJING_TZ) - timedelta(seconds=idle_seconds)

return idle_seconds, last_input_time
```

## Progress Tracking

**Overall Status:** Completed - 100%

### Subtasks

| ID | Description | Status | Updated | Notes |
| --- | -------------------------------- | -------- | ---------- | --------------------- |
| 1 | 修改 idle.py 使用 GetTickCount64 | Complete | 2026-02-10 | 使用 c_uint64 |
| 2 | 添加 32 位回绕处理 | Complete | 2026-02-10 | dwTime 仍是 32 位 |
| 3 | 添加单元测试覆盖边界情况 | Complete | 2026-02-10 | 增加 2 个回绕测试用例 |
| 4 | 更新文档 | Complete | 2026-02-10 | 任务文档已更新 |

## Progress Log

### 2026-02-10
- 创建任务
- 分析问题根因
- 设计修复方案
- 实现 GetTickCount64 修复(方案 A)
- 添加 32 位回绕边界测试用例
- `test_get_idle_info_handles_32bit_wraparound` - 测试回绕情况
- `test_get_idle_info_large_tick_count` - 测试接近上限值
- 所有 7 个 idle 测试通过
- 任务完成
43 changes: 43 additions & 0 deletions memory-bank/tasks/TASK015-add-system-info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# TASK015 - 添加设备信息端点与主机名配置

**Status:** Completed
**Added:** 2026-02-09
**Updated:** 2026-02-10

## Original Request
用户希望通过 API 获取设备硬件信息(主机名、型号、主板、CPU、显卡),并希望在配置文件中添加一个选项,如果设置了该选项,API 返回的主机名将使用配置值替代系统真实主机名。

## Thought Process
用户需要了解运行服务的宿主机硬件情况,用于多设备管理或展示。
考虑到跨平台兼容性和轻量化,决定使用 PowerShell 命令获取 Windows WMI 信息,无需额外依赖。
配置项 `device_name` 添加到 `[basic]` 节中,留空则使用系统主机名。

## Implementation Plan
- [x] Update `README.md` to document the new `/info` endpoint and `device_name` config.
- [x] Create `src/peekapi/system_info.py` to handle PowerShell commands and parsing.
- [x] Update `src/peekapi/config.py` to include `device_name` field.
- [x] Add `/info` route in `src/peekapi/server.py`.
- [x] Add unit tests for system info retrieval and config override.

## Progress Tracking

**Overall Status:** Completed - 100%

### Subtasks
| ID | Description | Status | Updated | Notes |
| --- | ---------------------- | -------- | ---------- | ------------------------------ |
| 1 | Update Documentation | Complete | 2026-02-10 | README, TASK 文件 |
| 2 | Implement System Info | Complete | 2026-02-10 | system_info.py 使用 PowerShell |
| 3 | Implement Config Logic | Complete | 2026-02-10 | device_name 字段 |
| 4 | Add API Endpoint | Complete | 2026-02-10 | /info 路由 |
| 5 | Add Unit Tests | Complete | 2026-02-10 | 13 测试通过 |

## Progress Log
### 2026-02-10
- Implemented `system_info.py` module with PowerShell WMI queries
- Added `device_name` config option to `BasicConfig`
- Added `/info` endpoint to `server.py`
- Created comprehensive unit tests (13 tests)
- Fixed existing mock test for `test_idle.py` (GetTickCount64 compatibility)
- All 106 tests pass
- ruff and basedpyright checks pass
12 changes: 7 additions & 5 deletions memory-bank/tasks/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
## Pending

- [TASK001] 支持 Linux 平台 - 替换 Windows 专用依赖,实现跨平台支持
- [TASK010] GitHub Release 自动发布 EXE - 配置 CI 自动打包并发布到 GitHub Releases

---

## Completed

- [TASK014] 修复 GetTickCount 溢出问题 - ✅ 2026-02-10 完成(使用 GetTickCount64)
- [TASK010] GitHub Release 自动发布 EXE - ✅ 2026-02-10 完成(配置 CI 自动打包并发布到 GitHub Releases)
- [TASK015] 添加设备信息端点 - ✅ 2026-02-10 完成(/info 端点获取硬件信息)
- [TASK013] 添加用户空闲时间端点 - ✅ 2026-02-09 完成(/idle 端点获取用户空闲时间)
- [TASK012] 系统托盘图标不显示 - ✅ 2026-02-03 完成(修复 stderr 为 None 导致崩溃)
- [TASK011] CI 代码检查修复 - ✅ 2026-02-02 完成(ruff、basedpyright、pytest 全部通过)
Expand All @@ -49,11 +51,11 @@
| 状态 | 数量 |
| ----------- | ------ |
| In Progress | 0 |
| Pending | 2 |
| Completed | 10 |
| Pending | 1 |
| Completed | 13 |
| Abandoned | 0 |
| **总计** | **12** |
| **总计** | **14** |

---

*最后更新: 2026-02-09*
*最后更新: 2026-02-10*
1 change: 1 addition & 0 deletions src/peekapi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class BasicConfig(Struct):
"""基础配置"""

is_public: bool = True
device_name: str = "" # 设备名称,留空则使用系统主机名
api_key: str = ""
host: str = "0.0.0.0"
port: int = 1920
Expand Down
17 changes: 14 additions & 3 deletions src/peekapi/idle.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,20 @@ def get_idle_info() -> tuple[float, datetime]:
lii.cbSize = sizeof(LASTINPUTINFO)
ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lii))

# GetTickCount 返回系统启动后的毫秒数
current_tick = ctypes.windll.kernel32.GetTickCount()
idle_ms = current_tick - lii.dwTime
# 使用 GetTickCount64 避免 49.7 天溢出问题
GetTickCount64 = ctypes.windll.kernel32.GetTickCount64
GetTickCount64.restype = ctypes.c_uint64
current_tick = GetTickCount64()

# dwTime 是 32 位,需要处理回绕
# 取 current_tick 的低 32 位与 dwTime 比较
current_tick_32 = current_tick & 0xFFFFFFFF
if current_tick_32 >= lii.dwTime:
idle_ms = current_tick_32 - lii.dwTime
else:
# 回绕情况:dwTime 在 GetTickCount 回绕之前设置
idle_ms = (0xFFFFFFFF - lii.dwTime) + current_tick_32 + 1

idle_seconds = idle_ms / 1000.0

# 计算最后操作时间
Expand Down
3 changes: 2 additions & 1 deletion src/peekapi/screenshot.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import io
import math

import mss
from PIL import Image, ImageFilter
Expand All @@ -15,7 +16,7 @@ def screenshot(radius: float, main_screen_only: bool) -> bytes:

img_pil = Image.frombytes("RGB", img.size, img.rgb)

if radius > 0:
if math.isfinite(radius) and radius > 0:
img_pil = img_pil.filter(ImageFilter.GaussianBlur(radius=radius))

img_byte = io.BytesIO()
Expand Down
21 changes: 21 additions & 0 deletions src/peekapi/server.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import math
from contextlib import asynccontextmanager
from threading import Thread

Expand All @@ -10,6 +11,7 @@
from .logging import logger, setup_logging
from .record import recorder
from .screenshot import screenshot
from .system_info import get_system_info
from .system_tray import start_system_tray


Expand Down Expand Up @@ -53,6 +55,11 @@ def screen_route(
"""获取屏幕截图"""
client_ip = request.client.host if request.client else "unknown"

# 拒绝 NaN / Inf 等非有限浮点数,防止绕过鉴权和模糊
if not math.isfinite(r):
logger.info(f"[{client_ip}] 截图请求被拒绝: 非法半径值 (r={r})")
raise HTTPException(status_code=401, detail="模糊半径必须为有限数值")

if not config.basic.is_public:
logger.info(f"[{client_ip}] 截图请求被拒绝: 私密模式")
raise HTTPException(status_code=403, detail="瑟瑟中")
Expand Down Expand Up @@ -112,6 +119,20 @@ def idle_route(request: Request):
}


@app.get("/info")
def info_route(request: Request):
"""获取设备信息"""
client_ip = request.client.host if request.client else "unknown"

if not config.basic.is_public:
logger.info(f"[{client_ip}] 设备信息请求被拒绝: 私密模式")
raise HTTPException(status_code=403, detail="瑟瑟中")

info = get_system_info(config.basic.device_name)
logger.info(f"[{client_ip}] 设备信息请求成功")
return info


@app.get("/check")
@app.post("/check")
def check_route():
Expand Down
Loading