@agentunion/kite 1.3.1 → 1.4.0
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.
- package/CHANGELOG.md +287 -1
- package/cli.js +76 -0
- package/extensions/agents/assistant/entry.py +111 -1
- package/extensions/agents/assistant/server.py +263 -197
- package/extensions/channels/acp_channel/entry.py +111 -1
- package/extensions/channels/acp_channel/module.md +23 -22
- package/extensions/channels/acp_channel/server.py +263 -197
- package/extensions/event_hub_bench/entry.py +107 -1
- package/extensions/services/backup/entry.py +408 -72
- package/extensions/services/backup/module.md +24 -22
- package/extensions/services/model_service/entry.py +255 -71
- package/extensions/services/model_service/module.md +21 -22
- package/extensions/services/watchdog/entry.py +344 -90
- package/extensions/services/watchdog/monitor.py +237 -21
- package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
- package/extensions/services/web/config_example.py +35 -0
- package/extensions/services/web/config_loader.py +110 -0
- package/extensions/services/web/entry.py +114 -26
- package/extensions/services/web/module.md +35 -24
- package/extensions/services/web/pairing.py +250 -0
- package/extensions/services/web/pairing_codes.jsonl +16 -0
- package/extensions/services/web/relay.py +643 -0
- package/extensions/services/web/relay_config.json5 +67 -0
- package/extensions/services/web/routes/routes_management_ws.py +127 -0
- package/extensions/services/web/routes/routes_rpc.py +89 -0
- package/extensions/services/web/routes/routes_test.py +61 -0
- package/extensions/services/web/server.py +445 -99
- package/extensions/services/web/static/css/style.css +138 -2
- package/extensions/services/web/static/index.html +295 -2
- package/extensions/services/web/static/js/app.js +1579 -5
- package/extensions/services/web/static/js/kernel-client-example.js +161 -0
- package/extensions/services/web/static/js/kernel-client.js +383 -0
- package/extensions/services/web/static/js/registry-tests.js +558 -0
- package/extensions/services/web/static/js/token-manager.js +175 -0
- package/extensions/services/web/static/pairing.html +248 -0
- package/extensions/services/web/static/test_registry.html +262 -0
- package/extensions/services/web/web_config.json5 +29 -0
- package/kernel/entry.py +120 -32
- package/kernel/event_hub.py +159 -16
- package/kernel/module.md +36 -33
- package/kernel/registry_store.py +70 -20
- package/kernel/rpc_router.py +134 -57
- package/kernel/server.py +292 -15
- package/kite_cli/__init__.py +3 -0
- package/kite_cli/__main__.py +5 -0
- package/kite_cli/commands/__init__.py +1 -0
- package/kite_cli/commands/clean.py +101 -0
- package/kite_cli/commands/doctor.py +35 -0
- package/kite_cli/commands/history.py +111 -0
- package/kite_cli/commands/info.py +96 -0
- package/kite_cli/commands/install.py +313 -0
- package/kite_cli/commands/list.py +143 -0
- package/kite_cli/commands/log.py +81 -0
- package/kite_cli/commands/rollback.py +88 -0
- package/kite_cli/commands/search.py +73 -0
- package/kite_cli/commands/uninstall.py +85 -0
- package/kite_cli/commands/update.py +118 -0
- package/kite_cli/core/__init__.py +1 -0
- package/kite_cli/core/checker.py +142 -0
- package/kite_cli/core/dependency.py +229 -0
- package/kite_cli/core/downloader.py +209 -0
- package/kite_cli/core/install_info.py +40 -0
- package/kite_cli/core/tool_installer.py +397 -0
- package/kite_cli/core/validator.py +78 -0
- package/kite_cli/main.py +289 -0
- package/kite_cli/utils/__init__.py +1 -0
- package/kite_cli/utils/i18n.py +252 -0
- package/kite_cli/utils/interactive.py +63 -0
- package/kite_cli/utils/operation_log.py +77 -0
- package/kite_cli/utils/paths.py +34 -0
- package/kite_cli/utils/version.py +308 -0
- package/launcher/count_lines.py +34 -0
- package/launcher/entry.py +905 -166
- package/launcher/logging_setup.py +104 -0
- package/launcher/module.md +37 -37
- package/launcher/process_manager.py +12 -1
- package/package.json +2 -1
- package/scripts/plan_manager.py +315 -0
|
@@ -50,6 +50,9 @@ def init_log_files():
|
|
|
50
50
|
_log_dir = os.path.join(module_data, "log")
|
|
51
51
|
os.makedirs(_log_dir, exist_ok=True)
|
|
52
52
|
|
|
53
|
+
# Clean up orphaned instance log files before initializing
|
|
54
|
+
_cleanup_orphaned_logs()
|
|
55
|
+
|
|
53
56
|
suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
|
|
54
57
|
|
|
55
58
|
# latest.log — truncate on each startup
|
|
@@ -73,6 +76,107 @@ def init_log_files():
|
|
|
73
76
|
_resolve_daily_log_path()
|
|
74
77
|
|
|
75
78
|
|
|
79
|
+
def _cleanup_orphaned_logs():
|
|
80
|
+
"""Clean up log files from dead instances (files with ~N suffix whose launcher is not running)."""
|
|
81
|
+
if not _log_dir or not os.path.isdir(_log_dir):
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Get state directory path
|
|
86
|
+
module_data = os.environ.get("KITE_MODULE_DATA")
|
|
87
|
+
if not module_data:
|
|
88
|
+
return
|
|
89
|
+
state_dir = os.path.join(os.path.dirname(module_data), "state")
|
|
90
|
+
|
|
91
|
+
# Scan for files with ~N suffix
|
|
92
|
+
import glob
|
|
93
|
+
orphaned_files = []
|
|
94
|
+
|
|
95
|
+
for pattern in ["latest~*.log", "crashes~*.jsonl"]:
|
|
96
|
+
for filepath in glob.glob(os.path.join(_log_dir, pattern)):
|
|
97
|
+
basename = os.path.basename(filepath)
|
|
98
|
+
# Extract instance number from filename
|
|
99
|
+
instance_num = _extract_instance_num(basename)
|
|
100
|
+
if instance_num is None:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
# Check if corresponding launcher is still running
|
|
104
|
+
if not _is_instance_alive(state_dir, instance_num):
|
|
105
|
+
orphaned_files.append(filepath)
|
|
106
|
+
|
|
107
|
+
# Delete orphaned files
|
|
108
|
+
if orphaned_files:
|
|
109
|
+
_builtin_print(f"[launcher] 清理 {len(orphaned_files)} 个孤儿日志文件:")
|
|
110
|
+
for filepath in orphaned_files:
|
|
111
|
+
try:
|
|
112
|
+
os.remove(filepath)
|
|
113
|
+
_builtin_print(f"[launcher] 已删除: {os.path.basename(filepath)}")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
_builtin_print(f"[launcher] 删除失败 {os.path.basename(filepath)}: {e}")
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
_builtin_print(f"[launcher] 清理孤儿日志文件时出错: {e}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _extract_instance_num(filename: str) -> int | None:
|
|
122
|
+
"""Extract instance number from log filename. Returns None if not a valid instance file."""
|
|
123
|
+
import re
|
|
124
|
+
# Match patterns like "latest~2.log" or "crashes~3.jsonl"
|
|
125
|
+
m = re.match(r"^(?:latest|crashes)~(\d+)\.(log|jsonl)$", filename)
|
|
126
|
+
return int(m.group(1)) if m else None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _is_instance_alive(state_dir: str, instance_num: int) -> bool:
|
|
130
|
+
"""Check if a launcher instance is still running by checking its processes file and PID."""
|
|
131
|
+
if not os.path.isdir(state_dir):
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
# Determine processes filename
|
|
135
|
+
if instance_num == 1:
|
|
136
|
+
processes_file = os.path.join(state_dir, "processes.json")
|
|
137
|
+
else:
|
|
138
|
+
processes_file = os.path.join(state_dir, f"processes~{instance_num}.json")
|
|
139
|
+
|
|
140
|
+
# If processes file doesn't exist, instance is dead
|
|
141
|
+
if not os.path.isfile(processes_file):
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
# Read launcher_pid from processes file
|
|
145
|
+
try:
|
|
146
|
+
import json
|
|
147
|
+
with open(processes_file, "r", encoding="utf-8") as f:
|
|
148
|
+
data = json.load(f)
|
|
149
|
+
launcher_pid = data.get("launcher_pid")
|
|
150
|
+
if not launcher_pid:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
# Check if process is still running
|
|
154
|
+
return _is_process_running(launcher_pid)
|
|
155
|
+
except Exception:
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_process_running(pid: int) -> bool:
|
|
160
|
+
"""Check if a process with given PID is running (cross-platform)."""
|
|
161
|
+
try:
|
|
162
|
+
if sys.platform == "win32":
|
|
163
|
+
# Windows: use tasklist
|
|
164
|
+
import subprocess
|
|
165
|
+
result = subprocess.run(
|
|
166
|
+
["tasklist", "/FI", f"PID eq {pid}", "/NH"],
|
|
167
|
+
capture_output=True,
|
|
168
|
+
text=True,
|
|
169
|
+
timeout=2
|
|
170
|
+
)
|
|
171
|
+
return str(pid) in result.stdout
|
|
172
|
+
else:
|
|
173
|
+
# Unix: send signal 0
|
|
174
|
+
os.kill(pid, 0)
|
|
175
|
+
return True
|
|
176
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
|
|
76
180
|
def _resolve_daily_log_path():
|
|
77
181
|
"""Resolve the daily log file path based on current date."""
|
|
78
182
|
global _log_daily_path, _log_daily_date
|
package/launcher/module.md
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: launcher
|
|
3
|
-
display_name:
|
|
4
|
-
version:
|
|
5
|
-
type: infrastructure
|
|
6
|
-
state: enabled
|
|
7
|
-
events:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
subscriptions: []
|
|
12
|
-
discovery:
|
|
13
|
-
test_modules:
|
|
14
|
-
type: scan_dir
|
|
15
|
-
path: test_modules
|
|
16
|
-
enabled: true
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Launcher
|
|
20
|
-
|
|
21
|
-
Kite 系统的启动器和进程管理器。与 main.py 在同一进程中运行,是整个系统的入口和控制台。
|
|
22
|
-
|
|
23
|
-
## 职责
|
|
24
|
-
|
|
25
|
-
- 启动 Registry,等待就绪
|
|
26
|
-
- 扫描 core/ 和 extensions/ 下的模块
|
|
27
|
-
- 并行启动 state: enabled 的模块
|
|
28
|
-
- 监控子进程状态,core 模块崩溃触发全量重启
|
|
29
|
-
- 提供 Launcher API 供其他模块启停管理
|
|
30
|
-
- 优雅退出时清理所有子进程
|
|
31
|
-
|
|
32
|
-
## API
|
|
33
|
-
|
|
34
|
-
- `GET /launcher/modules` — 列出所有模块及状态
|
|
35
|
-
- `POST /launcher/modules/{name}/start` — 启动模块
|
|
36
|
-
- `POST /launcher/modules/{name}/stop` — 停止模块
|
|
37
|
-
- `PUT /launcher/modules/{name}/state` — 修改模块 state
|
|
1
|
+
---
|
|
2
|
+
name: launcher
|
|
3
|
+
display_name: Launcher
|
|
4
|
+
version: '1.0'
|
|
5
|
+
type: infrastructure
|
|
6
|
+
state: enabled
|
|
7
|
+
events:
|
|
8
|
+
- module.started
|
|
9
|
+
- module.stopped
|
|
10
|
+
- module.state_changed
|
|
11
|
+
subscriptions: []
|
|
12
|
+
discovery:
|
|
13
|
+
test_modules:
|
|
14
|
+
type: scan_dir
|
|
15
|
+
path: test_modules
|
|
16
|
+
enabled: true
|
|
17
|
+
monitor: true
|
|
18
|
+
---
|
|
19
|
+
# Launcher
|
|
20
|
+
|
|
21
|
+
Kite 系统的启动器和进程管理器。与 main.py 在同一进程中运行,是整个系统的入口和控制台。
|
|
22
|
+
|
|
23
|
+
## 职责
|
|
24
|
+
|
|
25
|
+
- 启动 Registry,等待就绪
|
|
26
|
+
- 扫描 core/ 和 extensions/ 下的模块
|
|
27
|
+
- 并行启动 state: enabled 的模块
|
|
28
|
+
- 监控子进程状态,core 模块崩溃触发全量重启
|
|
29
|
+
- 提供 Launcher API 供其他模块启停管理
|
|
30
|
+
- 优雅退出时清理所有子进程
|
|
31
|
+
|
|
32
|
+
## API
|
|
33
|
+
|
|
34
|
+
- `GET /launcher/modules` — 列出所有模块及状态
|
|
35
|
+
- `POST /launcher/modules/{name}/start` — 启动模块
|
|
36
|
+
- `POST /launcher/modules/{name}/stop` — 停止模块
|
|
37
|
+
- `PUT /launcher/modules/{name}/state` — 修改模块 state
|
|
@@ -327,8 +327,19 @@ class ProcessManager:
|
|
|
327
327
|
return 0
|
|
328
328
|
|
|
329
329
|
# Dead launcher (or old format) — clean up its child processes
|
|
330
|
+
# Sort: watchdog first, kernel last, others in middle (prevents cascading issues)
|
|
331
|
+
def _cleanup_sort_key(entry):
|
|
332
|
+
name = entry.get("name", "")
|
|
333
|
+
if name == "watchdog":
|
|
334
|
+
return (0, name)
|
|
335
|
+
if name == "kernel":
|
|
336
|
+
return (2, name)
|
|
337
|
+
return (1, name)
|
|
338
|
+
|
|
339
|
+
records_sorted = sorted(records, key=_cleanup_sort_key)
|
|
340
|
+
|
|
330
341
|
killed = 0
|
|
331
|
-
for entry in
|
|
342
|
+
for entry in records_sorted:
|
|
332
343
|
pid = entry.get("pid", 0)
|
|
333
344
|
cmd = entry.get("cmd", [])
|
|
334
345
|
name = entry.get("name", "?")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentunion/kite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Kite framework launcher — start Kite from anywhere",
|
|
5
5
|
"bin": {
|
|
6
6
|
"kite": "./cli.js"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"launcher/**",
|
|
12
12
|
"kernel/**",
|
|
13
13
|
"extensions/**",
|
|
14
|
+
"kite_cli/**",
|
|
14
15
|
"scripts/**",
|
|
15
16
|
"CHANGELOG.md",
|
|
16
17
|
"!**/__pycache__",
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Kite CLI 开发计划管理器
|
|
3
|
+
|
|
4
|
+
用于管理开发计划的进度、状态和任务分配。
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Dict, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PlanManager:
|
|
14
|
+
"""开发计划管理器"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, plan_file: str = None):
|
|
17
|
+
if plan_file is None:
|
|
18
|
+
plan_file = Path(__file__).parent.parent / "docs" / "CLI开发计划.md"
|
|
19
|
+
self.plan_file = Path(plan_file)
|
|
20
|
+
self.state_file = self.plan_file.parent / ".cli_plan_state.json"
|
|
21
|
+
self.state = self._load_state()
|
|
22
|
+
|
|
23
|
+
def _load_state(self) -> Dict:
|
|
24
|
+
"""加载计划状态"""
|
|
25
|
+
if self.state_file.exists():
|
|
26
|
+
with open(self.state_file, "r", encoding="utf-8") as f:
|
|
27
|
+
return json.load(f)
|
|
28
|
+
return {
|
|
29
|
+
"tasks": {},
|
|
30
|
+
"history": []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def _save_state(self):
|
|
34
|
+
"""保存计划状态"""
|
|
35
|
+
with open(self.state_file, "w", encoding="utf-8") as f:
|
|
36
|
+
json.dump(self.state, f, indent=2, ensure_ascii=False)
|
|
37
|
+
|
|
38
|
+
def list_tasks(self, phase: Optional[str] = None, status: Optional[str] = None):
|
|
39
|
+
"""列出任务"""
|
|
40
|
+
content = self.plan_file.read_text(encoding="utf-8")
|
|
41
|
+
|
|
42
|
+
# 解析任务
|
|
43
|
+
tasks = []
|
|
44
|
+
current_phase = None
|
|
45
|
+
current_section = None
|
|
46
|
+
|
|
47
|
+
for line in content.split("\n"):
|
|
48
|
+
# 检测 Phase
|
|
49
|
+
phase_match = re.match(r"## (Phase \d+):", line)
|
|
50
|
+
if phase_match:
|
|
51
|
+
current_phase = phase_match.group(1)
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
# 检测子任务
|
|
55
|
+
section_match = re.match(r"### ([\d.]+) (.+)", line)
|
|
56
|
+
if section_match:
|
|
57
|
+
task_id = section_match.group(1)
|
|
58
|
+
task_name = section_match.group(2)
|
|
59
|
+
current_section = f"{current_phase}.{task_id}"
|
|
60
|
+
|
|
61
|
+
# 获取状态
|
|
62
|
+
task_state = self.state["tasks"].get(current_section, {})
|
|
63
|
+
task_status = task_state.get("status", "待开始")
|
|
64
|
+
|
|
65
|
+
tasks.append({
|
|
66
|
+
"id": current_section,
|
|
67
|
+
"phase": current_phase,
|
|
68
|
+
"name": task_name,
|
|
69
|
+
"status": task_status,
|
|
70
|
+
"assignee": task_state.get("assignee"),
|
|
71
|
+
"started_at": task_state.get("started_at"),
|
|
72
|
+
"completed_at": task_state.get("completed_at")
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# 过滤
|
|
76
|
+
if phase:
|
|
77
|
+
tasks = [t for t in tasks if t["phase"] == phase]
|
|
78
|
+
if status:
|
|
79
|
+
tasks = [t for t in tasks if t["status"] == status]
|
|
80
|
+
|
|
81
|
+
return tasks
|
|
82
|
+
|
|
83
|
+
def show_tasks(self, phase: Optional[str] = None, status: Optional[str] = None):
|
|
84
|
+
"""显示任务列表"""
|
|
85
|
+
tasks = self.list_tasks(phase, status)
|
|
86
|
+
|
|
87
|
+
if not tasks:
|
|
88
|
+
print("没有找到任务")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
print(f"\n{'ID':<20} {'任务名称':<30} {'状态':<10} {'负责人':<10}")
|
|
92
|
+
print("-" * 80)
|
|
93
|
+
|
|
94
|
+
for task in tasks:
|
|
95
|
+
task_id = task["id"]
|
|
96
|
+
name = task["name"][:28] + ".." if len(task["name"]) > 30 else task["name"]
|
|
97
|
+
status_str = task["status"]
|
|
98
|
+
assignee = task["assignee"] or "-"
|
|
99
|
+
|
|
100
|
+
print(f"{task_id:<20} {name:<30} {status_str:<10} {assignee:<10}")
|
|
101
|
+
|
|
102
|
+
print(f"\n共 {len(tasks)} 个任务")
|
|
103
|
+
|
|
104
|
+
def start_task(self, task_id: str, assignee: str = "开发者"):
|
|
105
|
+
"""开始任务"""
|
|
106
|
+
if task_id not in self.state["tasks"]:
|
|
107
|
+
self.state["tasks"][task_id] = {}
|
|
108
|
+
|
|
109
|
+
self.state["tasks"][task_id].update({
|
|
110
|
+
"status": "进行中",
|
|
111
|
+
"assignee": assignee,
|
|
112
|
+
"started_at": datetime.now().isoformat()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
self.state["history"].append({
|
|
116
|
+
"action": "start",
|
|
117
|
+
"task_id": task_id,
|
|
118
|
+
"assignee": assignee,
|
|
119
|
+
"timestamp": datetime.now().isoformat()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
self._save_state()
|
|
123
|
+
print(f"[Done] 任务 {task_id} 已开始")
|
|
124
|
+
|
|
125
|
+
def complete_task(self, task_id: str, note: str = None):
|
|
126
|
+
"""完成任务"""
|
|
127
|
+
if task_id not in self.state["tasks"]:
|
|
128
|
+
print(f"[Error] 任务 {task_id} 不存在")
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
self.state["tasks"][task_id].update({
|
|
132
|
+
"status": "已完成",
|
|
133
|
+
"completed_at": datetime.now().isoformat(),
|
|
134
|
+
"note": note
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
self.state["history"].append({
|
|
138
|
+
"action": "complete",
|
|
139
|
+
"task_id": task_id,
|
|
140
|
+
"note": note,
|
|
141
|
+
"timestamp": datetime.now().isoformat()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
self._save_state()
|
|
145
|
+
print(f"[Done] 任务 {task_id} 已完成")
|
|
146
|
+
|
|
147
|
+
def block_task(self, task_id: str, reason: str):
|
|
148
|
+
"""阻塞任务"""
|
|
149
|
+
if task_id not in self.state["tasks"]:
|
|
150
|
+
self.state["tasks"][task_id] = {}
|
|
151
|
+
|
|
152
|
+
self.state["tasks"][task_id].update({
|
|
153
|
+
"status": "阻塞",
|
|
154
|
+
"blocked_reason": reason,
|
|
155
|
+
"blocked_at": datetime.now().isoformat()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
self.state["history"].append({
|
|
159
|
+
"action": "block",
|
|
160
|
+
"task_id": task_id,
|
|
161
|
+
"reason": reason,
|
|
162
|
+
"timestamp": datetime.now().isoformat()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
self._save_state()
|
|
166
|
+
print(f"[Warning] 任务 {task_id} 已阻塞: {reason}")
|
|
167
|
+
|
|
168
|
+
def unblock_task(self, task_id: str):
|
|
169
|
+
"""解除阻塞"""
|
|
170
|
+
if task_id not in self.state["tasks"]:
|
|
171
|
+
print(f"[Error] 任务 {task_id} 不存在")
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
self.state["tasks"][task_id].update({
|
|
175
|
+
"status": "进行中",
|
|
176
|
+
"blocked_reason": None,
|
|
177
|
+
"unblocked_at": datetime.now().isoformat()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
self.state["history"].append({
|
|
181
|
+
"action": "unblock",
|
|
182
|
+
"task_id": task_id,
|
|
183
|
+
"timestamp": datetime.now().isoformat()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
self._save_state()
|
|
187
|
+
print(f"[Done] 任务 {task_id} 已解除阻塞")
|
|
188
|
+
|
|
189
|
+
def show_progress(self):
|
|
190
|
+
"""显示整体进度"""
|
|
191
|
+
tasks = self.list_tasks()
|
|
192
|
+
|
|
193
|
+
total = len(tasks)
|
|
194
|
+
completed = len([t for t in tasks if t["status"] == "已完成"])
|
|
195
|
+
in_progress = len([t for t in tasks if t["status"] == "进行中"])
|
|
196
|
+
blocked = len([t for t in tasks if t["status"] == "阻塞"])
|
|
197
|
+
pending = total - completed - in_progress - blocked
|
|
198
|
+
|
|
199
|
+
print("\n=== Kite CLI 开发进度 ===\n")
|
|
200
|
+
print(f"总任务数: {total}")
|
|
201
|
+
print(f"已完成: {completed} ({completed/total*100:.1f}%)")
|
|
202
|
+
print(f"进行中: {in_progress} ({in_progress/total*100:.1f}%)")
|
|
203
|
+
print(f"阻塞: {blocked} ({blocked/total*100:.1f}%)")
|
|
204
|
+
print(f"待开始: {pending} ({pending/total*100:.1f}%)")
|
|
205
|
+
|
|
206
|
+
# 按 Phase 统计
|
|
207
|
+
print("\n=== 各阶段进度 ===\n")
|
|
208
|
+
phases = {}
|
|
209
|
+
for task in tasks:
|
|
210
|
+
phase = task["phase"]
|
|
211
|
+
if phase not in phases:
|
|
212
|
+
phases[phase] = {"total": 0, "completed": 0}
|
|
213
|
+
phases[phase]["total"] += 1
|
|
214
|
+
if task["status"] == "已完成":
|
|
215
|
+
phases[phase]["completed"] += 1
|
|
216
|
+
|
|
217
|
+
for phase, stats in sorted(phases.items()):
|
|
218
|
+
total_p = stats["total"]
|
|
219
|
+
completed_p = stats["completed"]
|
|
220
|
+
percent = completed_p / total_p * 100 if total_p > 0 else 0
|
|
221
|
+
print(f"{phase}: {completed_p}/{total_p} ({percent:.1f}%)")
|
|
222
|
+
|
|
223
|
+
def show_history(self, limit: int = 10):
|
|
224
|
+
"""显示操作历史"""
|
|
225
|
+
history = self.state["history"][-limit:]
|
|
226
|
+
|
|
227
|
+
if not history:
|
|
228
|
+
print("暂无操作历史")
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
print(f"\n最近 {len(history)} 条操作:\n")
|
|
232
|
+
|
|
233
|
+
for entry in history:
|
|
234
|
+
timestamp = entry["timestamp"][:19]
|
|
235
|
+
action = entry["action"]
|
|
236
|
+
task_id = entry["task_id"]
|
|
237
|
+
|
|
238
|
+
action_map = {
|
|
239
|
+
"start": "开始",
|
|
240
|
+
"complete": "完成",
|
|
241
|
+
"block": "阻塞",
|
|
242
|
+
"unblock": "解除阻塞"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
action_str = action_map.get(action, action)
|
|
246
|
+
print(f"{timestamp} - {action_str} {task_id}")
|
|
247
|
+
|
|
248
|
+
if action == "complete" and entry.get("note"):
|
|
249
|
+
print(f" 备注: {entry['note']}")
|
|
250
|
+
elif action == "block" and entry.get("reason"):
|
|
251
|
+
print(f" 原因: {entry['reason']}")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def main():
|
|
255
|
+
"""命令行入口"""
|
|
256
|
+
import argparse
|
|
257
|
+
|
|
258
|
+
parser = argparse.ArgumentParser(description="Kite CLI 开发计划管理器")
|
|
259
|
+
subparsers = parser.add_subparsers(dest="command", help="命令")
|
|
260
|
+
|
|
261
|
+
# list 命令
|
|
262
|
+
list_parser = subparsers.add_parser("list", help="列出任务")
|
|
263
|
+
list_parser.add_argument("--phase", help="过滤 Phase")
|
|
264
|
+
list_parser.add_argument("--status", help="过滤状态")
|
|
265
|
+
|
|
266
|
+
# start 命令
|
|
267
|
+
start_parser = subparsers.add_parser("start", help="开始任务")
|
|
268
|
+
start_parser.add_argument("task_id", help="任务 ID")
|
|
269
|
+
start_parser.add_argument("--assignee", default="开发者", help="负责人")
|
|
270
|
+
|
|
271
|
+
# complete 命令
|
|
272
|
+
complete_parser = subparsers.add_parser("complete", help="完成任务")
|
|
273
|
+
complete_parser.add_argument("task_id", help="任务 ID")
|
|
274
|
+
complete_parser.add_argument("--note", help="备注")
|
|
275
|
+
|
|
276
|
+
# block 命令
|
|
277
|
+
block_parser = subparsers.add_parser("block", help="阻塞任务")
|
|
278
|
+
block_parser.add_argument("task_id", help="任务 ID")
|
|
279
|
+
block_parser.add_argument("reason", help="阻塞原因")
|
|
280
|
+
|
|
281
|
+
# unblock 命令
|
|
282
|
+
unblock_parser = subparsers.add_parser("unblock", help="解除阻塞")
|
|
283
|
+
unblock_parser.add_argument("task_id", help="任务 ID")
|
|
284
|
+
|
|
285
|
+
# progress 命令
|
|
286
|
+
progress_parser = subparsers.add_parser("progress", help="显示进度")
|
|
287
|
+
|
|
288
|
+
# history 命令
|
|
289
|
+
history_parser = subparsers.add_parser("history", help="显示历史")
|
|
290
|
+
history_parser.add_argument("--limit", type=int, default=10, help="显示条数")
|
|
291
|
+
|
|
292
|
+
args = parser.parse_args()
|
|
293
|
+
|
|
294
|
+
manager = PlanManager()
|
|
295
|
+
|
|
296
|
+
if args.command == "list":
|
|
297
|
+
manager.show_tasks(phase=args.phase, status=args.status)
|
|
298
|
+
elif args.command == "start":
|
|
299
|
+
manager.start_task(args.task_id, args.assignee)
|
|
300
|
+
elif args.command == "complete":
|
|
301
|
+
manager.complete_task(args.task_id, args.note)
|
|
302
|
+
elif args.command == "block":
|
|
303
|
+
manager.block_task(args.task_id, args.reason)
|
|
304
|
+
elif args.command == "unblock":
|
|
305
|
+
manager.unblock_task(args.task_id)
|
|
306
|
+
elif args.command == "progress":
|
|
307
|
+
manager.show_progress()
|
|
308
|
+
elif args.command == "history":
|
|
309
|
+
manager.show_history(args.limit)
|
|
310
|
+
else:
|
|
311
|
+
parser.print_help()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
if __name__ == "__main__":
|
|
315
|
+
main()
|