@hupan56/wlkj 2.0.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.
Files changed (87) hide show
  1. package/bin/cli.js +213 -0
  2. package/package.json +11 -0
  3. package/templates/cli.js +198 -0
  4. package/templates/qoder/commands/wl-code.md +43 -0
  5. package/templates/qoder/commands/wl-commit.md +30 -0
  6. package/templates/qoder/commands/wl-init.md +80 -0
  7. package/templates/qoder/commands/wl-insight.md +51 -0
  8. package/templates/qoder/commands/wl-prd.md +199 -0
  9. package/templates/qoder/commands/wl-report.md +166 -0
  10. package/templates/qoder/commands/wl-search.md +52 -0
  11. package/templates/qoder/commands/wl-spec.md +18 -0
  12. package/templates/qoder/commands/wl-status.md +51 -0
  13. package/templates/qoder/commands/wl-task.md +71 -0
  14. package/templates/qoder/commands/wl-test.md +42 -0
  15. package/templates/qoder/config.toml +5 -0
  16. package/templates/qoder/config.yaml +141 -0
  17. package/templates/qoder/hooks/inject-workflow-state.py +117 -0
  18. package/templates/qoder/hooks/session-start.py +204 -0
  19. package/templates/qoder/rules/wl-pipeline.md +105 -0
  20. package/templates/qoder/scripts/add_session.py +245 -0
  21. package/templates/qoder/scripts/benchmark.py +209 -0
  22. package/templates/qoder/scripts/build_style_index.py +268 -0
  23. package/templates/qoder/scripts/code_index.py +41 -0
  24. package/templates/qoder/scripts/collect_prds.py +31 -0
  25. package/templates/qoder/scripts/common/__init__.py +0 -0
  26. package/templates/qoder/scripts/common/active_task.py +230 -0
  27. package/templates/qoder/scripts/common/atomicio.py +172 -0
  28. package/templates/qoder/scripts/common/developer.py +161 -0
  29. package/templates/qoder/scripts/common/eval_api.py +144 -0
  30. package/templates/qoder/scripts/common/feishu.py +278 -0
  31. package/templates/qoder/scripts/common/filelock.py +211 -0
  32. package/templates/qoder/scripts/common/identity.py +285 -0
  33. package/templates/qoder/scripts/common/mentions.py +134 -0
  34. package/templates/qoder/scripts/common/paths.py +311 -0
  35. package/templates/qoder/scripts/common/reqid.py +218 -0
  36. package/templates/qoder/scripts/common/search_engine.py +205 -0
  37. package/templates/qoder/scripts/common/task_utils.py +342 -0
  38. package/templates/qoder/scripts/common/terms.py +234 -0
  39. package/templates/qoder/scripts/common/utf8.py +38 -0
  40. package/templates/qoder/scripts/context_pack.py +196 -0
  41. package/templates/qoder/scripts/eval_prd.py +225 -0
  42. package/templates/qoder/scripts/export.py +487 -0
  43. package/templates/qoder/scripts/git_sync.py +1087 -0
  44. package/templates/qoder/scripts/handoff.py +22 -0
  45. package/templates/qoder/scripts/init_developer.py +76 -0
  46. package/templates/qoder/scripts/init_doctor.py +527 -0
  47. package/templates/qoder/scripts/install_qoderwork.py +339 -0
  48. package/templates/qoder/scripts/learn.py +67 -0
  49. package/templates/qoder/scripts/notify.py +5 -0
  50. package/templates/qoder/scripts/parse_prds.py +33 -0
  51. package/templates/qoder/scripts/report.py +281 -0
  52. package/templates/qoder/scripts/role.py +39 -0
  53. package/templates/qoder/scripts/run_weekly_update.bat +17 -0
  54. package/templates/qoder/scripts/run_weekly_update.sh +20 -0
  55. package/templates/qoder/scripts/search_index.py +352 -0
  56. package/templates/qoder/scripts/setup.py +453 -0
  57. package/templates/qoder/scripts/setup_weekly_cron.bat +22 -0
  58. package/templates/qoder/scripts/setup_weekly_cron.sh +19 -0
  59. package/templates/qoder/scripts/status.py +389 -0
  60. package/templates/qoder/scripts/syncgate.py +330 -0
  61. package/templates/qoder/scripts/task.py +954 -0
  62. package/templates/qoder/scripts/team.py +29 -0
  63. package/templates/qoder/scripts/team_sync.py +419 -0
  64. package/templates/qoder/scripts/workspace_init.py +102 -0
  65. package/templates/qoder/settings.json +53 -0
  66. package/templates/qoder/skills/design-review/SKILL.md +25 -0
  67. package/templates/qoder/skills/prd-generator/SKILL.md +180 -0
  68. package/templates/qoder/skills/prd-review/SKILL.md +36 -0
  69. package/templates/qoder/skills/prototype-generator/SKILL.md +141 -0
  70. package/templates/qoder/skills/spec-coder/SKILL.md +69 -0
  71. package/templates/qoder/skills/spec-generator/SKILL.md +67 -0
  72. package/templates/qoder/skills/test-generator/SKILL.md +72 -0
  73. package/templates/qoder/skills/wl-commit/SKILL.md +76 -0
  74. package/templates/qoder/skills/wl-init/SKILL.md +67 -0
  75. package/templates/qoder/skills/wl-insight/SKILL.md +81 -0
  76. package/templates/qoder/skills/wl-report/SKILL.md +87 -0
  77. package/templates/qoder/skills/wl-search/SKILL.md +75 -0
  78. package/templates/qoder/skills/wl-status/SKILL.md +61 -0
  79. package/templates/qoder/skills/wl-task/SKILL.md +58 -0
  80. package/templates/qoder/templates/prd-full-template.md +103 -0
  81. package/templates/qoder/templates/prd-quick-template.md +69 -0
  82. package/templates/qoder/templates/prototype-app.html +344 -0
  83. package/templates/qoder/templates/prototype-web.html +310 -0
  84. package/templates/root/AGENTS.md +182 -0
  85. package/templates/root/README-pipeline.md +56 -0
  86. package/templates/root/ROLES.md +85 -0
  87. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -0
