bridgeflow 0.1.0__tar.gz
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.
- bridgeflow-0.1.0/PKG-INFO +174 -0
- bridgeflow-0.1.0/README.md +164 -0
- bridgeflow-0.1.0/pyproject.toml +26 -0
- bridgeflow-0.1.0/setup.cfg +4 -0
- bridgeflow-0.1.0/src/bridgeflow/__init__.py +3 -0
- bridgeflow-0.1.0/src/bridgeflow/binding.py +125 -0
- bridgeflow-0.1.0/src/bridgeflow/cli.py +304 -0
- bridgeflow-0.1.0/src/bridgeflow/config.py +262 -0
- bridgeflow-0.1.0/src/bridgeflow/desktop/__init__.py +1 -0
- bridgeflow-0.1.0/src/bridgeflow/desktop/cursor_probe.py +145 -0
- bridgeflow-0.1.0/src/bridgeflow/desktop/executor.py +155 -0
- bridgeflow-0.1.0/src/bridgeflow/desktop/patrol.py +82 -0
- bridgeflow-0.1.0/src/bridgeflow/desktop/runner.py +413 -0
- bridgeflow-0.1.0/src/bridgeflow/desktop/status_store.py +177 -0
- bridgeflow-0.1.0/src/bridgeflow/desktop/window_control.py +78 -0
- bridgeflow-0.1.0/src/bridgeflow/file_protocol.py +167 -0
- bridgeflow-0.1.0/src/bridgeflow/file_watcher.py +47 -0
- bridgeflow-0.1.0/src/bridgeflow/human_admin/__init__.py +1 -0
- bridgeflow-0.1.0/src/bridgeflow/human_admin/bridge.py +8 -0
- bridgeflow-0.1.0/src/bridgeflow/models/__init__.py +1 -0
- bridgeflow-0.1.0/src/bridgeflow/models/events.py +19 -0
- bridgeflow-0.1.0/src/bridgeflow/relay_client/__init__.py +1 -0
- bridgeflow-0.1.0/src/bridgeflow/relay_client/ws_client.py +30 -0
- bridgeflow-0.1.0/src/bridgeflow/task_writer.py +108 -0
- bridgeflow-0.1.0/src/bridgeflow.egg-info/PKG-INFO +174 -0
- bridgeflow-0.1.0/src/bridgeflow.egg-info/SOURCES.txt +28 -0
- bridgeflow-0.1.0/src/bridgeflow.egg-info/dependency_links.txt +1 -0
- bridgeflow-0.1.0/src/bridgeflow.egg-info/entry_points.txt +2 -0
- bridgeflow-0.1.0/src/bridgeflow.egg-info/requires.txt +1 -0
- bridgeflow-0.1.0/src/bridgeflow.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bridgeflow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: BridgeFlow 人机协作桥接与团队巡检工具
|
|
5
|
+
Author: joinwell52-AI
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: websockets<16,>=12
|
|
10
|
+
|
|
11
|
+
# BridgeFlow
|
|
12
|
+
|
|
13
|
+
`BridgeFlow` 是一个面向多 Agent 团队协作场景的人机桥接与轻量巡检工具。
|
|
14
|
+
|
|
15
|
+
第一阶段目标不是“远程操控电脑”,而是把真人成员 `ADMIN01` 正式纳入团队协议:
|
|
16
|
+
|
|
17
|
+
- 手机端通过 PWA 发送文本
|
|
18
|
+
- 桌面端收到文本后落成标准任务文件
|
|
19
|
+
- `PM01` 按既有 `TASK-*.md` 协议接单
|
|
20
|
+
- `PM01` 或团队给 `ADMIN01` 的回复,也通过标准文本文件回流
|
|
21
|
+
|
|
22
|
+
这样,手机端看到的不是一套独立聊天记录,而是按时间展开的一条条项目文本文件。
|
|
23
|
+
|
|
24
|
+
## 第一阶段能力
|
|
25
|
+
|
|
26
|
+
- Python 包与 CLI 入口
|
|
27
|
+
- `ADMIN01 -> PM01` 任务文件生成
|
|
28
|
+
- `PM01/DEV01/OPS01/QA01 -> ADMIN01` 回复文件识别与摘要推送
|
|
29
|
+
- WebSocket 轻量中继
|
|
30
|
+
- GitHub Pages 可托管的手机 PWA 静态页
|
|
31
|
+
- `bridgeflow_config.json` 配置化
|
|
32
|
+
|
|
33
|
+
## 架构
|
|
34
|
+
|
|
35
|
+
```mermaid
|
|
36
|
+
flowchart LR
|
|
37
|
+
adminPwa[AdminPWA]
|
|
38
|
+
relayServer[RelayServer]
|
|
39
|
+
desktopBridge[DesktopBridge]
|
|
40
|
+
taskFiles[TaskFiles]
|
|
41
|
+
replyFiles[ReplyFiles]
|
|
42
|
+
pmAgent[PMAgent]
|
|
43
|
+
|
|
44
|
+
adminPwa -->|"文本消息"| relayServer
|
|
45
|
+
relayServer -->|"JSON事件"| desktopBridge
|
|
46
|
+
desktopBridge -->|"写 TASK-*-ADMIN01-to-PM01.md"| taskFiles
|
|
47
|
+
pmAgent -->|"处理任务"| taskFiles
|
|
48
|
+
pmAgent -->|"写 TASK-*-PM01-to-ADMIN01.md"| replyFiles
|
|
49
|
+
desktopBridge -->|"扫描回复并推送摘要"| relayServer
|
|
50
|
+
relayServer -->|"状态/回执/链接"| adminPwa
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 目录结构
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
BridgeFlow/
|
|
57
|
+
├── .cursor/
|
|
58
|
+
│ └── rules/
|
|
59
|
+
│ └── admin-human-bridge.mdc
|
|
60
|
+
├── pyproject.toml
|
|
61
|
+
├── README.md
|
|
62
|
+
├── docs/
|
|
63
|
+
│ ├── 产品设计说明.md
|
|
64
|
+
│ └── agents/
|
|
65
|
+
│ ├── README.md
|
|
66
|
+
│ └── ADMIN-01.md
|
|
67
|
+
├── examples/
|
|
68
|
+
│ └── bridgeflow_config.json
|
|
69
|
+
├── server/
|
|
70
|
+
│ └── relay/
|
|
71
|
+
│ └── server.py
|
|
72
|
+
├── src/
|
|
73
|
+
│ └── bridgeflow/
|
|
74
|
+
│ ├── cli.py
|
|
75
|
+
│ ├── config.py
|
|
76
|
+
│ ├── file_protocol.py
|
|
77
|
+
│ ├── task_writer.py
|
|
78
|
+
│ ├── relay_client/
|
|
79
|
+
│ │ └── ws_client.py
|
|
80
|
+
│ ├── desktop/
|
|
81
|
+
│ │ └── runner.py
|
|
82
|
+
│ └── models/
|
|
83
|
+
│ └── events.py
|
|
84
|
+
└── web/
|
|
85
|
+
└── pwa/
|
|
86
|
+
└── index.html
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Agent 文件结构
|
|
90
|
+
|
|
91
|
+
`BridgeFlow` 第一阶段已经内置一套最小 `agent_bridge` 协议骨架:
|
|
92
|
+
|
|
93
|
+
- [docs/agents/README.md](docs/agents/README.md)
|
|
94
|
+
- [docs/agents/ADMIN-01.md](docs/agents/ADMIN-01.md)
|
|
95
|
+
- [docs/联调启动说明.md](docs/联调启动说明.md)
|
|
96
|
+
- [.cursor/rules/admin-human-bridge.mdc](.cursor/rules/admin-human-bridge.mdc)
|
|
97
|
+
|
|
98
|
+
作用分别是:
|
|
99
|
+
|
|
100
|
+
- `README.md`:说明 `docs/agents/` 的目录结构和文件协议
|
|
101
|
+
- `ADMIN-01.md`:定义真人角色 `ADMIN01` 的职责和边界
|
|
102
|
+
- `admin-human-bridge.mdc`:给桥接逻辑或后续会话一个明确规则,要求“每条手机文本都必须落成任务文件”
|
|
103
|
+
|
|
104
|
+
## 核心原则
|
|
105
|
+
|
|
106
|
+
- 手机端只处理文本文件,不碰 Cursor 窗口
|
|
107
|
+
- PC 端负责团队内部巡检与文件桥接
|
|
108
|
+
- 中继只传文本与链接,不传大文件
|
|
109
|
+
- 发送与回复都必须文件化,避免形成第二套协议
|
|
110
|
+
- 角色与显示名分离,后续支持 `PM/CTO` 等别名
|
|
111
|
+
|
|
112
|
+
## 中继边界
|
|
113
|
+
|
|
114
|
+
- 默认接口端口:`5252`
|
|
115
|
+
- 仅转发文本 JSON 事件
|
|
116
|
+
- 不落盘、不执行、不上传文件
|
|
117
|
+
- 单条消息限制 `8KB`
|
|
118
|
+
- 默认频率限制:`10 秒内最多 20 条`
|
|
119
|
+
- 公网入口示例:`wss://relay.example.com/bridgeflow/ws/`
|
|
120
|
+
|
|
121
|
+
公开发布建议:
|
|
122
|
+
|
|
123
|
+
- 不要把真实生产 `wss` 地址写死到默认配置
|
|
124
|
+
- `room_key` 建议改成随机值,不要继续使用演示房间
|
|
125
|
+
- 生产环境建议通过配置文件或环境变量覆盖示例值
|
|
126
|
+
|
|
127
|
+
## 本地开发
|
|
128
|
+
|
|
129
|
+
```powershell
|
|
130
|
+
cd BridgeFlow
|
|
131
|
+
py -3.10 -m venv .venv
|
|
132
|
+
.\.venv\Scripts\Activate.ps1
|
|
133
|
+
python -m pip install -U pip
|
|
134
|
+
python -m pip install -e .
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## 主要命令
|
|
138
|
+
|
|
139
|
+
```powershell
|
|
140
|
+
bridgeflow init
|
|
141
|
+
bridgeflow write-admin-task --text "请 PM 帮我安排下一步任务"
|
|
142
|
+
bridgeflow write-reply --sender PM01 --text "已接单,开始拆解任务" --thread-key "demo-thread-001"
|
|
143
|
+
bridgeflow relay-connect
|
|
144
|
+
bridgeflow run
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
示例配置建议:
|
|
148
|
+
|
|
149
|
+
- 本地联调可使用 `ws://127.0.0.1:5252`
|
|
150
|
+
- 公开示例中的 `room_key` 请替换成你自己的随机房间名
|
|
151
|
+
- PWA 默认配置建议使用示例中继地址,发布时再按部署环境覆盖
|
|
152
|
+
|
|
153
|
+
## 第一阶段联调顺序
|
|
154
|
+
|
|
155
|
+
1. 启动 `server/relay/server.py`
|
|
156
|
+
2. 运行 `bridgeflow init`
|
|
157
|
+
3. 运行 `bridgeflow run`
|
|
158
|
+
4. 打开 `web/pwa/index.html` 本地预览或发布到 GitHub Pages
|
|
159
|
+
5. 在手机端输入文本,验证桌面端是否生成 `TASK-*-ADMIN01-to-PM01.md`
|
|
160
|
+
6. 运行 `bridgeflow write-reply --sender PM01 --text "已接单"`,验证手机端是否收到回复摘要
|
|
161
|
+
|
|
162
|
+
## 后续扩展
|
|
163
|
+
|
|
164
|
+
- 对接现有 `ops/auto_patrol.py` 的窗口巡检逻辑
|
|
165
|
+
- 把当前基础 `thread_key` 能力升级成完整会话线程 UI
|
|
166
|
+
- 增加 OSS 链接分享与图片附件
|
|
167
|
+
- 加入 `room_key` 和更严格的中继鉴权
|
|
168
|
+
|
|
169
|
+
## 命名约定
|
|
170
|
+
|
|
171
|
+
- 应用名:`BridgeFlow`
|
|
172
|
+
- 文件协作协议:`agent_bridge`
|
|
173
|
+
- Python 包名:`bridgeflow`
|
|
174
|
+
- CLI 命令:`bridgeflow`
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# BridgeFlow
|
|
2
|
+
|
|
3
|
+
`BridgeFlow` 是一个面向多 Agent 团队协作场景的人机桥接与轻量巡检工具。
|
|
4
|
+
|
|
5
|
+
第一阶段目标不是“远程操控电脑”,而是把真人成员 `ADMIN01` 正式纳入团队协议:
|
|
6
|
+
|
|
7
|
+
- 手机端通过 PWA 发送文本
|
|
8
|
+
- 桌面端收到文本后落成标准任务文件
|
|
9
|
+
- `PM01` 按既有 `TASK-*.md` 协议接单
|
|
10
|
+
- `PM01` 或团队给 `ADMIN01` 的回复,也通过标准文本文件回流
|
|
11
|
+
|
|
12
|
+
这样,手机端看到的不是一套独立聊天记录,而是按时间展开的一条条项目文本文件。
|
|
13
|
+
|
|
14
|
+
## 第一阶段能力
|
|
15
|
+
|
|
16
|
+
- Python 包与 CLI 入口
|
|
17
|
+
- `ADMIN01 -> PM01` 任务文件生成
|
|
18
|
+
- `PM01/DEV01/OPS01/QA01 -> ADMIN01` 回复文件识别与摘要推送
|
|
19
|
+
- WebSocket 轻量中继
|
|
20
|
+
- GitHub Pages 可托管的手机 PWA 静态页
|
|
21
|
+
- `bridgeflow_config.json` 配置化
|
|
22
|
+
|
|
23
|
+
## 架构
|
|
24
|
+
|
|
25
|
+
```mermaid
|
|
26
|
+
flowchart LR
|
|
27
|
+
adminPwa[AdminPWA]
|
|
28
|
+
relayServer[RelayServer]
|
|
29
|
+
desktopBridge[DesktopBridge]
|
|
30
|
+
taskFiles[TaskFiles]
|
|
31
|
+
replyFiles[ReplyFiles]
|
|
32
|
+
pmAgent[PMAgent]
|
|
33
|
+
|
|
34
|
+
adminPwa -->|"文本消息"| relayServer
|
|
35
|
+
relayServer -->|"JSON事件"| desktopBridge
|
|
36
|
+
desktopBridge -->|"写 TASK-*-ADMIN01-to-PM01.md"| taskFiles
|
|
37
|
+
pmAgent -->|"处理任务"| taskFiles
|
|
38
|
+
pmAgent -->|"写 TASK-*-PM01-to-ADMIN01.md"| replyFiles
|
|
39
|
+
desktopBridge -->|"扫描回复并推送摘要"| relayServer
|
|
40
|
+
relayServer -->|"状态/回执/链接"| adminPwa
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 目录结构
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
BridgeFlow/
|
|
47
|
+
├── .cursor/
|
|
48
|
+
│ └── rules/
|
|
49
|
+
│ └── admin-human-bridge.mdc
|
|
50
|
+
├── pyproject.toml
|
|
51
|
+
├── README.md
|
|
52
|
+
├── docs/
|
|
53
|
+
│ ├── 产品设计说明.md
|
|
54
|
+
│ └── agents/
|
|
55
|
+
│ ├── README.md
|
|
56
|
+
│ └── ADMIN-01.md
|
|
57
|
+
├── examples/
|
|
58
|
+
│ └── bridgeflow_config.json
|
|
59
|
+
├── server/
|
|
60
|
+
│ └── relay/
|
|
61
|
+
│ └── server.py
|
|
62
|
+
├── src/
|
|
63
|
+
│ └── bridgeflow/
|
|
64
|
+
│ ├── cli.py
|
|
65
|
+
│ ├── config.py
|
|
66
|
+
│ ├── file_protocol.py
|
|
67
|
+
│ ├── task_writer.py
|
|
68
|
+
│ ├── relay_client/
|
|
69
|
+
│ │ └── ws_client.py
|
|
70
|
+
│ ├── desktop/
|
|
71
|
+
│ │ └── runner.py
|
|
72
|
+
│ └── models/
|
|
73
|
+
│ └── events.py
|
|
74
|
+
└── web/
|
|
75
|
+
└── pwa/
|
|
76
|
+
└── index.html
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Agent 文件结构
|
|
80
|
+
|
|
81
|
+
`BridgeFlow` 第一阶段已经内置一套最小 `agent_bridge` 协议骨架:
|
|
82
|
+
|
|
83
|
+
- [docs/agents/README.md](docs/agents/README.md)
|
|
84
|
+
- [docs/agents/ADMIN-01.md](docs/agents/ADMIN-01.md)
|
|
85
|
+
- [docs/联调启动说明.md](docs/联调启动说明.md)
|
|
86
|
+
- [.cursor/rules/admin-human-bridge.mdc](.cursor/rules/admin-human-bridge.mdc)
|
|
87
|
+
|
|
88
|
+
作用分别是:
|
|
89
|
+
|
|
90
|
+
- `README.md`:说明 `docs/agents/` 的目录结构和文件协议
|
|
91
|
+
- `ADMIN-01.md`:定义真人角色 `ADMIN01` 的职责和边界
|
|
92
|
+
- `admin-human-bridge.mdc`:给桥接逻辑或后续会话一个明确规则,要求“每条手机文本都必须落成任务文件”
|
|
93
|
+
|
|
94
|
+
## 核心原则
|
|
95
|
+
|
|
96
|
+
- 手机端只处理文本文件,不碰 Cursor 窗口
|
|
97
|
+
- PC 端负责团队内部巡检与文件桥接
|
|
98
|
+
- 中继只传文本与链接,不传大文件
|
|
99
|
+
- 发送与回复都必须文件化,避免形成第二套协议
|
|
100
|
+
- 角色与显示名分离,后续支持 `PM/CTO` 等别名
|
|
101
|
+
|
|
102
|
+
## 中继边界
|
|
103
|
+
|
|
104
|
+
- 默认接口端口:`5252`
|
|
105
|
+
- 仅转发文本 JSON 事件
|
|
106
|
+
- 不落盘、不执行、不上传文件
|
|
107
|
+
- 单条消息限制 `8KB`
|
|
108
|
+
- 默认频率限制:`10 秒内最多 20 条`
|
|
109
|
+
- 公网入口示例:`wss://relay.example.com/bridgeflow/ws/`
|
|
110
|
+
|
|
111
|
+
公开发布建议:
|
|
112
|
+
|
|
113
|
+
- 不要把真实生产 `wss` 地址写死到默认配置
|
|
114
|
+
- `room_key` 建议改成随机值,不要继续使用演示房间
|
|
115
|
+
- 生产环境建议通过配置文件或环境变量覆盖示例值
|
|
116
|
+
|
|
117
|
+
## 本地开发
|
|
118
|
+
|
|
119
|
+
```powershell
|
|
120
|
+
cd BridgeFlow
|
|
121
|
+
py -3.10 -m venv .venv
|
|
122
|
+
.\.venv\Scripts\Activate.ps1
|
|
123
|
+
python -m pip install -U pip
|
|
124
|
+
python -m pip install -e .
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 主要命令
|
|
128
|
+
|
|
129
|
+
```powershell
|
|
130
|
+
bridgeflow init
|
|
131
|
+
bridgeflow write-admin-task --text "请 PM 帮我安排下一步任务"
|
|
132
|
+
bridgeflow write-reply --sender PM01 --text "已接单,开始拆解任务" --thread-key "demo-thread-001"
|
|
133
|
+
bridgeflow relay-connect
|
|
134
|
+
bridgeflow run
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
示例配置建议:
|
|
138
|
+
|
|
139
|
+
- 本地联调可使用 `ws://127.0.0.1:5252`
|
|
140
|
+
- 公开示例中的 `room_key` 请替换成你自己的随机房间名
|
|
141
|
+
- PWA 默认配置建议使用示例中继地址,发布时再按部署环境覆盖
|
|
142
|
+
|
|
143
|
+
## 第一阶段联调顺序
|
|
144
|
+
|
|
145
|
+
1. 启动 `server/relay/server.py`
|
|
146
|
+
2. 运行 `bridgeflow init`
|
|
147
|
+
3. 运行 `bridgeflow run`
|
|
148
|
+
4. 打开 `web/pwa/index.html` 本地预览或发布到 GitHub Pages
|
|
149
|
+
5. 在手机端输入文本,验证桌面端是否生成 `TASK-*-ADMIN01-to-PM01.md`
|
|
150
|
+
6. 运行 `bridgeflow write-reply --sender PM01 --text "已接单"`,验证手机端是否收到回复摘要
|
|
151
|
+
|
|
152
|
+
## 后续扩展
|
|
153
|
+
|
|
154
|
+
- 对接现有 `ops/auto_patrol.py` 的窗口巡检逻辑
|
|
155
|
+
- 把当前基础 `thread_key` 能力升级成完整会话线程 UI
|
|
156
|
+
- 增加 OSS 链接分享与图片附件
|
|
157
|
+
- 加入 `room_key` 和更严格的中继鉴权
|
|
158
|
+
|
|
159
|
+
## 命名约定
|
|
160
|
+
|
|
161
|
+
- 应用名:`BridgeFlow`
|
|
162
|
+
- 文件协作协议:`agent_bridge`
|
|
163
|
+
- Python 包名:`bridgeflow`
|
|
164
|
+
- CLI 命令:`bridgeflow`
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bridgeflow"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "BridgeFlow 人机协作桥接与团队巡检工具"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "joinwell52-AI" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"websockets>=12,<16"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
bridgeflow = "bridgeflow.cli:main"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
package-dir = { "" = "src" }
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
import secrets
|
|
5
|
+
import string
|
|
6
|
+
|
|
7
|
+
from bridgeflow.config import PatrolConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
BIND_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _now() -> datetime:
|
|
14
|
+
return datetime.now()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _fmt(dt: datetime) -> str:
|
|
18
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse(value: str) -> datetime | None:
|
|
22
|
+
if not value:
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
|
|
26
|
+
except ValueError:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _new_code(length: int = 6) -> str:
|
|
31
|
+
return "".join(secrets.choice(BIND_ALPHABET) for _ in range(length))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def has_active_bind_code(config: PatrolConfig) -> bool:
|
|
35
|
+
if not config.pending_bind_code or not config.pending_bind_expires_at:
|
|
36
|
+
return False
|
|
37
|
+
expires_at = _parse(config.pending_bind_expires_at)
|
|
38
|
+
return expires_at is not None and expires_at >= _now()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def public_bind_state(config: PatrolConfig) -> dict:
|
|
42
|
+
return {
|
|
43
|
+
"status": config.bind_status,
|
|
44
|
+
"machine_code": config.machine_code,
|
|
45
|
+
"bound_mobile_device_id": config.bound_mobile_device_id,
|
|
46
|
+
"bound_mobile_device_name": config.bound_mobile_device_name,
|
|
47
|
+
"bound_at": config.bound_at,
|
|
48
|
+
"has_pending_code": has_active_bind_code(config),
|
|
49
|
+
"pending_bind_expires_at": config.pending_bind_expires_at if has_active_bind_code(config) else "",
|
|
50
|
+
"pending_mobile_device_id": config.pending_mobile_device_id,
|
|
51
|
+
"pending_mobile_device_name": config.pending_mobile_device_name,
|
|
52
|
+
"bind_code_ttl_seconds": config.bind_code_ttl_seconds,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def private_bind_state(config: PatrolConfig) -> dict:
|
|
57
|
+
data = public_bind_state(config)
|
|
58
|
+
data["pending_bind_code"] = config.pending_bind_code if has_active_bind_code(config) else ""
|
|
59
|
+
data["last_bind_code_issued_at"] = config.last_bind_code_issued_at
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def issue_bind_code(
|
|
64
|
+
config: PatrolConfig,
|
|
65
|
+
*,
|
|
66
|
+
mobile_device_id: str = "",
|
|
67
|
+
mobile_device_name: str = "",
|
|
68
|
+
) -> dict:
|
|
69
|
+
now = _now()
|
|
70
|
+
code = _new_code()
|
|
71
|
+
expires_at = now + timedelta(seconds=config.bind_code_ttl_seconds)
|
|
72
|
+
config.update_bind(
|
|
73
|
+
status="pending",
|
|
74
|
+
pending_bind_code=code,
|
|
75
|
+
pending_bind_expires_at=_fmt(expires_at),
|
|
76
|
+
pending_mobile_device_id=mobile_device_id.strip(),
|
|
77
|
+
pending_mobile_device_name=mobile_device_name.strip(),
|
|
78
|
+
last_bind_code_issued_at=_fmt(now),
|
|
79
|
+
)
|
|
80
|
+
return private_bind_state(config)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def approve_bind(
|
|
84
|
+
config: PatrolConfig,
|
|
85
|
+
*,
|
|
86
|
+
bind_code: str,
|
|
87
|
+
mobile_device_id: str,
|
|
88
|
+
mobile_device_name: str,
|
|
89
|
+
) -> dict:
|
|
90
|
+
bind_code = bind_code.strip().upper()
|
|
91
|
+
if not bind_code:
|
|
92
|
+
raise ValueError("绑定码不能为空")
|
|
93
|
+
if not mobile_device_id.strip():
|
|
94
|
+
raise ValueError("mobile_device_id 不能为空")
|
|
95
|
+
if not has_active_bind_code(config):
|
|
96
|
+
raise ValueError("当前没有可用的绑定码")
|
|
97
|
+
if bind_code != config.pending_bind_code.strip().upper():
|
|
98
|
+
raise ValueError("绑定码不正确")
|
|
99
|
+
|
|
100
|
+
now = _now()
|
|
101
|
+
config.update_bind(
|
|
102
|
+
status="bound",
|
|
103
|
+
bound_mobile_device_id=mobile_device_id.strip(),
|
|
104
|
+
bound_mobile_device_name=mobile_device_name.strip(),
|
|
105
|
+
bound_at=_fmt(now),
|
|
106
|
+
pending_bind_code="",
|
|
107
|
+
pending_bind_expires_at="",
|
|
108
|
+
pending_mobile_device_id="",
|
|
109
|
+
pending_mobile_device_name="",
|
|
110
|
+
)
|
|
111
|
+
return public_bind_state(config)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def clear_binding(config: PatrolConfig) -> dict:
|
|
115
|
+
config.update_bind(
|
|
116
|
+
status="unbound",
|
|
117
|
+
bound_mobile_device_id="",
|
|
118
|
+
bound_mobile_device_name="",
|
|
119
|
+
bound_at="",
|
|
120
|
+
pending_bind_code="",
|
|
121
|
+
pending_bind_expires_at="",
|
|
122
|
+
pending_mobile_device_id="",
|
|
123
|
+
pending_mobile_device_name="",
|
|
124
|
+
)
|
|
125
|
+
return public_bind_state(config)
|