devmate 2.5.0__tar.gz → 2.5.1__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.
- {devmate-2.5.0 → devmate-2.5.1}/PKG-INFO +1 -1
- {devmate-2.5.0 → devmate-2.5.1}/pyproject.toml +1 -1
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/__init__.py +1 -1
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/agent/validators.py +86 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/app/cli.py +24 -7
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/app/web/__init__.py +15 -0
- devmate-2.5.1/src/devmate/sub_agent/__init__.py +130 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/workflow/__init__.py +104 -5
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate.egg-info/PKG-INFO +1 -1
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate.egg-info/SOURCES.txt +1 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_agent/__init__.py +81 -17
- {devmate-2.5.0 → devmate-2.5.1}/README.md +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/setup.cfg +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/__main__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/agent_eval/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/ai_config/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/ai_scenarios/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/app/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/app/tui.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/app/web/cli_docs.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/bus/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/bus/bus.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/bus/command.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/core/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/core/config.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/core/logging.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/core/security.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/events/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/events/bus.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/events/domain_event.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/mcp/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/memory_store/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/module/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/module/registry.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/plugin/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/plugin_market/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/plugin_market/ratings.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/plugin_market/remote.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/rag/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/security/audit.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/security/vault.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/share/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate/skill/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate.egg-info/dependency_links.txt +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate.egg-info/entry_points.txt +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate.egg-info/requires.txt +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate.egg-info/top_level.txt +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_apidev/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_apihub/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_dbadmin/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_gitflow/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_monitor/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_notekeeper/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_plugins/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_plugins/code_stats.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_plugins/env_switch.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_plugins/file_watch.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_plugins/pomodoro.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_plugins/port_scan.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_publisher/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_regexlab/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_reporter/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_scaffold/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_sshman/__init__.py +0 -0
- {devmate-2.5.0 → devmate-2.5.1}/src/devmate_toolkit/__init__.py +0 -0
|
@@ -195,3 +195,89 @@ def summarize_validation(results: list[ValidatorResult]) -> str:
|
|
|
195
195
|
return "无适用校验器"
|
|
196
196
|
ok_count = sum(1 for r in results if r.ok)
|
|
197
197
|
return f"{ok_count}/{len(results)} 校验通过"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ═══════════════════════════════════════════════════
|
|
201
|
+
# 自动修复闭环(P0-②):校验失败 → 自动修复 → 重验
|
|
202
|
+
# ═══════════════════════════════════════════════════
|
|
203
|
+
|
|
204
|
+
def auto_fix(path: str, dry_run: bool = False) -> dict:
|
|
205
|
+
"""校验失败时尝试自动修复
|
|
206
|
+
|
|
207
|
+
当前支持:
|
|
208
|
+
- python_style: ruff check --fix(自动修风格问题)
|
|
209
|
+
- python_syntax: 无自动修复(需 Agent 介入)
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
path: 文件路径
|
|
213
|
+
dry_run: 仅预览修复(ruff --diff)
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
{"fixed": bool, "before": str, "after": str, "details": [...]}
|
|
217
|
+
"""
|
|
218
|
+
result = {"fixed": False, "before": "", "after": "", "details": []}
|
|
219
|
+
ext = Path(path).suffix.lower()
|
|
220
|
+
|
|
221
|
+
# ruff 自动修复(Python 风格问题)
|
|
222
|
+
if ext == ".py":
|
|
223
|
+
try:
|
|
224
|
+
cmd = ["ruff", "check", "--fix"]
|
|
225
|
+
if dry_run:
|
|
226
|
+
cmd = ["ruff", "check", "--diff"]
|
|
227
|
+
cmd.append(path)
|
|
228
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
229
|
+
if r.returncode in (0, 1):
|
|
230
|
+
# returncode 0 = 无问题;1 = 有问题但已修/有剩余
|
|
231
|
+
# 检测是否有修复
|
|
232
|
+
after = Path(path).read_text(errors="replace")
|
|
233
|
+
before = result["before"] # noqa: F841
|
|
234
|
+
if dry_run:
|
|
235
|
+
result["details"].append("dry-run: 预览修复差异")
|
|
236
|
+
result["after"] = r.stdout[:500]
|
|
237
|
+
else:
|
|
238
|
+
result["after"] = after[:200]
|
|
239
|
+
# 检查是否还有残留问题
|
|
240
|
+
remaining = validate_file_feedback(path)
|
|
241
|
+
if not remaining:
|
|
242
|
+
result["fixed"] = True
|
|
243
|
+
result["details"].append("ruff 自动修复完成,校验通过")
|
|
244
|
+
else:
|
|
245
|
+
result["details"].append(f"ruff 修复后仍有问题: {remaining[:100]}")
|
|
246
|
+
except FileNotFoundError:
|
|
247
|
+
result["details"].append("ruff 不可用,跳过自动修复")
|
|
248
|
+
|
|
249
|
+
# 记录修复前状态(用于对比)
|
|
250
|
+
try:
|
|
251
|
+
result["before"] = Path(path).read_text(errors="replace")[:200]
|
|
252
|
+
except Exception:
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def validate_with_autofix(path: str) -> dict:
|
|
259
|
+
"""校验 + 自动修复 + 重验 完整闭环
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
{"feedback": str, "auto_fixed": bool, "results": [...]}
|
|
263
|
+
"""
|
|
264
|
+
# 第一轮校验
|
|
265
|
+
feedback = validate_file_feedback(path)
|
|
266
|
+
auto_fixed = False
|
|
267
|
+
details = []
|
|
268
|
+
|
|
269
|
+
if feedback:
|
|
270
|
+
# 尝试自动修复
|
|
271
|
+
fix_result = auto_fix(path)
|
|
272
|
+
auto_fixed = fix_result["fixed"]
|
|
273
|
+
if fix_result["details"]:
|
|
274
|
+
details.extend(fix_result["details"])
|
|
275
|
+
# 重验
|
|
276
|
+
feedback = validate_file_feedback(path)
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
"feedback": feedback,
|
|
280
|
+
"auto_fixed": auto_fixed,
|
|
281
|
+
"details": details,
|
|
282
|
+
"results": validate_file(path),
|
|
283
|
+
}
|
|
@@ -3524,20 +3524,29 @@ def wf_drift_check():
|
|
|
3524
3524
|
|
|
3525
3525
|
|
|
3526
3526
|
@wf_group.command("daemon", help="调度守护进程")
|
|
3527
|
-
@click.argument("action", type=click.Choice(["start", "stop", "status"]))
|
|
3527
|
+
@click.argument("action", type=click.Choice(["start", "stop", "status", "autostart", "no-autostart"])) # noqa: E501
|
|
3528
3528
|
@click.option("--interval", default=30, type=int, help="检查间隔(秒)")
|
|
3529
|
-
|
|
3529
|
+
@click.option("--retries", default=0, type=int, help="失败自动重试次数")
|
|
3530
|
+
def wf_daemon(action, interval, retries):
|
|
3530
3531
|
"""调度守护进程:常驻自动执行 cron 到期的调度任务
|
|
3531
3532
|
|
|
3532
3533
|
用法:
|
|
3533
|
-
devmate wf daemon start [--interval 30]
|
|
3534
|
-
devmate wf daemon status
|
|
3535
|
-
devmate wf daemon stop
|
|
3534
|
+
devmate wf daemon start [--interval 30] [--retries 2] # 启动(重试 2 次)
|
|
3535
|
+
devmate wf daemon status # 查询状态
|
|
3536
|
+
devmate wf daemon stop # 停止
|
|
3537
|
+
devmate wf daemon autostart # 安装 launchd 开机自启(macOS)
|
|
3538
|
+
devmate wf daemon no-autostart # 卸载开机自启
|
|
3536
3539
|
"""
|
|
3537
|
-
from devmate.workflow import
|
|
3540
|
+
from devmate.workflow import (
|
|
3541
|
+
daemon_install_launchd,
|
|
3542
|
+
daemon_start,
|
|
3543
|
+
daemon_status,
|
|
3544
|
+
daemon_stop,
|
|
3545
|
+
daemon_uninstall_launchd,
|
|
3546
|
+
)
|
|
3538
3547
|
|
|
3539
3548
|
if action == "start":
|
|
3540
|
-
result = daemon_start(interval=interval)
|
|
3549
|
+
result = daemon_start(interval=interval, retries=retries)
|
|
3541
3550
|
console.print(f"[green]{result.data['message']}[/green]" if result.success
|
|
3542
3551
|
else f"[red]❌ {result.error}[/red]")
|
|
3543
3552
|
console.print(f"[dim]日志: {result.data.get('log', '')}[/dim]")
|
|
@@ -3552,6 +3561,14 @@ def wf_daemon(action, interval):
|
|
|
3552
3561
|
result = daemon_stop()
|
|
3553
3562
|
console.print(f"[green]{result.data['message']}[/green]" if result.success
|
|
3554
3563
|
else f"[yellow]❌ {result.error}[/yellow]")
|
|
3564
|
+
elif action == "autostart":
|
|
3565
|
+
result = daemon_install_launchd()
|
|
3566
|
+
console.print(f"[green]{result.data['message']}[/green]" if result.success
|
|
3567
|
+
else f"[red]❌ {result.error}[/red]")
|
|
3568
|
+
elif action == "no-autostart":
|
|
3569
|
+
result = daemon_uninstall_launchd()
|
|
3570
|
+
console.print(f"[green]{result.data['message']}[/green]" if result.success
|
|
3571
|
+
else f"[red]❌ {result.error}[/red]")
|
|
3555
3572
|
|
|
3556
3573
|
|
|
3557
3574
|
@wf_group.command("rm-step", help="删除工作流的第 N 个步骤")
|
|
@@ -781,6 +781,21 @@ async def ai_config_set(request: Request, _: str = Depends(require_auth)):
|
|
|
781
781
|
|
|
782
782
|
# ── Skill 库页面 ─────────────────────────────────
|
|
783
783
|
|
|
784
|
+
@app.get("/plugins", response_class=HTMLResponse)
|
|
785
|
+
async def plugins_page(request: Request, _: str = Depends(require_auth),
|
|
786
|
+
message: str = ""):
|
|
787
|
+
"""已安装插件管理页面"""
|
|
788
|
+
from devmate.plugin import list_installed, registry
|
|
789
|
+
installed = list_installed()
|
|
790
|
+
discovered = [p for p in registry.discover() if "error" not in p]
|
|
791
|
+
return templates.TemplateResponse(
|
|
792
|
+
"plugins.html",
|
|
793
|
+
{"request": request, "modules": MODULES, "active_module": "",
|
|
794
|
+
"installed": installed, "discovered": discovered,
|
|
795
|
+
"version": __version__, "message": message},
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
|
|
784
799
|
@app.get("/skill", response_class=HTMLResponse)
|
|
785
800
|
async def skill_page(request: Request, _: str = Depends(require_auth),
|
|
786
801
|
message: str = ""):
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""devmate.sub_agent — Sub-agent 隔离(P0-⑥ · Context Isolate)
|
|
2
|
+
|
|
3
|
+
方法论:Context Engineering 五动作之一「隔离(Isolate)」
|
|
4
|
+
- 旁支任务委派给独立上下文的 Sub-agent
|
|
5
|
+
- 主 Agent 不承载子任务细节,只拿回结构化结果摘要
|
|
6
|
+
- 长任务 → 拆子任务 → 各子任务独立上下文 → 汇总
|
|
7
|
+
|
|
8
|
+
设计:
|
|
9
|
+
- Sub-agent 复用 AgentRuntime(独立 SessionMemory,互不污染)
|
|
10
|
+
- 结果自动截断/摘要,只回传关键信息给主上下文
|
|
11
|
+
- 支持并行执行多个子任务(ThreadPoolExecutor)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path # noqa: F401
|
|
17
|
+
|
|
18
|
+
from devmate.bus.command import CommandResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class SubTask:
|
|
23
|
+
"""子任务定义"""
|
|
24
|
+
name: str # 子任务名
|
|
25
|
+
prompt: str # 子任务指令(独立上下文)
|
|
26
|
+
max_steps: int = 5 # 最大步数
|
|
27
|
+
tool_whitelist: list[str] = field(default_factory=list) # 允许的工具白名单
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict:
|
|
30
|
+
return {"name": self.name, "prompt": self.prompt,
|
|
31
|
+
"max_steps": self.max_steps,
|
|
32
|
+
"tool_whitelist": self.tool_whitelist}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class SubTaskResult:
|
|
37
|
+
"""子任务结果"""
|
|
38
|
+
name: str
|
|
39
|
+
success: bool
|
|
40
|
+
summary: str = ""
|
|
41
|
+
duration_ms: float = 0.0
|
|
42
|
+
steps: int = 0
|
|
43
|
+
error: str = ""
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict:
|
|
46
|
+
return {"name": self.name, "success": self.success,
|
|
47
|
+
"summary": self.summary[:300],
|
|
48
|
+
"duration_ms": round(self.duration_ms, 1),
|
|
49
|
+
"steps": self.steps, "error": self.error[:200]}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_subtask(task: SubTask, workdir: str = ".") -> SubTaskResult:
|
|
53
|
+
"""执行单个子任务(独立上下文)
|
|
54
|
+
|
|
55
|
+
用 AgentRuntime 跑,但:
|
|
56
|
+
- 独立 SessionMemory(不污染主 Agent)
|
|
57
|
+
- 独立工具环境(tool_whitelist 过滤)
|
|
58
|
+
- 结果只回传摘要
|
|
59
|
+
"""
|
|
60
|
+
start = time.time()
|
|
61
|
+
try:
|
|
62
|
+
from devmate_agent import AgentRuntime
|
|
63
|
+
|
|
64
|
+
runtime = AgentRuntime()
|
|
65
|
+
|
|
66
|
+
# 工具白名单过滤(隔离:子任务只看到允许的工具)
|
|
67
|
+
if task.tool_whitelist:
|
|
68
|
+
allowed = set(task.tool_whitelist)
|
|
69
|
+
for tname in list(runtime.tools.list_names()):
|
|
70
|
+
if tname not in allowed:
|
|
71
|
+
runtime.tools.unregister(tname)
|
|
72
|
+
|
|
73
|
+
# 运行子任务(dry_run=False 真实执行)
|
|
74
|
+
result = runtime.run(task.prompt, max_steps=task.max_steps)
|
|
75
|
+
|
|
76
|
+
if result.success:
|
|
77
|
+
data = result.data
|
|
78
|
+
summary = str(data.get("final_output", data.get("output", "")))[:300]
|
|
79
|
+
return SubTaskResult(
|
|
80
|
+
name=task.name, success=True, summary=summary,
|
|
81
|
+
duration_ms=(time.time() - start) * 1000,
|
|
82
|
+
steps=data.get("step_count", data.get("total_steps", 0)),
|
|
83
|
+
)
|
|
84
|
+
return SubTaskResult(
|
|
85
|
+
name=task.name, success=False,
|
|
86
|
+
error=str(result.error or "任务失败"),
|
|
87
|
+
duration_ms=(time.time() - start) * 1000,
|
|
88
|
+
)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
return SubTaskResult(
|
|
91
|
+
name=task.name, success=False, error=str(e),
|
|
92
|
+
duration_ms=(time.time() - start) * 1000,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def run_subtasks(tasks: list[SubTask], workdir: str = ".",
|
|
97
|
+
parallel: bool = True) -> CommandResult:
|
|
98
|
+
"""并行/串行执行多个子任务
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
tasks: 子任务列表
|
|
102
|
+
workdir: 工作目录
|
|
103
|
+
parallel: 是否并行(默认 True)
|
|
104
|
+
"""
|
|
105
|
+
results = []
|
|
106
|
+
if parallel and len(tasks) > 1:
|
|
107
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
108
|
+
with ThreadPoolExecutor(max_workers=min(len(tasks), 4)) as pool:
|
|
109
|
+
futures = {pool.submit(run_subtask, t, workdir): t.name
|
|
110
|
+
for t in tasks}
|
|
111
|
+
# 保持任务顺序
|
|
112
|
+
ordered = [futures[f] for f in futures]
|
|
113
|
+
for f, name in zip(futures, ordered):
|
|
114
|
+
results.append(f.result())
|
|
115
|
+
else:
|
|
116
|
+
for t in tasks:
|
|
117
|
+
results.append(run_subtask(t, workdir))
|
|
118
|
+
|
|
119
|
+
success = sum(1 for r in results if r.success)
|
|
120
|
+
total = len(results)
|
|
121
|
+
return CommandResult(success=success == total, data={
|
|
122
|
+
"results": [r.to_dict() for r in results],
|
|
123
|
+
"success_count": success,
|
|
124
|
+
"total": total,
|
|
125
|
+
"message": f"Sub-agents: {success}/{total} 成功",
|
|
126
|
+
"summary": "\n".join(
|
|
127
|
+
f"[{'✅' if r.success else '❌'}] {r.name}: "
|
|
128
|
+
f"{r.summary or r.error}" for r in results
|
|
129
|
+
)[:800],
|
|
130
|
+
})
|
|
@@ -1238,9 +1238,14 @@ def list_schedules() -> CommandResult:
|
|
|
1238
1238
|
})
|
|
1239
1239
|
|
|
1240
1240
|
|
|
1241
|
-
def run_due_workflows(now: time.struct_time | None = None
|
|
1241
|
+
def run_due_workflows(now: time.struct_time | None = None,
|
|
1242
|
+
retries: int = 0) -> CommandResult:
|
|
1242
1243
|
"""执行所有到期的调度工作流(供 scheduler 守护进程调用)
|
|
1243
1244
|
|
|
1245
|
+
Args:
|
|
1246
|
+
now: 当前时间(测试可注入)
|
|
1247
|
+
retries: 失败自动重试次数(P2-⑪,默认 0 不重试)
|
|
1248
|
+
|
|
1244
1249
|
返回: 已执行的调度列表
|
|
1245
1250
|
"""
|
|
1246
1251
|
schedules = _load_schedules()
|
|
@@ -1256,15 +1261,27 @@ def run_due_workflows(now: time.struct_time | None = None) -> CommandResult:
|
|
|
1256
1261
|
continue
|
|
1257
1262
|
result = run(name)
|
|
1258
1263
|
ok = result.success and result.data.get("fail_count", 0) == 0
|
|
1264
|
+
retry_count = 0
|
|
1265
|
+
|
|
1266
|
+
# 失败重试(指数退避:2s → 4s → 8s)
|
|
1267
|
+
while not ok and retry_count < retries:
|
|
1268
|
+
retry_count += 1
|
|
1269
|
+
import time as _t
|
|
1270
|
+
_t.sleep(min(2 ** retry_count, 10))
|
|
1271
|
+
result = run(name)
|
|
1272
|
+
ok = result.success and result.data.get("fail_count", 0) == 0
|
|
1273
|
+
|
|
1259
1274
|
# 更新调度状态
|
|
1260
1275
|
sched["last_run"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
1261
1276
|
sched["last_status"] = "success" if ok else "failed"
|
|
1277
|
+
sched["retries"] = retry_count
|
|
1262
1278
|
executed.append({
|
|
1263
1279
|
"name": name,
|
|
1264
1280
|
"status": "success" if ok else "failed",
|
|
1265
1281
|
"success_count": result.data.get("success_count", 0),
|
|
1266
1282
|
"fail_count": result.data.get("fail_count", 0),
|
|
1267
1283
|
"verified": result.data.get("verified"),
|
|
1284
|
+
"retries": retry_count,
|
|
1268
1285
|
})
|
|
1269
1286
|
# 失败告警(调度级 notify)
|
|
1270
1287
|
if sched.get("notify") and not ok:
|
|
@@ -1365,9 +1382,14 @@ def daemon_status() -> CommandResult:
|
|
|
1365
1382
|
})
|
|
1366
1383
|
|
|
1367
1384
|
|
|
1368
|
-
def daemon_start(interval: int = POLL_INTERVAL
|
|
1385
|
+
def daemon_start(interval: int = POLL_INTERVAL,
|
|
1386
|
+
retries: int = 0) -> CommandResult:
|
|
1369
1387
|
"""启动守护进程(后台)
|
|
1370
1388
|
|
|
1389
|
+
Args:
|
|
1390
|
+
interval: 检查间隔(秒)
|
|
1391
|
+
retries: 失败自动重试次数(P2-⑪)
|
|
1392
|
+
|
|
1371
1393
|
每 interval 秒检查一次 cron 到期的工作流并执行。
|
|
1372
1394
|
"""
|
|
1373
1395
|
import subprocess
|
|
@@ -1382,15 +1404,17 @@ import sys, time, json
|
|
|
1382
1404
|
sys.path.insert(0, {str(Path(__file__).resolve().parent.parent.parent)!r})
|
|
1383
1405
|
from devmate.workflow import run_due_workflows, daemon_status
|
|
1384
1406
|
INTERVAL = {int(interval)}
|
|
1407
|
+
RETRIES = {int(retries)}
|
|
1385
1408
|
def _log(msg):
|
|
1386
1409
|
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
1387
1410
|
print(f"[{{ts}}] {{msg}}", flush=True)
|
|
1388
1411
|
while True:
|
|
1389
1412
|
try:
|
|
1390
|
-
r = run_due_workflows()
|
|
1413
|
+
r = run_due_workflows(retries=RETRIES)
|
|
1391
1414
|
if r.data.get("ran"):
|
|
1392
1415
|
for item in r.data["ran"]:
|
|
1393
|
-
_log(f"执行 {{item['name']}}: {{item.get('status')}}"
|
|
1416
|
+
_log(f"执行 {{item['name']}}: {{item.get('status')}}"
|
|
1417
|
+
f"(重试{{item.get('retries', 0)}})")
|
|
1394
1418
|
except Exception as e:
|
|
1395
1419
|
_log(f"调度错误: {{e}}")
|
|
1396
1420
|
time.sleep(INTERVAL)
|
|
@@ -1405,13 +1429,88 @@ while True:
|
|
|
1405
1429
|
)
|
|
1406
1430
|
DAEMON_PID_FILE.write_text(str(proc.pid))
|
|
1407
1431
|
return CommandResult(success=True, data={
|
|
1408
|
-
"message": f"✅ 守护进程已启动 (PID {proc.pid}, 每 {interval}s
|
|
1432
|
+
"message": (f"✅ 守护进程已启动 (PID {proc.pid}, 每 {interval}s 检查, "
|
|
1433
|
+
f"重试 {retries} 次)"),
|
|
1409
1434
|
"pid": proc.pid, "log": str(DAEMON_LOG_FILE),
|
|
1410
1435
|
})
|
|
1411
1436
|
except Exception as e:
|
|
1412
1437
|
return CommandResult(success=False, error=f"启动失败: {e}")
|
|
1413
1438
|
|
|
1414
1439
|
|
|
1440
|
+
def daemon_install_launchd() -> CommandResult:
|
|
1441
|
+
"""安装 launchd 开机自启(P2-⑫ · macOS)
|
|
1442
|
+
|
|
1443
|
+
生成 ~/Library/LaunchAgents/com.devmate.scheduler.plist
|
|
1444
|
+
开机自动启动 `devmate wf daemon start`
|
|
1445
|
+
"""
|
|
1446
|
+
import shutil as _shutil
|
|
1447
|
+
import subprocess as _subprocess
|
|
1448
|
+
|
|
1449
|
+
label = "com.devmate.scheduler"
|
|
1450
|
+
plist_path = Path.home() / "Library" / "LaunchAgents" / f"{label}.plist"
|
|
1451
|
+
|
|
1452
|
+
# 找到 devmate 可执行文件
|
|
1453
|
+
devmate_bin = _shutil.which("devmate")
|
|
1454
|
+
if not devmate_bin:
|
|
1455
|
+
# 回退到 python -m devmate
|
|
1456
|
+
import sys
|
|
1457
|
+
devmate_bin = f"{sys.executable} -m devmate"
|
|
1458
|
+
|
|
1459
|
+
plist = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
1460
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1461
|
+
<plist version="1.0">
|
|
1462
|
+
<dict>
|
|
1463
|
+
<key>Label</key>
|
|
1464
|
+
<string>{label}</string>
|
|
1465
|
+
<key>ProgramArguments</key>
|
|
1466
|
+
<array>
|
|
1467
|
+
<string>bash</string>
|
|
1468
|
+
<string>-lc</string>
|
|
1469
|
+
<string>{devmate_bin} wf daemon start --interval 30 --retries 2</string>
|
|
1470
|
+
</array>
|
|
1471
|
+
<key>RunAtLoad</key>
|
|
1472
|
+
<true/>
|
|
1473
|
+
<key>KeepAlive</key>
|
|
1474
|
+
<true/>
|
|
1475
|
+
<key>StandardOutPath</key>
|
|
1476
|
+
<string>{DAEMON_LOG_FILE}</string>
|
|
1477
|
+
<key>StandardErrorPath</key>
|
|
1478
|
+
<string>{DAEMON_LOG_FILE}</string>
|
|
1479
|
+
</dict>
|
|
1480
|
+
</plist>
|
|
1481
|
+
"""
|
|
1482
|
+
try:
|
|
1483
|
+
plist_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1484
|
+
plist_path.write_text(plist)
|
|
1485
|
+
# 加载
|
|
1486
|
+
_subprocess.run(["launchctl", "load", str(plist_path)],
|
|
1487
|
+
capture_output=True, timeout=15)
|
|
1488
|
+
return CommandResult(success=True, data={
|
|
1489
|
+
"message": f"✅ 已安装开机自启: {plist_path}",
|
|
1490
|
+
"plist": str(plist_path),
|
|
1491
|
+
})
|
|
1492
|
+
except Exception as e:
|
|
1493
|
+
return CommandResult(success=False, error=f"安装失败: {e}")
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
def daemon_uninstall_launchd() -> CommandResult:
|
|
1497
|
+
"""卸载 launchd 开机自启"""
|
|
1498
|
+
import subprocess as _subprocess
|
|
1499
|
+
|
|
1500
|
+
label = "com.devmate.scheduler"
|
|
1501
|
+
plist_path = Path.home() / "Library" / "LaunchAgents" / f"{label}.plist"
|
|
1502
|
+
try:
|
|
1503
|
+
if plist_path.exists():
|
|
1504
|
+
_subprocess.run(["launchctl", "unload", str(plist_path)],
|
|
1505
|
+
capture_output=True, timeout=15)
|
|
1506
|
+
plist_path.unlink()
|
|
1507
|
+
return CommandResult(success=True, data={
|
|
1508
|
+
"message": "✅ 已卸载开机自启",
|
|
1509
|
+
})
|
|
1510
|
+
except Exception as e:
|
|
1511
|
+
return CommandResult(success=False, error=f"卸载失败: {e}")
|
|
1512
|
+
|
|
1513
|
+
|
|
1415
1514
|
def daemon_stop() -> CommandResult:
|
|
1416
1515
|
"""停止守护进程"""
|
|
1417
1516
|
st = daemon_status()
|
|
@@ -40,6 +40,7 @@ src/devmate/security/audit.py
|
|
|
40
40
|
src/devmate/security/vault.py
|
|
41
41
|
src/devmate/share/__init__.py
|
|
42
42
|
src/devmate/skill/__init__.py
|
|
43
|
+
src/devmate/sub_agent/__init__.py
|
|
43
44
|
src/devmate/workflow/__init__.py
|
|
44
45
|
src/devmate_agent/__init__.py
|
|
45
46
|
src/devmate_apidev/__init__.py
|
|
@@ -133,6 +133,10 @@ class ToolRegistry:
|
|
|
133
133
|
def list_names(self) -> list[str]:
|
|
134
134
|
return list(self._tools.keys())
|
|
135
135
|
|
|
136
|
+
def unregister(self, name: str):
|
|
137
|
+
"""移除工具(Sub-agent 隔离用)"""
|
|
138
|
+
self._tools.pop(name, None)
|
|
139
|
+
|
|
136
140
|
def register_builtin(self):
|
|
137
141
|
"""注册 16 个模块的核心工具"""
|
|
138
142
|
self.register(Tool(
|
|
@@ -384,20 +388,25 @@ class ToolRegistry:
|
|
|
384
388
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
385
389
|
p.write_text(content, encoding="utf-8")
|
|
386
390
|
result = f"已写入 {len(content)} 字节到 {path}"
|
|
387
|
-
# Post-edit checkpoint
|
|
391
|
+
# Post-edit checkpoint:写文件后自动校验 + 自动修复(验证信号注入)
|
|
388
392
|
# 下沉到 handler —— Agent 主循环 / workflow / skill 所有路径统一受益
|
|
389
393
|
try:
|
|
390
394
|
from devmate.agent.validators import (
|
|
391
395
|
summarize_validation,
|
|
392
|
-
|
|
393
|
-
validate_file_feedback,
|
|
396
|
+
validate_with_autofix,
|
|
394
397
|
)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
elif
|
|
400
|
-
result += f"\n\n[
|
|
398
|
+
outcome = validate_with_autofix(str(p))
|
|
399
|
+
if outcome["auto_fixed"]:
|
|
400
|
+
result += ("\n\n[校验] 已自动修复风格问题,"
|
|
401
|
+
"重新校验通过")
|
|
402
|
+
elif outcome["feedback"]:
|
|
403
|
+
result += f"\n\n[校验反馈]\n{outcome['feedback']}"
|
|
404
|
+
else:
|
|
405
|
+
results = outcome.get("results", [])
|
|
406
|
+
if results:
|
|
407
|
+
result += f"\n\n[校验] {summarize_validation(results)}"
|
|
408
|
+
else:
|
|
409
|
+
result += "\n\n[校验] 通过"
|
|
401
410
|
except Exception:
|
|
402
411
|
pass # 校验失败不影响写入结果
|
|
403
412
|
return result
|
|
@@ -556,21 +565,46 @@ class TraceStep:
|
|
|
556
565
|
|
|
557
566
|
@dataclass
|
|
558
567
|
class SessionMemory:
|
|
559
|
-
"""
|
|
568
|
+
"""会话记忆(滑动窗口 + 摘要压缩)"""
|
|
560
569
|
messages: list[dict] = field(default_factory=list)
|
|
561
570
|
max_turns: int = 10
|
|
571
|
+
compress: bool = False # 超窗口时压缩为摘要(P1-⑦),而非丢弃
|
|
572
|
+
summary: str = "" # 已压缩的早期历史摘要
|
|
562
573
|
|
|
563
574
|
def add(self, role: str, content: str, tool_calls: list | None = None):
|
|
564
575
|
msg = {"role": role, "content": content}
|
|
565
576
|
if tool_calls:
|
|
566
577
|
msg["tool_calls"] = tool_calls
|
|
567
578
|
self.messages.append(msg)
|
|
568
|
-
#
|
|
579
|
+
# 滑动窗口(超窗时压缩或丢弃早期历史)
|
|
569
580
|
if len(self.messages) > self.max_turns * 2:
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
self.
|
|
581
|
+
overflow = self.messages[:-(self.max_turns * 2)]
|
|
582
|
+
keep = self.messages[-(self.max_turns * 2):]
|
|
583
|
+
if self.compress:
|
|
584
|
+
self._compress(overflow)
|
|
585
|
+
if keep and keep[0]["role"] == "system":
|
|
586
|
+
keep = keep[1:]
|
|
587
|
+
# 压缩摘要作为 system 消息注入最前
|
|
588
|
+
if self.summary:
|
|
589
|
+
keep.insert(0, {
|
|
590
|
+
"role": "system",
|
|
591
|
+
"content": f"[历史摘要] {self.summary[:500]}",
|
|
592
|
+
})
|
|
593
|
+
self.messages = keep
|
|
594
|
+
|
|
595
|
+
def _compress(self, overflow: list[dict]):
|
|
596
|
+
"""把溢出的历史压缩为摘要(保留关键信息)"""
|
|
597
|
+
parts = []
|
|
598
|
+
for msg in overflow:
|
|
599
|
+
role = msg.get("role", "")
|
|
600
|
+
content = str(msg.get("content", ""))[:100]
|
|
601
|
+
if role == "user":
|
|
602
|
+
parts.append(f"用户说: {content}")
|
|
603
|
+
elif role == "assistant":
|
|
604
|
+
parts.append(f"助手: {content[:60]}")
|
|
605
|
+
elif role == "tool":
|
|
606
|
+
parts.append(f"工具({msg.get('name','')}): {content[:50]}")
|
|
607
|
+
self.summary = ";".join(parts[-10:]) # 只保留最近 10 条溢出
|
|
574
608
|
|
|
575
609
|
def add_tool_result(self, tool_call_id: str, name: str, content: str):
|
|
576
610
|
self.messages.append({
|
|
@@ -585,6 +619,7 @@ class SessionMemory:
|
|
|
585
619
|
|
|
586
620
|
def clear(self):
|
|
587
621
|
self.messages = []
|
|
622
|
+
self.summary = ""
|
|
588
623
|
|
|
589
624
|
|
|
590
625
|
_DEFAULT_SYSTEM_PROMPT = """你是一个 DevMate 智能助手,可以调用工具来帮助用户完成开发任务。
|
|
@@ -603,10 +638,11 @@ class AgentRuntime:
|
|
|
603
638
|
"""Agent 运行时——真正的 ReAct 循环 + LLM 驱动"""
|
|
604
639
|
|
|
605
640
|
def __init__(self, llm_config: dict | None = None,
|
|
606
|
-
system_prompt: str = "", connect_mcp: bool = False
|
|
641
|
+
system_prompt: str = "", connect_mcp: bool = False,
|
|
642
|
+
compress_memory: bool = True):
|
|
607
643
|
self.tools = ToolRegistry()
|
|
608
644
|
self.tools.register_builtin()
|
|
609
|
-
self.memory = SessionMemory()
|
|
645
|
+
self.memory = SessionMemory(compress=compress_memory)
|
|
610
646
|
self.trace: list[TraceStep] = []
|
|
611
647
|
self.system_prompt = system_prompt or _DEFAULT_SYSTEM_PROMPT
|
|
612
648
|
|
|
@@ -803,6 +839,12 @@ class AgentRuntime:
|
|
|
803
839
|
"duration_ms": round(duration_ms, 1),
|
|
804
840
|
})
|
|
805
841
|
self._save_audit()
|
|
842
|
+
# 行为信号自动采集(P1-⑨):成功工具调用 → 记忆观察计数自动递增
|
|
843
|
+
if success and action == "execute":
|
|
844
|
+
try:
|
|
845
|
+
self._collect_behavior_signal(tool, args)
|
|
846
|
+
except Exception:
|
|
847
|
+
pass
|
|
806
848
|
# 同步到全局审计模块
|
|
807
849
|
try:
|
|
808
850
|
from devmate.security.audit import AuditEntry, audit
|
|
@@ -817,6 +859,28 @@ class AgentRuntime:
|
|
|
817
859
|
except Exception:
|
|
818
860
|
pass
|
|
819
861
|
|
|
862
|
+
def _collect_behavior_signal(self, tool: str, args: dict):
|
|
863
|
+
"""行为信号自动采集(P1-⑨)
|
|
864
|
+
|
|
865
|
+
成功工具调用 → 记忆观察计数自动递增(无需手动 set)
|
|
866
|
+
key 形如: tool:monitor_dashboard / tool:write_file
|
|
867
|
+
"""
|
|
868
|
+
from devmate.memory_store import set_memory
|
|
869
|
+
key = f"tool:{tool}"
|
|
870
|
+
# 高频工具自动采集(排除敏感写操作)
|
|
871
|
+
if tool in ("write_file", "file_organize", "ssh_run"):
|
|
872
|
+
return
|
|
873
|
+
label = {"monitor_dashboard": "巡检系统资源",
|
|
874
|
+
"git_stats": "查看 Git 统计",
|
|
875
|
+
"http_request": "请求 API",
|
|
876
|
+
"note_search": "搜索笔记",
|
|
877
|
+
"rag_search": "检索知识库",
|
|
878
|
+
"file_size": "查看文件大小",
|
|
879
|
+
"read_file": "读取文件",
|
|
880
|
+
"skill_run": "执行 Skill",
|
|
881
|
+
"db_query": "查询数据库"}.get(tool, f"调用 {tool}")
|
|
882
|
+
set_memory(key, label, scope="session", category="behavior")
|
|
883
|
+
|
|
820
884
|
def run(self, user_message: str, max_steps: int = 10,
|
|
821
885
|
dry_run: bool = False) -> CommandResult:
|
|
822
886
|
"""运行 Agent ReAct 循环
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|