@hupan56/wlkj 3.3.0 → 3.3.2

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.
Files changed (32) hide show
  1. package/package.json +29 -29
  2. package/templates/.qoder/.runtime/hook-errors.log +4 -0
  3. package/templates/qoder/commands/optional/wl-status.md +2 -0
  4. package/templates/qoder/commands/wl-code.md +45 -10
  5. package/templates/qoder/commands/wl-init.md +129 -129
  6. package/templates/qoder/commands/wl-prd.md +27 -12
  7. package/templates/qoder/commands/wl-search.md +32 -0
  8. package/templates/qoder/commands/wl-task.md +8 -4
  9. package/templates/qoder/commands/wl-test.md +4 -0
  10. package/templates/qoder/hooks/post-tool-use.py +31 -1
  11. package/templates/qoder/hooks/pre-tool-use.py +136 -0
  12. package/templates/qoder/hooks/session-start.py +365 -365
  13. package/templates/qoder/scripts/capability/__pycache__/present_html.cpython-39.pyc +0 -0
  14. package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
  15. package/templates/qoder/scripts/capability/caps/__init__.py +4 -4
  16. package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
  17. package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
  18. package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
  19. package/templates/qoder/scripts/capability/caps/notify.py +64 -0
  20. package/templates/qoder/scripts/capability/caps/sandbox.py +38 -0
  21. package/templates/qoder/scripts/capability/present_html.py +68 -0
  22. package/templates/qoder/scripts/capability/registry.py +6 -2
  23. package/templates/qoder/scripts/capability/registry_mcp.py +312 -314
  24. package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
  25. package/templates/qoder/scripts/domain/integration/return_to_platform.py +394 -392
  26. package/templates/qoder/scripts/domain/integration/spec_upload.py +208 -209
  27. package/templates/qoder/scripts/domain/kg/switch_project.py +158 -159
  28. package/templates/qoder/scripts/engine/poller.py +219 -219
  29. package/templates/qoder/scripts/validation/metrics/__pycache__/present_board.cpython-39.pyc +0 -0
  30. package/templates/qoder/scripts/validation/metrics/present_board.py +180 -0
  31. package/templates/qoder/settings.json +10 -0
  32. package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +0 -430
@@ -11,9 +11,9 @@ from .repo import RepoCap, NoOpRepoCap
11
11
  from .identity import IdentityCap, NoOpIdentityCap
12
12
  from .context import ContextCap, NoOpContextCap
13
13
  from .cron import CronCap, NoOpCronCap
14
- from .notify import NotifyCap, NoOpNotifyCap
14
+ from .notify import NotifyCap, NoOpNotifyCap, WebhookNotifyCap, default_notify
15
15
  from .present import PresentCap, NoOpPresentCap
16
- from .sandbox import SandboxCap, NoOpSandboxCap
16
+ from .sandbox import SandboxCap, NoOpSandboxCap, SubprocessSandboxCap, default_sandbox
17
17
 
