@hupan56/wlkj 2.7.12 → 3.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 (152) hide show
  1. package/bin/cli.js +344 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +113 -4
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
6
+ if _cp not in _s.path: _s.path.insert(0, _cp)
7
+ from bootstrap import setup; setup()
8
+
3
9
  """
4
10
  benchmark.py - QODER 流水线性能基准 (性能优化 A7)
5
11
 
@@ -1,196 +1,202 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- 全自动登录: 获取验证码 ddddocr识别 页面JS加密提交 → 保存token
4
- 登录态保存到 auth-state.json, 后续测试复用(无需再次识别验证码)
5
- """
6
- import base64
7
- import json
8
- import os
9
- import sys
10
- import requests
11
- import ddddocr
12
-
13
- BASE_URL = 'https://icsqas.inforecloud.com'
14
- API_PREFIX = '/test-api'
15
- USERNAME = '15888888887'
16
- PASSWORD = 'admin123'
17
- CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
18
-
19
- # RSA公钥 (从 .env.production)
20
- RSA_PUBLIC_KEY_B64 = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDN/EkuOg1gq/t2/PUI8GpcJCsCFQgHkSHUEdk7ZPrEQYd8IM1hFkHmDJrCOQ+qUPqKHG9a5BAI44yE6FuPI9Vvqu2ZrCww/zryR7A+2n5jaKIG2rJG9KHDzT5mAVOJrlhBf8L2i7wZc8eWrNg0md0BlwEowQyVEEj9oIBlw/nqTwIDAQAB'
21
-
22
- # 保存路径
23
- SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
24
-
25
-
26
- def _read_developer():
27
- """读 .qoder/.developer 拿当前开发者名, 用于身份隔离。
28
- 多人共用 workspace 时, auth-state 必须按开发者分目录, 否则会串登录态。
29
- 没有身份 抛异常 (绝不 fallback 到 hupan/default, 防串号)。"""
30
- dev_file = os.path.join(SCRIPTS_DIR, '..', '..', '.qoder', '.developer')
31
- if os.path.isfile(dev_file):
32
- for ln in open(dev_file, encoding='utf-8'):
33
- if ln.strip().startswith('name='):
34
- return ln.strip().split('=', 1)[1].strip()
35
- raise RuntimeError(
36
- '未检测到开发者身份(.qoder/.developer)。请先运行 /wl-init 注册身份。'
37
- ' (auth-state 按开发者分目录, 没身份无法隔离 → 拒绝执行防串号)'
38
- )
39
-
40
-
41
- STATE_FILE = os.path.join(SCRIPTS_DIR, '..', '..', 'workspace', 'members',
42
- _read_developer(), 'auth-state.json')
43
-
44
- out = []
45
- def log(s=''):
46
- out.append(s)
47
- print(s)
48
-
49
-
50
- def get_captcha():
51
- """获取验证码图片 + uuid"""
52
- resp = requests.get(BASE_URL + API_PREFIX + '/auth/code', timeout=10)
53
- data = resp.json().get('data', {})
54
- return data.get('uuid'), data.get('img'), data.get('captchaEnabled', False)
55
-
56
-
57
- def recognize_captcha(img_b64):
58
- """用 ddddocr 识别验证码图片, 并解析数学题(ICS a op b = ? 形式).
59
- 返回算出的答案字符串; 解析失败返回 None."""
60
- import re as _re
61
- ocr = ddddocr.DdddOcr(show_ad=False)
62
- img_bytes = base64.b64decode(img_b64)
63
- raw = ocr.classification(img_bytes)
64
- # 规范化运算符 (x/X/× *, ÷ /), 去掉 = ? 尾部
65
- expr = raw.replace('x', '*').replace('X', '*').replace('×', '*').replace('÷', '/')
66
- expr = expr.rstrip('=??').strip()
67
- m = _re.match(r'^\s*(\d+)\s*([+\-*/])\s*(\d+)\s*$', expr)
68
- if not m:
69
- return None
70
- a, op, b = int(m.group(1)), m.group(2), int(m.group(3))
71
- if op == '+':
72
- ans = a + b
73
- elif op == '-':
74
- ans = a - b
75
- elif op == '*':
76
- ans = a * b
77
- elif op == '/':
78
- ans = a // b if b else None
79
- else:
80
- return None
81
- return str(ans) if ans is not None else None
82
-
83
-
84
- def encrypt_login(username, password, code, uuid):
85
- """用 RSA+AES 加密登录请求 (复现前端加密逻辑)。
86
- 使用 cryptography 库 (替代 pycryptodome)。"""
87
- import os as _os
88
- from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
89
- from cryptography.hazmat.primitives.padding import PKCS7
90
- from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
91
- from cryptography.hazmat.primitives.serialization import load_der_public_key
92
-
93
- # 1. 生成随机 AES 密钥 (前端用 randomUUID = 32 hex chars)
94
- aes_key = _os.urandom(16).hex().encode('utf-8') # 32 bytes
95
-
96
- # 2. AES-ECB-PKCS7 加密请求体
97
- login_body = json.dumps({
98
- 'username': username,
99
- 'password': password,
100
- 'grantType': 'password',
101
- 'tenantId': '000000',
102
- 'clientId': CLIENT_ID,
103
- 'code': code,
104
- 'uuid': uuid,
105
- }, ensure_ascii=False).encode('utf-8')
106
-
107
- padder = PKCS7(128).padder()
108
- padded = padder.update(login_body) + padder.finalize()
109
- cipher = Cipher(algorithms.AES(aes_key), modes.ECB())
110
- encryptor = cipher.encryptor()
111
- encrypted_body = base64.b64encode(encryptor.update(padded) + encryptor.finalize()).decode()
112
-
113
- # 3. RSA 加密 AES 密钥 (前端: encrypt(encryptBase64(aesKey)))
114
- aes_key_b64 = base64.b64encode(aes_key).decode()
115
- pub_key = load_der_public_key(base64.b64decode(RSA_PUBLIC_KEY_B64))
116
- encrypted_key_bytes = pub_key.encrypt(aes_key_b64.encode('utf-8'), asym_padding.PKCS1v15())
117
- encrypted_key = base64.b64encode(encrypted_key_bytes).decode()
118
-
119
- return encrypted_body, encrypted_key
120
-
121
-
122
- def login():
123
- """全自动登录流程"""
124
- log('=== 全自动登录 ===')
125
-
126
- # 1. 获取验证码
127
- uuid, img_b64, captcha_enabled = get_captcha()
128
- log('验证码: enabled=%s, uuid=%s' % (captcha_enabled, uuid[:8] if uuid else 'N/A'))
129
-
130
- if not captcha_enabled:
131
- log('验证码未开启, 直接登录')
132
- code = ''
133
- uuid = ''
134
- else:
135
- # 2. OCR 识别 + 数学题解析
136
- code = recognize_captcha(img_b64)
137
- log('OCR识别验证码: %s' % code)
138
- if code is None:
139
- # ddddocr 没读对(数学题识别不稳定), 本次放弃, 让外层重试
140
- log('验证码识别失败(数学题未解析), 重试')
141
- return None
142
-
143
- # 3. 加密 + 登录
144
- try:
145
- encrypted_body, encrypted_key = encrypt_login(USERNAME, PASSWORD, code, uuid)
146
- log('加密完成, 发送登录请求...')
147
-
148
- resp = requests.post(
149
- BASE_URL + API_PREFIX + '/auth/login',
150
- data=encrypted_body,
151
- headers={
152
- 'Content-Type': 'application/json',
153
- 'encrypt-key': encrypted_key,
154
- },
155
- timeout=10,
156
- )
157
- result = resp.json()
158
- log('登录响应: code=%s, msg=%s' % (result.get('code'), result.get('msg')))
159
-
160
- if result.get('code') == 200 and result.get('data', {}).get('access_token'):
161
- token = result['data']['access_token']
162
- log('✓ 登录成功! token=%s...' % token[:20])
163
-
164
- # 保存登录态
165
- os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
166
- with open(STATE_FILE, 'w', encoding='utf-8') as f:
167
- json.dump({
168
- 'access_token': token,
169
- 'base_url': BASE_URL,
170
- 'api_prefix': API_PREFIX,
171
- 'saved_at': __import__('datetime').datetime.now().isoformat(),
172
- }, f, indent=2)
173
- log('登录态已保存: %s' % STATE_FILE)
174
- return token
175
- else:
176
- log('✗ 登录失败: %s' % json.dumps(result, ensure_ascii=False)[:200])
177
- log(' 可能验证码识别错误, 重试...')
178
- return None
179
- except Exception as e:
180
- log('登录异常: %s' % str(e))
181
- return None
182
-
183
-
184
- if __name__ == '__main__':
185
- # 数学题验证码识别不稳定, 多重试几次 (实测 ~30% 一次读对, 20 次基本必中)
186
- import time as _time
187
- for attempt in range(20):
188
- log('--- 尝试 %d ---' % (attempt + 1))
189
- token = login()
190
- if token:
191
- break
192
- _time.sleep(0.3)
193
- else:
194
- log('20次尝试均失败, 请检查账号/密码/验证码/网络')
195
-
196
- open(os.path.join(SCRIPTS_DIR, '_login_log.txt'), 'w', encoding='utf-8').write('\n'.join(out))
1
+ # -*- coding: utf-8 -*-
2
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
3
+ import os as _o, sys as _s
4
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
5
+ if _cp not in _s.path: _s.path.insert(0, _cp)
6
+ from bootstrap import setup; setup()
7
+
8
+ """
9
+ 全自动登录: 获取验证码 → ddddocr识别 → 页面JS加密提交 → 保存token
10
+ 登录态保存到 auth-state.json, 后续测试复用(无需再次识别验证码)。
11
+ """
12
+ import base64
13
+ import json
14
+ import os
15
+ import sys
16
+ import requests
17
+ import ddddocr
18
+
19
+ BASE_URL = 'https://icsqas.inforecloud.com'
20
+ API_PREFIX = '/test-api'
21
+ USERNAME = '15888888887'
22
+ PASSWORD = 'admin123'
23
+ CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
24
+
25
+ # RSA公钥 (从 .env.production)
26
+ RSA_PUBLIC_KEY_B64 = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDN/EkuOg1gq/t2/PUI8GpcJCsCFQgHkSHUEdk7ZPrEQYd8IM1hFkHmDJrCOQ+qUPqKHG9a5BAI44yE6FuPI9Vvqu2ZrCww/zryR7A+2n5jaKIG2rJG9KHDzT5mAVOJrlhBf8L2i7wZc8eWrNg0md0BlwEowQyVEEj9oIBlw/nqTwIDAQAB'
27
+
28
+ # 保存路径
29
+ SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts/根
30
+
31
+
32
+ def _read_developer():
33
+ """读 .qoder/.developer 拿当前开发者名, 用于身份隔离。
34
+ 多人共用 workspace 时, auth-state 必须按开发者分目录, 否则会串登录态。
35
+ 没有身份 → 抛异常 (绝不 fallback 到 hupan/default, 防串号)。"""
36
+ dev_file = os.path.join(SCRIPTS_DIR, '..', '..', '.qoder', '.developer')
37
+ if os.path.isfile(dev_file):
38
+ for ln in open(dev_file, encoding='utf-8'):
39
+ if ln.strip().startswith('name='):
40
+ return ln.strip().split('=', 1)[1].strip()
41
+ raise RuntimeError(
42
+ '未检测到开发者身份(.qoder/.developer)。请先运行 /wl-init 注册身份。'
43
+ ' (auth-state 按开发者分目录, 没身份无法隔离 → 拒绝执行防串号)'
44
+ )
45
+
46
+
47
+ STATE_FILE = os.path.join(SCRIPTS_DIR, '..', '..', 'workspace', 'members',
48
+ _read_developer(), 'auth-state.json')
49
+
50
+ out = []
51
+ def log(s=''):
52
+ out.append(s)
53
+ print(s)
54
+
55
+
56
+ def get_captcha():
57
+ """获取验证码图片 + uuid"""
58
+ resp = requests.get(BASE_URL + API_PREFIX + '/auth/code', timeout=10)
59
+ data = resp.json().get('data', {})
60
+ return data.get('uuid'), data.get('img'), data.get('captchaEnabled', False)
61
+
62
+
63
+ def recognize_captcha(img_b64):
64
+ """用 ddddocr 识别验证码图片, 并解析数学题(ICS a op b = ? 形式).
65
+ 返回算出的答案字符串; 解析失败返回 None."""
66
+ import re as _re
67
+ ocr = ddddocr.DdddOcr(show_ad=False)
68
+ img_bytes = base64.b64decode(img_b64)
69
+ raw = ocr.classification(img_bytes)
70
+ # 规范化运算符 (x/X/× *, ÷ → /), 去掉 = ? 尾部
71
+ expr = raw.replace('x', '*').replace('X', '*').replace('×', '*').replace('÷', '/')
72
+ expr = expr.rstrip('=??').strip()
73
+ m = _re.match(r'^\s*(\d+)\s*([+\-*/])\s*(\d+)\s*$', expr)
74
+ if not m:
75
+ return None
76
+ a, op, b = int(m.group(1)), m.group(2), int(m.group(3))
77
+ if op == '+':
78
+ ans = a + b
79
+ elif op == '-':
80
+ ans = a - b
81
+ elif op == '*':
82
+ ans = a * b
83
+ elif op == '/':
84
+ ans = a // b if b else None
85
+ else:
86
+ return None
87
+ return str(ans) if ans is not None else None
88
+
89
+
90
+ def encrypt_login(username, password, code, uuid):
91
+ """用 RSA+AES 加密登录请求 (复现前端加密逻辑)。
92
+ 使用 cryptography 库 (替代 pycryptodome)。"""
93
+ import os as _os
94
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
95
+ from cryptography.hazmat.primitives.padding import PKCS7
96
+ from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
97
+ from cryptography.hazmat.primitives.serialization import load_der_public_key
98
+
99
+ # 1. 生成随机 AES 密钥 (前端用 randomUUID = 32 hex chars)
100
+ aes_key = _os.urandom(16).hex().encode('utf-8') # 32 bytes
101
+
102
+ # 2. AES-ECB-PKCS7 加密请求体
103
+ login_body = json.dumps({
104
+ 'username': username,
105
+ 'password': password,
106
+ 'grantType': 'password',
107
+ 'tenantId': '000000',
108
+ 'clientId': CLIENT_ID,
109
+ 'code': code,
110
+ 'uuid': uuid,
111
+ }, ensure_ascii=False).encode('utf-8')
112
+
113
+ padder = PKCS7(128).padder()
114
+ padded = padder.update(login_body) + padder.finalize()
115
+ cipher = Cipher(algorithms.AES(aes_key), modes.ECB())
116
+ encryptor = cipher.encryptor()
117
+ encrypted_body = base64.b64encode(encryptor.update(padded) + encryptor.finalize()).decode()
118
+
119
+ # 3. RSA 加密 AES 密钥 (前端: encrypt(encryptBase64(aesKey)))
120
+ aes_key_b64 = base64.b64encode(aes_key).decode()
121
+ pub_key = load_der_public_key(base64.b64decode(RSA_PUBLIC_KEY_B64))
122
+ encrypted_key_bytes = pub_key.encrypt(aes_key_b64.encode('utf-8'), asym_padding.PKCS1v15())
123
+ encrypted_key = base64.b64encode(encrypted_key_bytes).decode()
124
+
125
+ return encrypted_body, encrypted_key
126
+
127
+
128
+ def login():
129
+ """全自动登录流程"""
130
+ log('=== 全自动登录 ===')
131
+
132
+ # 1. 获取验证码
133
+ uuid, img_b64, captcha_enabled = get_captcha()
134
+ log('验证码: enabled=%s, uuid=%s' % (captcha_enabled, uuid[:8] if uuid else 'N/A'))
135
+
136
+ if not captcha_enabled:
137
+ log('验证码未开启, 直接登录')
138
+ code = ''
139
+ uuid = ''
140
+ else:
141
+ # 2. OCR 识别 + 数学题解析
142
+ code = recognize_captcha(img_b64)
143
+ log('OCR识别验证码: %s' % code)
144
+ if code is None:
145
+ # ddddocr 没读对(数学题识别不稳定), 本次放弃, 让外层重试
146
+ log('验证码识别失败(数学题未解析), 重试')
147
+ return None
148
+
149
+ # 3. 加密 + 登录
150
+ try:
151
+ encrypted_body, encrypted_key = encrypt_login(USERNAME, PASSWORD, code, uuid)
152
+ log('加密完成, 发送登录请求...')
153
+
154
+ resp = requests.post(
155
+ BASE_URL + API_PREFIX + '/auth/login',
156
+ data=encrypted_body,
157
+ headers={
158
+ 'Content-Type': 'application/json',
159
+ 'encrypt-key': encrypted_key,
160
+ },
161
+ timeout=10,
162
+ )
163
+ result = resp.json()
164
+ log('登录响应: code=%s, msg=%s' % (result.get('code'), result.get('msg')))
165
+
166
+ if result.get('code') == 200 and result.get('data', {}).get('access_token'):
167
+ token = result['data']['access_token']
168
+ log('✓ 登录成功! token=%s...' % token[:20])
169
+
170
+ # 保存登录态
171
+ os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
172
+ with open(STATE_FILE, 'w', encoding='utf-8') as f:
173
+ json.dump({
174
+ 'access_token': token,
175
+ 'base_url': BASE_URL,
176
+ 'api_prefix': API_PREFIX,
177
+ 'saved_at': __import__('datetime').datetime.now().isoformat(),
178
+ }, f, indent=2)
179
+ log('登录态已保存: %s' % STATE_FILE)
180
+ return token
181
+ else:
182
+ log('✗ 登录失败: %s' % json.dumps(result, ensure_ascii=False)[:200])
183
+ log(' 可能验证码识别错误, 重试...')
184
+ return None
185
+ except Exception as e:
186
+ log('登录异常: %s' % str(e))
187
+ return None
188
+
189
+
190
+ if __name__ == '__main__':
191
+ # 数学题验证码识别不稳定, 多重试几次 (实测 ~30% 一次读对, 20 次基本必中)
192
+ import time as _time
193
+ for attempt in range(20):
194
+ log('--- 尝试 %d ---' % (attempt + 1))
195
+ token = login()
196
+ if token:
197
+ break
198
+ _time.sleep(0.3)
199
+ else:
200
+ log('20次尝试均失败, 请检查账号/密码/验证码/网络')
201
+
202
+ open(os.path.join(SCRIPTS_DIR, '_login_log.txt'), 'w', encoding='utf-8').write('\n'.join(out))
@@ -1,4 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
3
+ import os as _o, sys as _s
4
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
5
+ if _cp not in _s.path: _s.path.insert(0, _cp)
6
+ from bootstrap import setup; setup()
7
+
2
8
  """
3
9
  Automated test runner using Playwright (headless) + knowledge graph.
4
10
 
@@ -23,7 +29,7 @@ import sys
23
29
  import time
24
30
  import yaml
25
31
 
26
- SCRIPTS = os.path.dirname(os.path.abspath(__file__))
32
+ SCRIPTS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts/根
27
33
  sys.path.insert(0, SCRIPTS)
28
34
  from common.paths import DATA_INDEX_DIR, PROJECT_ROOT
29
35