@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.
- package/package.json +29 -29
- package/templates/.qoder/.runtime/hook-errors.log +4 -0
- package/templates/qoder/commands/optional/wl-status.md +2 -0
- package/templates/qoder/commands/wl-code.md +45 -10
- package/templates/qoder/commands/wl-init.md +129 -129
- package/templates/qoder/commands/wl-prd.md +27 -12
- package/templates/qoder/commands/wl-search.md +32 -0
- package/templates/qoder/commands/wl-task.md +8 -4
- package/templates/qoder/commands/wl-test.md +4 -0
- package/templates/qoder/hooks/post-tool-use.py +31 -1
- package/templates/qoder/hooks/pre-tool-use.py +136 -0
- package/templates/qoder/hooks/session-start.py +365 -365
- package/templates/qoder/scripts/capability/__pycache__/present_html.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__init__.py +4 -4
- package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/notify.py +64 -0
- package/templates/qoder/scripts/capability/caps/sandbox.py +38 -0
- package/templates/qoder/scripts/capability/present_html.py +68 -0
- package/templates/qoder/scripts/capability/registry.py +6 -2
- package/templates/qoder/scripts/capability/registry_mcp.py +312 -314
- package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/return_to_platform.py +394 -392
- package/templates/qoder/scripts/domain/integration/spec_upload.py +208 -209
- package/templates/qoder/scripts/domain/kg/switch_project.py +158 -159
- package/templates/qoder/scripts/engine/poller.py +219 -219
- package/templates/qoder/scripts/validation/metrics/__pycache__/present_board.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/validation/metrics/present_board.py +180 -0
- package/templates/qoder/settings.json +10 -0
- package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +0 -430
|
Binary file
|
|
Binary file
|
|
@@ -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
|
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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):
|