18
18
  __all__ = [
19
19
  "MemoryCap", "NoOpMemoryCap",
@@ -21,7 +21,7 @@ __all__ = [
21
21
  "IdentityCap", "NoOpIdentityCap",
22
22
  "ContextCap", "NoOpContextCap",
23
23
  "CronCap", "NoOpCronCap",
24
- "NotifyCap", "NoOpNotifyCap",
24
+ "NotifyCap", "NoOpNotifyCap", "WebhookNotifyCap", "default_notify",
25
25
  "PresentCap", "NoOpPresentCap",
26
- "SandboxCap", "NoOpSandboxCap",
26
+ "SandboxCap", "NoOpSandboxCap", "SubprocessSandboxCap", "default_sandbox",
27
27
  ]
@@ -11,6 +11,9 @@ NotifyCap —— IM 通知能力契约。
11
11
  """
12
12
  from __future__ import annotations
13
13
 
14
+ import json
15
+ import os
16
+ import urllib.request
14
17
  from typing import Protocol, runtime_checkable
15
18
 
16
19
 
@@ -39,3 +42,64 @@ class NoOpNotifyCap:
39
42
 
40
43
  def list_channels(self) -> list[dict]:
41
44
  return []
45
+
46
+
47
+ class WebhookNotifyCap:
48
+ """真实通知 adapter(#17):POST 消息到配置的 IM webhook(飞书/企业微信/钉钉)。
49
+
50
+ webhook 从 env WLKJ_NOTIFY_WEBHOOKS 解析:
51
+ "default=https://open.feishu.cn/...,review=https://..." 或 JSON '{"default":"..."}'
52
+ 用 stdlib urllib(宿主脚本可能无 httpx/requests),POST 通用 text payload。
53
+ available 仅当至少配了一个 webhook,否则 False(优雅降级 NoOp,不谎报)。
54
+ """
55
+
56
+ def __init__(self) -> None:
57
+ self._channels: dict[str, str] = self._load()
58
+
59
+ def _load(self) -> dict[str, str]:
60
+ raw = os.environ.get("WLKJ_NOTIFY_WEBHOOKS", "").strip()
61
+ if not raw:
62
+ return {}
63
+ if raw.startswith("{"): # JSON 形式
64
+ try:
65
+ d = json.loads(raw)
66
+ return {str(k): str(v) for k, v in d.items() if v}
67
+ except Exception:
68
+ return {}
69
+ out: dict[str, str] = {}
70
+ for pair in raw.split(","):
71
+ if "=" in pair:
72
+ k, v = pair.split("=", 1)
73
+ k, v = k.strip(), v.strip()
74
+ if v:
75
+ out[k] = v
76
+ return out
77
+
78
+ @property
79
+ def available(self) -> bool:
80
+ return len(self._channels) > 0
81
+
82
+ def send(self, channel: str, message: str) -> bool:
83
+ url = self._channels.get(channel) or self._channels.get("default")
84
+ if not url:
85
+ return False
86
+ try:
87
+ # 通用 text payload(飞书 msg_type/content;钉钉/企微也兼容 text 字段)
88
+ payload = json.dumps({"msg_type": "text", "content": {"text": message}}).encode("utf-8")
89
+ req = urllib.request.Request(url, data=payload,
90
+ headers={"Content-Type": "application/json"})
91
+ with urllib.request.urlopen(req, timeout=5) as r:
92
+ return getattr(r, "status", 200) == 200
93
+ except Exception:
94
+ return False # IM 不可达不阻塞工作流
95
+
96
+ def list_channels(self) -> list[dict]:
97
+ return [{"channel": k, "configured": bool(v)} for k, v in self._channels.items()]
98
+
99
+
100
+ def default_notify() -> NotifyCap:
101
+ """工厂:配了 webhook→WebhookNotifyCap;否则 NoOpNotifyCap。"""
102
+ cap = WebhookNotifyCap()
103
+ if cap.available:
104
+ return cap # type: ignore[return-value]
105
+ return NoOpNotifyCap() # type: ignore[return-value]
@@ -13,6 +13,8 @@ SandboxCap —— 安全执行能力契约。
13
13
  """
14
14
  from __future__ import annotations
15
15
 
16
+ import os
17
+ import subprocess
16
18
  from typing import Protocol, runtime_checkable
17
19
 
18
20
 
@@ -41,3 +43,39 @@ class NoOpSandboxCap:
41
43
 
42
44
  def approval_mode(self) -> str:
43
45
  return "suggest"
46
+
47
+
48
+ class SubprocessSandboxCap:
49
+ """真实沙箱 adapter(#17):子进程执行 + 超时强杀 + stdout/stderr/returncode 捕获。
50
+
51
+ 无 docker 时的进程级隔离(超时强杀 + 输出捕获 + cwd 隔离),全平台可用。
52
+ docker 容器级隔离(FS/网络限制)后续可加 DockerSandboxCap,工厂按 env 切换。
53
+ available=True(subprocess 全平台可用)。
54
+ """
55
+
56
+ available = True
57
+
58
+ def run(self, command: str, cwd: str | None = None, timeout: int = 60) -> dict:
59
+ try:
60
+ # errors='replace' 健壮化:Windows 命令常输出 GBK 中文,UTF-8 解码会崩;
61
+ # replace 不崩(个别字符显示?),比 exec_error 丢全部输出好。
62
+ p = subprocess.run(command, shell=True, cwd=cwd, timeout=timeout,
63
+ capture_output=True, text=True, errors="replace")
64
+ return {"ok": p.returncode == 0, "stdout": p.stdout,
65
+ "stderr": p.stderr, "returncode": p.returncode}
66
+ except subprocess.TimeoutExpired:
67
+ return {"ok": False, "reason": "timeout(%ds)" % timeout, "returncode": -1}
68
+ except Exception as e: # 命令不存在 / 权限 等
69
+ return {"ok": False, "reason": "exec_error: %s" % str(e)[:200], "returncode": -1}
70
+
71
+ def approval_mode(self) -> str:
72
+ return os.environ.get("WLKJ_APPROVAL_MODE", "suggest")
73
+
74
+
75
+ def default_sandbox() -> SandboxCap:
76
+ """工厂:env WLKJ_SANDBOX=off→NoOp;docker(占位)→Subprocess 兜底;默认 Subprocess。"""
77
+ mode = os.environ.get("WLKJ_SANDBOX", "subprocess").lower()
78
+ if mode == "off":
79
+ return NoOpSandboxCap() # type: ignore[return-value]
80
+ # docker 模式暂未实现容器隔离,先兜底 Subprocess(避免谎报容器级安全)
81
+ return SubprocessSandboxCap() # type: ignore[return-value]
@@ -0,0 +1,68 @@
1
+ # -*- coding: utf-8 -*-
2
+ """present_html.py — 富展示 HTML 统一入口(cap.present 双轨)。
3
+
4
+ 模式(抄 usability_score.py --html):
5
+ 1. QoderWork(cap.present.available)→ cap.present.widget(html) 嵌入对话流
6
+ 2. 降级 → 落盘 workspace/members/{dev}/journal/{name}-{ts}.html
7
+ 3. 最终兜底 → stdout
8
+
9
+ 所有命令的富展示(impact/coverage/feature/report 看板)统一调 present(html, name),
10
+ 不在各自脚本重复 widget+落盘逻辑。复用点:caps/present.py PresentCap + adapters/qw.py QwPresentCap。
11
+ """
12
+ import os
13
+ import sys
14
+ from pathlib import Path
15
+
16
+
17
+ def _resolve_repo(repo_root=None):
18
+ if repo_root:
19
+ return Path(repo_root)
20
+ env = os.environ.get("QODER_REPO") or os.environ.get("WLKJ_REPO")
21
+ if env:
22
+ return Path(env)
23
+ p = Path(__file__).resolve()
24
+ for parent in p.parents:
25
+ if (parent / ".qoder").is_dir() or (parent / "packages").is_dir():
26
+ return parent
27
+ return p.parent
28
+
29
+
30
+ def _read_dev(repo):
31
+ f = repo / ".qoder" / ".developer"
32
+ if f.is_file():
33
+ for line in f.read_text(encoding="utf-8").splitlines():
34
+ if "name=" in line:
35
+ return line.split("name=", 1)[1].strip().split()[0]
36
+ return os.environ.get("QODER_DEV", "") or "shared"
37
+
38
+
39
+ def present(html, name="dashboard", repo_root=None, dev=None):
40
+ """展示 HTML:QoderWork 嵌入对话;降级落盘 journal;兜底 stdout。返回 (sinked, path)。"""
41
+ repo = _resolve_repo(repo_root)
42
+ # 1. QoderWork widget(富宿主嵌入对话流)
43
+ try:
44
+ scripts_dir = str(repo / ".qoder" / "scripts")
45
+ if scripts_dir not in sys.path:
46
+ sys.path.insert(0, scripts_dir)
47
+ from capability.registry import resolve
48
+ cap = resolve()
49
+ if getattr(cap.present, "available", False):
50
+ if cap.present.widget(html):
51
+ return True, None
52
+ except Exception:
53
+ pass
54
+ # 2. 降级落盘
55
+ try:
56
+ out_dev = dev or _read_dev(repo)
57
+ out = repo / "workspace" / "members" / out_dev / "journal"
58
+ out.mkdir(parents=True, exist_ok=True)
59
+ from datetime import datetime
60
+ import re
61
+ safe_name = re.sub(r'[\\/:*?"<>|]', "_", name) # Windows 非法字符清洗
62
+ fp = out / ("%s-%s.html" % (safe_name, datetime.now().strftime("%Y%m%d-%H%M%S")))
63
+ fp.write_text(html, encoding="utf-8")
64
+ print("[HTML] 已落盘: %s" % fp)
65
+ return False, str(fp)
66
+ except Exception:
67
+ print(html)
68
+ return False, None
@@ -196,7 +196,9 @@ def _resolve_cron(host: str):
196
196
 
197
197
 
198
198
  def _resolve_notify(host: str):
199
- return NoOpNotifyCap()
199
+ """L2 通知 —— 配了 WLKJ_NOTIFY_WEBHOOKS→WebhookNotifyCap(飞书/企微/钉钉);否则 NoOp 降级。"""
200
+ from .caps.notify import default_notify
201
+ return default_notify()
200
202
 
201
203
 
202
204
  def _resolve_present(host: str):
@@ -211,7 +213,9 @@ def _resolve_present(host: str):
211
213
 
212
214
 
213
215
  def _resolve_sandbox(host: str):
214
- return NoOpSandboxCap()
216
+ """L2 安全执行 —— env WLKJ_SANDBOX≠off→SubprocessSandboxCap(进程级隔离+超时);off→NoOp。"""
217
+ from .caps.sandbox import default_sandbox
218
+ return default_sandbox()
215
219
 
216
220
 
217
221
  def _resolve_mcp(host: str):