bridgeflow 0.1.0__py3-none-any.whl
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/__init__.py +3 -0
- bridgeflow/binding.py +125 -0
- bridgeflow/cli.py +304 -0
- bridgeflow/config.py +262 -0
- bridgeflow/desktop/__init__.py +1 -0
- bridgeflow/desktop/cursor_probe.py +145 -0
- bridgeflow/desktop/executor.py +155 -0
- bridgeflow/desktop/patrol.py +82 -0
- bridgeflow/desktop/runner.py +413 -0
- bridgeflow/desktop/status_store.py +177 -0
- bridgeflow/desktop/window_control.py +78 -0
- bridgeflow/file_protocol.py +167 -0
- bridgeflow/file_watcher.py +47 -0
- bridgeflow/human_admin/__init__.py +1 -0
- bridgeflow/human_admin/bridge.py +8 -0
- bridgeflow/models/__init__.py +1 -0
- bridgeflow/models/events.py +19 -0
- bridgeflow/relay_client/__init__.py +1 -0
- bridgeflow/relay_client/ws_client.py +30 -0
- bridgeflow/task_writer.py +108 -0
- bridgeflow-0.1.0.dist-info/METADATA +174 -0
- bridgeflow-0.1.0.dist-info/RECORD +25 -0
- bridgeflow-0.1.0.dist-info/WHEEL +5 -0
- bridgeflow-0.1.0.dist-info/entry_points.txt +2 -0
- bridgeflow-0.1.0.dist-info/top_level.txt +1 -0
bridgeflow/__init__.py
ADDED
bridgeflow/binding.py
ADDED
|
@@ -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)
|
bridgeflow/cli.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import socket
|
|
9
|
+
import sys
|
|
10
|
+
import uuid
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from bridgeflow.binding import approve_bind, clear_binding, issue_bind_code, private_bind_state, public_bind_state
|
|
14
|
+
from bridgeflow.config import DEFAULT_CONFIG_NAME, DEFAULT_FIXED_ROLES, load_config
|
|
15
|
+
from bridgeflow.desktop.executor import execute_desktop_action
|
|
16
|
+
from bridgeflow.desktop.patrol import patrol_once, print_doctor_result, run_doctor
|
|
17
|
+
from bridgeflow.desktop.runner import start_desktop_bridge
|
|
18
|
+
from bridgeflow.task_writer import write_admin_task, write_role_reply
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _default_project_root() -> Path:
|
|
22
|
+
return Path.cwd()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _slug(value: str) -> str:
|
|
26
|
+
clean = re.sub(r"[^A-Za-z0-9_-]+", "-", value.strip())
|
|
27
|
+
clean = clean.strip("-").lower()
|
|
28
|
+
return clean or "bridgeflow-pc"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_machine_code(hostname: str) -> str:
|
|
32
|
+
seed = f"{hostname}-{uuid.getnode()}".encode("utf-8", errors="ignore")
|
|
33
|
+
digest = hashlib.sha1(seed).hexdigest()[:12].upper()
|
|
34
|
+
return f"BF-{digest}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _print_bind_state(state: dict) -> None:
|
|
38
|
+
print(f"绑定状态:{state.get('status', '')}")
|
|
39
|
+
print(f"机器码:{state.get('machine_code', '')}")
|
|
40
|
+
if state.get("pending_bind_code"):
|
|
41
|
+
print(f"一次性绑定码:{state['pending_bind_code']}")
|
|
42
|
+
if state.get("pending_bind_expires_at"):
|
|
43
|
+
print(f"绑定码过期时间:{state['pending_bind_expires_at']}")
|
|
44
|
+
if state.get("pending_mobile_device_id") or state.get("pending_mobile_device_name"):
|
|
45
|
+
print(f"待确认手机:{state.get('pending_mobile_device_name', '')} {state.get('pending_mobile_device_id', '')}".strip())
|
|
46
|
+
if state.get("bound_mobile_device_id") or state.get("bound_mobile_device_name"):
|
|
47
|
+
print(f"已绑定手机:{state.get('bound_mobile_device_name', '')} {state.get('bound_mobile_device_id', '')}".strip())
|
|
48
|
+
if state.get("bound_at"):
|
|
49
|
+
print(f"绑定时间:{state['bound_at']}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _print_action_result(result: dict) -> None:
|
|
53
|
+
print(f"动作:{result.get('action', '')}")
|
|
54
|
+
print(f"结果:{'成功' if result.get('ok') else '失败'}")
|
|
55
|
+
print(f"说明:{result.get('message', '')}")
|
|
56
|
+
print(f"窗口数:{result.get('window_count', 0)}")
|
|
57
|
+
print(f"目标窗口:{result.get('target_title', '')}")
|
|
58
|
+
if result.get("typed_text"):
|
|
59
|
+
print(f"发送文本:{result['typed_text']}")
|
|
60
|
+
print(f"dry-run:{result.get('dry_run', False)}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
64
|
+
project_root = Path(args.project_root or _default_project_root()).resolve()
|
|
65
|
+
project_root.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
target = project_root / DEFAULT_CONFIG_NAME
|
|
67
|
+
example = Path(__file__).resolve().parents[2] / "examples" / DEFAULT_CONFIG_NAME
|
|
68
|
+
if target.exists() and not args.force:
|
|
69
|
+
print(f"配置已存在:{target}")
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
hostname = socket.gethostname() or "BridgeFlow-PC"
|
|
73
|
+
raw = json.loads(example.read_text(encoding="utf-8-sig"))
|
|
74
|
+
raw["project"]["root_dir"] = str(project_root)
|
|
75
|
+
raw.setdefault("device", {})
|
|
76
|
+
raw["device"]["device_id"] = f"{_slug(hostname)}-pc"
|
|
77
|
+
raw["device"]["device_name"] = f"{hostname} AI执行机"
|
|
78
|
+
raw["device"].setdefault("owner_role", "PM01")
|
|
79
|
+
raw["device"]["machine_code"] = _build_machine_code(hostname)
|
|
80
|
+
|
|
81
|
+
raw.setdefault("ai_team", {})
|
|
82
|
+
raw["ai_team"]["fixed_roles"] = DEFAULT_FIXED_ROLES[:]
|
|
83
|
+
raw["ai_team"]["cursor_only"] = True
|
|
84
|
+
|
|
85
|
+
raw.setdefault("bind", {})
|
|
86
|
+
raw["bind"].setdefault("status", "unbound")
|
|
87
|
+
raw["bind"].setdefault("bound_mobile_device_id", "")
|
|
88
|
+
raw["bind"].setdefault("bound_mobile_device_name", "")
|
|
89
|
+
raw["bind"].setdefault("bound_at", "")
|
|
90
|
+
raw["bind"].setdefault("bind_code_ttl_seconds", 600)
|
|
91
|
+
raw["bind"].setdefault("pending_bind_code", "")
|
|
92
|
+
raw["bind"].setdefault("pending_bind_expires_at", "")
|
|
93
|
+
raw["bind"].setdefault("pending_mobile_device_id", "")
|
|
94
|
+
raw["bind"].setdefault("pending_mobile_device_name", "")
|
|
95
|
+
raw["bind"].setdefault("last_bind_code_issued_at", "")
|
|
96
|
+
|
|
97
|
+
raw.setdefault("runtime", {})
|
|
98
|
+
raw["runtime"].setdefault("runtime_dir", ".bridgeflow/runtime")
|
|
99
|
+
raw["runtime"].setdefault("status_dir", ".bridgeflow/runtime/status")
|
|
100
|
+
raw["runtime"].setdefault("task_details_dir", ".bridgeflow/runtime/task_details")
|
|
101
|
+
raw["runtime"].setdefault("idle_seconds", 180)
|
|
102
|
+
raw["runtime"].setdefault("stale_task_seconds", 900)
|
|
103
|
+
|
|
104
|
+
target.write_text(json.dumps(raw, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
105
|
+
config = load_config(target)
|
|
106
|
+
config.ensure_runtime_dirs()
|
|
107
|
+
print(f"已生成配置:{target}")
|
|
108
|
+
print(f"设备ID:{config.device_id}")
|
|
109
|
+
print(f"机器码:{config.machine_code}")
|
|
110
|
+
print(f"运行态目录:{config.runtime_dir}")
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cmd_write_admin_task(args: argparse.Namespace) -> int:
|
|
115
|
+
config = load_config(args.config)
|
|
116
|
+
written = write_admin_task(
|
|
117
|
+
config,
|
|
118
|
+
args.text,
|
|
119
|
+
recipient=args.recipient,
|
|
120
|
+
priority=args.priority,
|
|
121
|
+
attachments=args.attachments or [],
|
|
122
|
+
thread_key=args.thread_key,
|
|
123
|
+
)
|
|
124
|
+
print(f"已写入:{written.path}")
|
|
125
|
+
print(f"线程:{written.thread_key}")
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def cmd_write_reply(args: argparse.Namespace) -> int:
|
|
130
|
+
config = load_config(args.config)
|
|
131
|
+
written = write_role_reply(
|
|
132
|
+
config,
|
|
133
|
+
args.text,
|
|
134
|
+
sender=args.sender,
|
|
135
|
+
recipient=args.recipient,
|
|
136
|
+
priority=args.priority,
|
|
137
|
+
attachments=args.attachments or [],
|
|
138
|
+
thread_key=args.thread_key,
|
|
139
|
+
)
|
|
140
|
+
print(f"已写入:{written.path}")
|
|
141
|
+
print(f"线程:{written.thread_key}")
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def cmd_bind_status(args: argparse.Namespace) -> int:
|
|
146
|
+
config = load_config(args.config)
|
|
147
|
+
_print_bind_state(private_bind_state(config))
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_bind_code(args: argparse.Namespace) -> int:
|
|
152
|
+
config = load_config(args.config)
|
|
153
|
+
state = issue_bind_code(
|
|
154
|
+
config,
|
|
155
|
+
mobile_device_id=args.mobile_device_id,
|
|
156
|
+
mobile_device_name=args.mobile_device_name,
|
|
157
|
+
)
|
|
158
|
+
_print_bind_state(state)
|
|
159
|
+
return 0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cmd_approve_bind(args: argparse.Namespace) -> int:
|
|
163
|
+
config = load_config(args.config)
|
|
164
|
+
state = approve_bind(
|
|
165
|
+
config,
|
|
166
|
+
bind_code=args.code,
|
|
167
|
+
mobile_device_id=args.mobile_device_id,
|
|
168
|
+
mobile_device_name=args.mobile_device_name,
|
|
169
|
+
)
|
|
170
|
+
_print_bind_state(state)
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def cmd_unbind(args: argparse.Namespace) -> int:
|
|
175
|
+
config = load_config(args.config)
|
|
176
|
+
state = clear_binding(config)
|
|
177
|
+
_print_bind_state(state)
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def cmd_desktop_action(args: argparse.Namespace) -> int:
|
|
182
|
+
config = load_config(args.config)
|
|
183
|
+
result = execute_desktop_action(config, args.action, dry_run=args.dry_run).to_dict()
|
|
184
|
+
_print_action_result(result)
|
|
185
|
+
return 0 if result.get("ok") else 1
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def cmd_relay_connect(args: argparse.Namespace) -> int:
|
|
189
|
+
config = load_config(args.config)
|
|
190
|
+
asyncio.run(start_desktop_bridge(config))
|
|
191
|
+
return 0
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
195
|
+
return cmd_relay_connect(args)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def cmd_doctor(args: argparse.Namespace) -> int:
|
|
199
|
+
config = load_config(args.config)
|
|
200
|
+
config.validate_roles()
|
|
201
|
+
config.ensure_runtime_dirs()
|
|
202
|
+
result = run_doctor(config)
|
|
203
|
+
print_doctor_result(result)
|
|
204
|
+
return 0 if result.tasks_dir_exists and result.reports_dir_exists else 1
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def cmd_patrol_once(args: argparse.Namespace) -> int:
|
|
208
|
+
config = load_config(args.config)
|
|
209
|
+
targets = sorted(patrol_once(config))
|
|
210
|
+
if not targets:
|
|
211
|
+
print("本轮未发现需要巡检的目标。")
|
|
212
|
+
return 0
|
|
213
|
+
print("本轮目标:")
|
|
214
|
+
for item in targets:
|
|
215
|
+
print(f"- {item}")
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
220
|
+
parser = argparse.ArgumentParser(prog="bridgeflow", description="BridgeFlow 人机协作桥接与团队巡检工具")
|
|
221
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
222
|
+
|
|
223
|
+
init_p = sub.add_parser("init", help="生成默认 bridgeflow_config.json")
|
|
224
|
+
init_p.add_argument("--project-root", default="")
|
|
225
|
+
init_p.add_argument("--force", action="store_true")
|
|
226
|
+
init_p.set_defaults(func=cmd_init)
|
|
227
|
+
|
|
228
|
+
bind_status_p = sub.add_parser("bind-status", help="查看当前绑定状态")
|
|
229
|
+
bind_status_p.add_argument("--config", default=DEFAULT_CONFIG_NAME)
|
|
230
|
+
bind_status_p.set_defaults(func=cmd_bind_status)
|
|
231
|
+
|
|
232
|
+
bind_code_p = sub.add_parser("bind-code", help="生成一次性绑定码")
|
|
233
|
+
bind_code_p.add_argument("--config", default=DEFAULT_CONFIG_NAME)
|
|
234
|
+
bind_code_p.add_argument("--mobile-device-id", default="")
|
|
235
|
+
bind_code_p.add_argument("--mobile-device-name", default="")
|
|
236
|
+
bind_code_p.set_defaults(func=cmd_bind_code)
|
|
237
|
+
|
|
238
|
+
approve_bind_p = sub.add_parser("approve-bind", help="确认手机绑定")
|
|
239
|
+
approve_bind_p.add_argument("--config", default=DEFAULT_CONFIG_NAME)
|
|
240
|
+
approve_bind_p.add_argument("--code", required=True)
|
|
241
|
+
approve_bind_p.add_argument("--mobile-device-id", required=True)
|
|
242
|
+
approve_bind_p.add_argument("--mobile-device-name", default="")
|
|
243
|
+
approve_bind_p.set_defaults(func=cmd_approve_bind)
|
|
244
|
+
|
|
245
|
+
unbind_p = sub.add_parser("unbind", help="解除当前绑定")
|
|
246
|
+
unbind_p.add_argument("--config", default=DEFAULT_CONFIG_NAME)
|
|
247
|
+
unbind_p.set_defaults(func=cmd_unbind)
|
|
248
|
+
|
|
249
|
+
action_p = sub.add_parser("desktop-action", help="执行 PC 端桌面动作")
|
|
250
|
+
action_p.add_argument("--config", default=DEFAULT_CONFIG_NAME)
|
|
251
|
+
action_p.add_argument("--action", choices=["focus_cursor", "inspect", "start_work"], required=True)
|
|
252
|
+
action_p.add_argument("--dry-run", action="store_true")
|
|
253
|
+
action_p.set_defaults(func=cmd_desktop_action)
|
|
254
|
+
|
|
255
|
+
write_p = sub.add_parser("write-admin-task", help="直接生成 ADMIN01 -> PM01 任务文件")
|
|
256
|
+
write_p.add_argument("--config", default=DEFAULT_CONFIG_NAME)
|
|
257
|
+
write_p.add_argument("--text", required=True)
|
|
258
|
+
write_p.add_argument("--recipient", default="")
|
|
259
|
+
write_p.add_argument("--priority", default="")
|
|
260
|
+
write_p.add_argument("--thread-key", default="")
|
|
261
|
+
write_p.add_argument("--attachments", nargs="*")
|
|
262
|
+
write_p.set_defaults(func=cmd_write_admin_task)
|
|
263
|
+
|
|
264
|
+
reply_p = sub.add_parser("write-reply", help="直接生成 PM/DEV/QA -> ADMIN01 回执文件")
|
|
265
|
+
reply_p.add_argument("--config", default=DEFAULT_CONFIG_NAME)
|
|
266
|
+
reply_p.add_argument("--sender", required=True, help="发送角色,如 PM01/DEV01/QA01")
|
|
267
|
+
reply_p.add_argument("--recipient", default="ADMIN01")
|
|
268
|
+
reply_p.add_argument("--text", required=True)
|
|
269
|
+
reply_p.add_argument("--priority", default="")
|
|
270
|
+
reply_p.add_argument("--thread-key", default="")
|
|
271
|
+
reply_p.add_argument("--attachments", nargs="*")
|
|
272
|
+
reply_p.set_defaults(func=cmd_write_reply)
|
|
273
|
+
|
|
274
|
+
relay_p = sub.add_parser("relay-connect", help="连接中继并启动桌面桥接")
|
|
275
|
+
relay_p.add_argument("--config", default=DEFAULT_CONFIG_NAME)
|
|
276
|
+
relay_p.set_defaults(func=cmd_relay_connect)
|
|
277
|
+
|
|
278
|
+
doctor_p = sub.add_parser("doctor", help="检查目录、模板和 pyautogui 环境")
|
|
279
|
+
doctor_p.add_argument("--config", default=DEFAULT_CONFIG_NAME)
|
|
280
|
+
doctor_p.set_defaults(func=cmd_doctor)
|
|
281
|
+
|
|
282
|
+
patrol_p = sub.add_parser("patrol-once", help="扫描当前任务/报告并输出本轮巡检目标")
|
|
283
|
+
patrol_p.add_argument("--config", default=DEFAULT_CONFIG_NAME)
|
|
284
|
+
patrol_p.set_defaults(func=cmd_patrol_once)
|
|
285
|
+
|
|
286
|
+
run_p = sub.add_parser("run", help="启动桌面桥接主循环")
|
|
287
|
+
run_p.add_argument("--config", default=DEFAULT_CONFIG_NAME)
|
|
288
|
+
run_p.set_defaults(func=cmd_run)
|
|
289
|
+
|
|
290
|
+
return parser
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def main() -> None:
|
|
294
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
295
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
296
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
297
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
298
|
+
parser = build_parser()
|
|
299
|
+
args = parser.parse_args()
|
|
300
|
+
raise SystemExit(args.func(args))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
if __name__ == "__main__":
|
|
304
|
+
main()
|
bridgeflow/config.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DEFAULT_CONFIG_NAME = "bridgeflow_config.json"
|
|
10
|
+
DEFAULT_FIXED_ROLES = ["ADMIN01", "PM01", "DEV01", "QA01"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class PatrolConfig:
|
|
15
|
+
raw: dict
|
|
16
|
+
path: Path
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def relay_url(self) -> str:
|
|
20
|
+
return str(self.raw.get("relay", {}).get("url", "")).strip()
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def room_key(self) -> str:
|
|
24
|
+
return str(self.raw.get("relay", {}).get("room_key", "")).strip()
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def shared_secret(self) -> str:
|
|
28
|
+
return str(self.raw.get("relay", {}).get("shared_secret", "")).strip()
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def device_id(self) -> str:
|
|
32
|
+
return str(self.raw.get("device", {}).get("device_id", "")).strip()
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def device_name(self) -> str:
|
|
36
|
+
return str(self.raw.get("device", {}).get("device_name", "")).strip()
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def owner_role(self) -> str:
|
|
40
|
+
return str(self.raw.get("device", {}).get("owner_role", "")).strip()
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def machine_code(self) -> str:
|
|
44
|
+
return str(self.raw.get("device", {}).get("machine_code", "")).strip()
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def device_label(self) -> str:
|
|
48
|
+
return self.device_name or self.device_id or "未命名设备"
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def bind_status(self) -> str:
|
|
52
|
+
return str(self.raw.get("bind", {}).get("status", "unbound")).strip() or "unbound"
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def bound_mobile_device_id(self) -> str:
|
|
56
|
+
return str(self.raw.get("bind", {}).get("bound_mobile_device_id", "")).strip()
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def bound_mobile_device_name(self) -> str:
|
|
60
|
+
return str(self.raw.get("bind", {}).get("bound_mobile_device_name", "")).strip()
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def bound_at(self) -> str:
|
|
64
|
+
return str(self.raw.get("bind", {}).get("bound_at", "")).strip()
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def bind_code_ttl_seconds(self) -> int:
|
|
68
|
+
return int(self.raw.get("bind", {}).get("bind_code_ttl_seconds", 600))
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def pending_bind_code(self) -> str:
|
|
72
|
+
return str(self.raw.get("bind", {}).get("pending_bind_code", "")).strip()
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def pending_bind_expires_at(self) -> str:
|
|
76
|
+
return str(self.raw.get("bind", {}).get("pending_bind_expires_at", "")).strip()
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def pending_mobile_device_id(self) -> str:
|
|
80
|
+
return str(self.raw.get("bind", {}).get("pending_mobile_device_id", "")).strip()
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def pending_mobile_device_name(self) -> str:
|
|
84
|
+
return str(self.raw.get("bind", {}).get("pending_mobile_device_name", "")).strip()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def last_bind_code_issued_at(self) -> str:
|
|
88
|
+
return str(self.raw.get("bind", {}).get("last_bind_code_issued_at", "")).strip()
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def project_root(self) -> Path:
|
|
92
|
+
root = self.raw.get("project", {}).get("root_dir", ".")
|
|
93
|
+
return Path(root).expanduser().resolve()
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def tasks_dir(self) -> Path:
|
|
97
|
+
rel = self.raw.get("project", {}).get("tasks_dir", "docs/agents/tasks")
|
|
98
|
+
return (self.project_root / rel).resolve()
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def reports_dir(self) -> Path:
|
|
102
|
+
rel = self.raw.get("project", {}).get("reports_dir", "docs/agents/reports")
|
|
103
|
+
return (self.project_root / rel).resolve()
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def issues_dir(self) -> Path:
|
|
107
|
+
rel = self.raw.get("project", {}).get("issues_dir", "docs/agents/issues")
|
|
108
|
+
return (self.project_root / rel).resolve()
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def templates_dir(self) -> Path:
|
|
112
|
+
rel = self.raw.get("patrol", {}).get("templates_dir", "ops/patrol_templates")
|
|
113
|
+
return (self.project_root / rel).resolve()
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def runtime_dir(self) -> Path:
|
|
117
|
+
rel = self.raw.get("runtime", {}).get("runtime_dir", ".bridgeflow/runtime")
|
|
118
|
+
return (self.project_root / rel).resolve()
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def status_dir(self) -> Path:
|
|
122
|
+
rel = self.raw.get("runtime", {}).get("status_dir", ".bridgeflow/runtime/status")
|
|
123
|
+
return (self.project_root / rel).resolve()
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def task_details_dir(self) -> Path:
|
|
127
|
+
rel = self.raw.get("runtime", {}).get("task_details_dir", ".bridgeflow/runtime/task_details")
|
|
128
|
+
return (self.project_root / rel).resolve()
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def heartbeat_file(self) -> Path:
|
|
132
|
+
return self.status_dir / "heartbeat.json"
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def device_status_file(self) -> Path:
|
|
136
|
+
return self.status_dir / "device_status.json"
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def task_index_file(self) -> Path:
|
|
140
|
+
return self.status_dir / "task_index.json"
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def last_activity_file(self) -> Path:
|
|
144
|
+
return self.status_dir / "last_activity.json"
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def admin_sender(self) -> str:
|
|
148
|
+
return str(self.raw.get("admin", {}).get("sender", "ADMIN01")).strip()
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def admin_target(self) -> str:
|
|
152
|
+
return str(self.raw.get("admin", {}).get("target", "PM01")).strip()
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def default_priority(self) -> str:
|
|
156
|
+
return str(self.raw.get("admin", {}).get("default_priority", "P1")).strip()
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def patrol_message(self) -> str:
|
|
160
|
+
return str(self.raw.get("patrol", {}).get("message", "巡检")).strip()
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def patrol_poll_interval(self) -> int:
|
|
164
|
+
return int(self.raw.get("patrol", {}).get("poll_interval", 10))
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def patrol_check_delay(self) -> int:
|
|
168
|
+
return int(self.raw.get("patrol", {}).get("check_delay", 15))
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def patrol_max_retry(self) -> int:
|
|
172
|
+
return int(self.raw.get("patrol", {}).get("max_retry", 3))
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def patrol_confidence(self) -> float:
|
|
176
|
+
return float(self.raw.get("patrol", {}).get("confidence", 0.7))
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def all_worker_chats(self) -> list[str]:
|
|
180
|
+
return list(self.raw.get("patrol", {}).get("all_worker_chats", []))
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def role_to_chat(self) -> dict[str, str]:
|
|
184
|
+
return dict(self.raw.get("patrol", {}).get("role_to_chat", {}))
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def roles(self) -> dict[str, dict]:
|
|
188
|
+
raw_roles = self.raw.get("roles", {})
|
|
189
|
+
return {str(key).strip(): dict(value) for key, value in raw_roles.items()}
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def fixed_roles(self) -> list[str]:
|
|
193
|
+
raw_roles = self.raw.get("ai_team", {}).get("fixed_roles", DEFAULT_FIXED_ROLES)
|
|
194
|
+
items = [str(item).strip() for item in raw_roles if str(item).strip()]
|
|
195
|
+
return items or DEFAULT_FIXED_ROLES[:]
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def cursor_only(self) -> bool:
|
|
199
|
+
return self.raw.get("ai_team", {}).get("cursor_only", True) is not False
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def idle_seconds(self) -> int:
|
|
203
|
+
return int(self.raw.get("runtime", {}).get("idle_seconds", 180))
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def stale_task_seconds(self) -> int:
|
|
207
|
+
return int(self.raw.get("runtime", {}).get("stale_task_seconds", 900))
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def sendable_roles(self) -> list[dict[str, str]]:
|
|
211
|
+
items: list[dict[str, str]] = []
|
|
212
|
+
for role_name in self.fixed_roles:
|
|
213
|
+
meta = self.roles.get(role_name, {})
|
|
214
|
+
if role_name == self.admin_sender:
|
|
215
|
+
continue
|
|
216
|
+
display_names = list(meta.get("display_names", []))
|
|
217
|
+
label = display_names[0] if display_names else role_name
|
|
218
|
+
items.append({"role": role_name, "label": label})
|
|
219
|
+
if not any(item["role"] == self.admin_target for item in items):
|
|
220
|
+
items.insert(0, {"role": self.admin_target, "label": self.admin_target})
|
|
221
|
+
return items
|
|
222
|
+
|
|
223
|
+
def validate_device(self) -> None:
|
|
224
|
+
missing = []
|
|
225
|
+
if not self.device_id:
|
|
226
|
+
missing.append("device.device_id")
|
|
227
|
+
if not self.device_name:
|
|
228
|
+
missing.append("device.device_name")
|
|
229
|
+
if not self.owner_role:
|
|
230
|
+
missing.append("device.owner_role")
|
|
231
|
+
if not self.machine_code:
|
|
232
|
+
missing.append("device.machine_code")
|
|
233
|
+
if missing:
|
|
234
|
+
raise ValueError(f"配置缺少设备身份字段: {', '.join(missing)}")
|
|
235
|
+
|
|
236
|
+
def validate_roles(self) -> None:
|
|
237
|
+
missing = [role for role in self.fixed_roles if role not in self.roles]
|
|
238
|
+
if missing:
|
|
239
|
+
raise ValueError(f"配置缺少固定角色定义: {', '.join(missing)}")
|
|
240
|
+
|
|
241
|
+
def ensure_runtime_dirs(self) -> None:
|
|
242
|
+
self.tasks_dir.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
self.reports_dir.mkdir(parents=True, exist_ok=True)
|
|
244
|
+
self.issues_dir.mkdir(parents=True, exist_ok=True)
|
|
245
|
+
self.runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
self.status_dir.mkdir(parents=True, exist_ok=True)
|
|
247
|
+
self.task_details_dir.mkdir(parents=True, exist_ok=True)
|
|
248
|
+
|
|
249
|
+
def save(self, raw: dict[str, Any] | None = None) -> None:
|
|
250
|
+
self.path.write_text(json.dumps(raw or self.raw, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
251
|
+
|
|
252
|
+
def update_bind(self, **kwargs: Any) -> None:
|
|
253
|
+
bind = dict(self.raw.get("bind", {}))
|
|
254
|
+
bind.update(kwargs)
|
|
255
|
+
self.raw["bind"] = bind
|
|
256
|
+
self.save()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def load_config(path: str | Path) -> PatrolConfig:
|
|
260
|
+
cfg_path = Path(path).expanduser().resolve()
|
|
261
|
+
raw = json.loads(cfg_path.read_text(encoding="utf-8-sig"))
|
|
262
|
+
return PatrolConfig(raw=raw, path=cfg_path)
|