@@ -0,0 +1,285 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ identity.py - 零信任身份层: 可验证身份 + HMAC 操作签名
4
+
5
+ 解决"身份可伪造、操作无归属、审计可篡改"三类治理问题。
6
+
7
+ 设计:
8
+ - 每个成员注册时生成一个本地签名密钥 (32 字节 hex), 存在
9
+ workspace/members/{name}/.signing_key (gitignore, 永不离开本机)
10
+ - member.json 是团队共享的注册表, 只含 member_id / git_author / role
11
+ (不含密钥), 提交到 git 让全员可查
12
+ - 关键操作 (push / publish / finish / eval) 用密钥对 payload 做 HMAC-SHA256
13
+ 签名, 审计日志里每行带 sig, 篡改即检出
14
+ - git 作者强制: member.json 的 git_author 与本地 git config 不一致则拒绝 push
15
+
16
+ 核心 API:
17
+ register_member(name, role, git_author) # 首次注册, 生成密钥
18
+ get_member(name) -> dict | None # 查注册表
19
+ verify_member(name) -> bool # 本机有密钥 + 注册表有记录
20
+ get_signing_key(name) -> bytes # 读本地密钥
21
+ sign(name, payload: dict) -> str # HMAC 签名
22
+ verify_signature(name, sig, payload) -> bool
23
+ require_member(name) # 不存在则 exit(4) authz_denied
24
+
25
+ 安全模型说明:
26
+ - 这是内部 20 人工具, 用对称密钥 (HMAC) 而非 PKI。够用且零依赖。
27
+ - 密钥泄露 = 该成员身份被冒用; 但 member.json 在 git 历史里,
28
+ 注册表本身的完整性靠 git 保护。
29
+ - 不防"管理员作恶"(admin 可改任意 member.json), 这是内部信任边界内可接受的。
30
+ """
31
+
32
+ import hashlib
33
+ import hmac
34
+ import os
35
+ import re
36
+ import secrets
37
+ import sys
38
+ from datetime import datetime
39
+ from pathlib import Path
40
+ from typing import Optional, Dict, Any, Union
41
+
42
+ # 相对导入兄弟模块
43
+ try:
44
+ from .atomicio import atomic_write_json, safe_read_json
45
+ from .paths import get_repo_root, MEMBERS_DIR
46
+ except ImportError:
47
+ _COMMON_DIR = os.path.dirname(os.path.abspath(__file__))
48
+ if _COMMON_DIR not in sys.path:
49
+ sys.path.insert(0, _COMMON_DIR)
50
+ from atomicio import atomic_write_json, safe_read_json
51
+ # paths 可能不在 common/ (在 scripts/common/paths.py)
52
+ _SCRIPTS_DIR = os.path.dirname(_COMMON_DIR)
53
+ if _SCRIPTS_DIR not in sys.path:
54
+ sys.path.insert(0, _SCRIPTS_DIR)
55
+ from common.paths import get_repo_root, MEMBERS_DIR
56
+
57
+ __all__ = [
58
+ "IdentityError",
59
+ "register_member",
60
+ "get_member",
61
+ "list_members",
62
+ "verify_member",
63
+ "get_signing_key",
64
+ "sign",
65
+ "verify_signature",
66
+ "require_member",
67
+ "ensure_current_member",
68
+ ]
69
+
70
+ # 退出码 (全脚本统一): 4 = authz_denied
71
+ EXIT_AUTHZ = 4
72
+
73
+ # 名字合法字符: 字母/数字/中文/下划线/短横, 2-32 字符
74
+ _NAME_RE = re.compile(r"^[\w\u4e00-\u9fa5\-]{2,32}$")
75
+
76
+ # git_author 格式: "Name <email@example.com>"
77
+ _AUTHOR_RE = re.compile(r"^[^<>]+ <[^<>]+@[^<>]+>$")
78
+
79
+
80
+ class IdentityError(Exception):
81
+ """身份相关错误。"""
82
+
83
+
84
+ # ============================================================
85
+ # 路径辅助
86
+ # ============================================================
87
+
88
+ def _member_dir(name: str, repo_root: Optional[Union[str, Path]] = None) -> Path:
89
+ if repo_root is None:
90
+ repo_root = get_repo_root()
91
+ return Path(repo_root) / "workspace" / "members" / name
92
+
93
+
94
+ def _member_json(name: str, repo_root: Optional[Union[str, Path]] = None) -> Path:
95
+ return _member_dir(name, repo_root) / "member.json"
96
+
97
+
98
+ def _key_file(name: str, repo_root: Optional[Union[str, Path]] = None) -> Path:
99
+ return _member_dir(name, repo_root) / ".signing_key"
100
+
101
+
102
+ # ============================================================
103
+ # 注册表读写
104
+ # ============================================================
105
+
106
+ def get_member(name: str, repo_root: Optional[Union[str, Path]] = None) -> Optional[Dict[str, Any]]:
107
+ """查注册表。返回 member.json 内容 (不含密钥) 或 None。"""
108
+ if not name or not _NAME_RE.match(name):
109
+ return None
110
+ mp = _member_json(name, repo_root)
111
+ data = safe_read_json(mp, default=None)
112
+ if data and data.get("name") == name:
113
+ return data
114
+ return None
115
+
116
+
117
+ def list_members(repo_root: Optional[Union[str, Path]] = None) -> Dict[str, Dict[str, Any]]:
118
+ """列出所有注册成员 {name: member_json}。"""
119
+ if repo_root is None:
120
+ repo_root = get_repo_root()
121
+ members_dir = Path(repo_root) / "workspace" / "members"
122
+ out = {}
123
+ if not members_dir.is_dir():
124
+ return out
125
+ for child in members_dir.iterdir():
126
+ if not child.is_dir():
127
+ continue
128
+ mj = child / "member.json"
129
+ if mj.is_file():
130
+ data = safe_read_json(mj, default=None)
131
+ if data and data.get("name"):
132
+ out[data["name"]] = data
133
+ return out
134
+
135
+
136
+ def register_member(
137
+ name: str,
138
+ role: str = "pm",
139
+ git_author: Optional[str] = None,
140
+ repo_root: Optional[Union[str, Path]] = None,
141
+ ) -> Dict[str, Any]:
142
+ """注册/升级一个成员。
143
+
144
+ - 首次注册: 生成密钥, 写 member.json + .signing_key
145
+ - 已存在: 仅更新 role/git_author (密钥不动), 保留 member_id 和 joined_at
146
+
147
+ Args:
148
+ name: 成员名 (2-32 字符, 字母数字中文下划线短横)
149
+ role: 角色 (pm/design/dev/admin)
150
+ git_author: git 作者 "Name <email>", 用于强制 git config 一致
151
+ """
152
+ if not name or not _NAME_RE.match(name):
153
+ raise IdentityError("非法成员名 (需 2-32 字符): %r" % name)
154
+ if git_author and not _AUTHOR_RE.match(git_author):
155
+ raise IdentityError("git_author 格式应为 'Name <email>': %r" % git_author)
156
+
157
+ mdir = _member_dir(name, repo_root)
158
+ mdir.mkdir(parents=True, exist_ok=True)
159
+ mj = _member_json(name, repo_root)
160
+ kf = _key_file(name, repo_root)
161
+
162
+ existing = safe_read_json(mj, default=None)
163
+
164
+ # 生成或保留密钥
165
+ if existing and kf.is_file():
166
+ # 已注册 —— 保留密钥和 ID
167
+ signing_key_hex = kf.read_text(encoding="utf-8").strip()
168
+ member_id = existing.get("member_id") or "u_%s_%s" % (
169
+ name.lower()[:16], datetime.now().strftime("%Y")
170
+ )
171
+ joined_at = existing.get("joined_at") or datetime.now().isoformat(timespec="seconds")
172
+ else:
173
+ # 首次注册
174
+ signing_key_hex = secrets.token_hex(32)
175
+ kf.write_text(signing_key_hex + "\n", encoding="utf-8")
176
+ # 尽量只本用户可读 (POSIX); Windows 上 chmod 影响有限但不报错
177
+ try:
178
+ os.chmod(str(kf), 0o600)
179
+ except OSError:
180
+ pass
181
+ member_id = "u_%s_%s" % (name.lower()[:16], datetime.now().strftime("%Y"))
182
+ joined_at = datetime.now().isoformat(timespec="seconds")
183
+
184
+ record = {
185
+ "name": name,
186
+ "member_id": member_id,
187
+ "role": role,
188
+ "git_author": git_author or existing.get("git_author") or "%s <unknown@local>" % name,
189
+ "joined_at": joined_at,
190
+ }
191
+ atomic_write_json(mj, record)
192
+ return record
193
+
194
+
195
+ # ============================================================
196
+ # 密钥与签名
197
+ # ============================================================
198
+
199
+ def get_signing_key(name: str, repo_root: Optional[Union[str, Path]] = None) -> bytes:
200
+ """读本地签名密钥。不存在则抛 IdentityError。"""
201
+ kf = _key_file(name, repo_root)
202
+ if not kf.is_file():
203
+ raise IdentityError("成员 %s 在本机无签名密钥 (未注册或换机器)" % name)
204
+ return bytes.fromhex(kf.read_text(encoding="utf-8").strip())
205
+
206
+
207
+ def verify_member(name: str, repo_root: Optional[Union[str, Path]] = None) -> bool:
208
+ """本机有密钥 + 注册表有记录 = 可验证身份。"""
209
+ if not get_member(name, repo_root):
210
+ return False
211
+ return _key_file(name, repo_root).is_file()
212
+
213
+
214
+ def _canonical(payload: Any) -> bytes:
215
+ """payload 规范化为稳定字节串 (sorted keys, 无 ascii 转义, 无空格)。"""
216
+ import json
217
+ return json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
218
+
219
+
220
+ def sign(name: str, payload: Any, repo_root: Optional[Union[str, Path]] = None) -> str:
221
+ """用成员密钥对 payload 做 HMAC-SHA256, 返回 hex 签名。
222
+
223
+ payload 会被规范化 (sorted keys), 同样的 payload 签名稳定。
224
+ """
225
+ key = get_signing_key(name, repo_root)
226
+ return hmac.new(key, _canonical(payload), hashlib.sha256).hexdigest()
227
+
228
+
229
+ def verify_signature(
230
+ name: str,
231
+ signature: str,
232
+ payload: Any,
233
+ repo_root: Optional[Union[str, Path]] = None,
234
+ ) -> bool:
235
+ """校验签名。密钥不在本机则返回 False (不抛错, 让调用方决定)。"""
236
+ try:
237
+ expected = sign(name, payload, repo_root)
238
+ except IdentityError:
239
+ return False
240
+ return hmac.compare_digest(expected, signature)
241
+
242
+
243
+ # ============================================================
244
+ # 守卫函数 (给 CLI 脚本用)
245
+ # ============================================================
246
+
247
+ def require_member(name: Optional[str] = None, repo_root: Optional[Union[str, Path]] = None) -> str:
248
+ """要求当前身份已注册且本机有密钥。不满足则 exit(EXIT_AUTHZ)。
249
+
250
+ Args:
251
+ name: 指定名字; 不传则从 .developer 读。
252
+
253
+ Returns:
254
+ 已验证的成员名。
255
+ """
256
+ if name is None:
257
+ try:
258
+ from .developer_paths import get_developer
259
+ except ImportError:
260
+ _this = os.path.dirname(os.path.abspath(__file__))
261
+ sys.path.insert(0, _this)
262
+ from paths import get_developer # type: ignore
263
+ name = get_developer(repo_root)
264
+ if not name:
265
+ sys.stderr.write("[authz] 拒绝: 未设置开发者身份。先跑 /wl-init 注册。\n")
266
+ sys.exit(EXIT_AUTHZ)
267
+ if not verify_member(name, repo_root):
268
+ sys.stderr.write(
269
+ "[authz] 拒绝: 成员 %s 未注册或本机无密钥。先跑 /wl-init %s 注册。\n" % (name, name)
270
+ )
271
+ sys.exit(EXIT_AUTHZ)
272
+ return name
273
+
274
+
275
+ def ensure_current_member(repo_root: Optional[Union[str, Path]] = None) -> Optional[str]:
276
+ """软检查: 返回当前已验证的成员名, 或 None (不 exit)。给非破坏性命令用。"""
277
+ try:
278
+ from paths import get_developer
279
+ except ImportError:
280
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
281
+ from paths import get_developer # type: ignore
282
+ name = get_developer(repo_root)
283
+ if name and verify_member(name, repo_root):
284
+ return name
285
+ return None
@@ -0,0 +1,134 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ mentions.py - @提及解析与飞书通知 (阶段 D4)
4
+
5
+ 从 PRD 评审/任务评论/报告内容里提取 @用户名, 映射到飞书 user_key,
6
+ 通过飞书机器人推送提及通知。
7
+
8
+ 触发点:
9
+ - PRD 评审意见里有 @某人
10
+ - 任务评论 (task.py comment, 未来) 有 @某人
11
+ - 报告里 @某人
12
+
13
+ member.json 扩展字段: feishu_user_key (可选, 飞书 open_id/user_id)
14
+ """
15
+
16
+ import os
17
+ import re
18
+ import sys
19
+ from typing import List, Tuple, Optional
20
+
21
+ _THIS_DIR = os.path.dirname(os.path.abspath(__file__))
22
+ if _THIS_DIR not in sys.path:
23
+ sys.path.insert(0, _THIS_DIR)
24
+
25
+ try:
26
+ from .identity import get_member, list_members
27
+ from .feishu import send_card, is_enabled
28
+ except ImportError:
29
+ from identity import get_member, list_members
30
+ from feishu import send_card, is_enabled
31
+
32
+ __all__ = [
33
+ "extract_mentions",
34
+ "resolve_to_feishu_keys",
35
+ "notify_mentions",
36
+ ]
37
+
38
+ # @提及模式: @用户名 (支持中文/英文/数字/下划线, 2-32 字符)
39
+ MENTION_RE = re.compile(r'@([\w\u4e00-\u9fa5\-]{2,32})')
40
+
41
+
42
+ def extract_mentions(text: str, exclude: Optional[str] = None) -> List[str]:
43
+ """从文本提取 @提及的用户名。
44
+
45
+ Args:
46
+ text: 要扫描的文本。
47
+ exclude: 排除的用户名 (通常是作者自己, 避免自提及)。
48
+
49
+ Returns:
50
+ 去重后的用户名列表 (只保留在注册表里的成员)。
51
+ """
52
+ raw = set(m.group(1) for m in MENTION_RE.finditer(text))
53
+ if exclude:
54
+ raw.discard(exclude)
55
+ # 只保留已注册的成员 (避免 @system @all 等噪音)
56
+ members = list_members()
57
+ return sorted(name for name in raw if name in members)
58
+
59
+
60
+ def resolve_to_feishu_keys(names: List[str]) -> List[Tuple[str, str]]:
61
+ """把用户名映射到飞书 user_key。
62
+
63
+ Args:
64
+ names: 用户名列表。
65
+
66
+ Returns:
67
+ [(name, feishu_user_key), ...] 只含配了 feishu_user_key 的成员。
68
+ """
69
+ out = []
70
+ for name in names:
71
+ m = get_member(name)
72
+ if m and m.get('feishu_user_key'):
73
+ out.append((name, m['feishu_user_key']))
74
+ return out
75
+
76
+
77
+ def notify_mentions(
78
+ text: str,
79
+ context: str,
80
+ author: Optional[str] = None,
81
+ link: Optional[str] = None,
82
+ ) -> int:
83
+ """扫描文本里的 @提及, 给每个被提及者发飞书通知。
84
+
85
+ Args:
86
+ text: 含 @提及的文本 (如 PRD 评审意见)。
87
+ context: 上下文描述 (如 "REQ-2026-007 评审")。
88
+ author: 提及的发起人 (从 .developer 读)。
89
+ link: 相关链接 (可选)。
90
+
91
+ Returns:
92
+ 成功通知的人数。
93
+ """
94
+ if not is_enabled():
95
+ return 0
96
+
97
+ names = extract_mentions(text, exclude=author)
98
+ if not names:
99
+ return 0
100
+
101
+ # 解析到飞书 user_key
102
+ keyed = resolve_to_feishu_keys(names)
103
+ # 未配 feishu_user_key 的成员, 仍然在通知内容里显示名字 (但不定向)
104
+ unkeyed = [n for n in names if n not in dict(keyed)]
105
+
106
+ sent = 0
107
+ for name, user_key in keyed:
108
+ lines = [
109
+ f'**你被 @{author or "某人"} 提及了**',
110
+ f'上下文: {context}',
111
+ ]
112
+ if link:
113
+ lines.append(f'链接: {link}')
114
+ lines.append(f'飞书 ID: {user_key}')
115
+ try:
116
+ if send_card(f'@提及: {context}', lines, event_type=None, theme='orange'):
117
+ sent += 1
118
+ except Exception:
119
+ pass
120
+
121
+ # 未配 key 的合并发一条到群 (作为 fallback)
122
+ if unkeyed and not keyed:
123
+ lines = [
124
+ f'**{", ".join(unkeyed)} 被 @{author or "某人"} 提及**',
125
+ f'(以上成员未配置 feishu_user_key, 此为群广播)',
126
+ f'上下文: {context}',
127
+ ]
128
+ try:
129
+ send_card(f'@提及: {context}', lines, event_type=None, theme='grey')
130
+ sent += 1
131
+ except Exception:
132
+ pass
133
+
134
+ return sent