kcode-pi 0.1.6 → 0.1.7

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 (47) hide show
  1. package/README.md +18 -2
  2. package/package.json +1 -1
  3. package/src/official/kingdee-skills.ts +60 -13
  4. package/src/rules/checker.ts +143 -0
  5. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/SKILL.md +2 -2
  6. package/vendor/kingdee-skills/ok-cosmic/SKILL.md +52 -101
  7. package/vendor/kingdee-skills/ok-cosmic/agents/openai.yaml +4 -4
  8. package/vendor/kingdee-skills/ok-cosmic/manifest.json +21 -20
  9. package/vendor/kingdee-skills/ok-cosmic/ok-cosmic-intro.html +1 -1
  10. package/vendor/kingdee-skills/ok-cosmic/rules/a-layer-rules.json +1 -1
  11. package/vendor/kingdee-skills/ok-cosmic/rules/anti-patterns.md +2 -2
  12. package/vendor/kingdee-skills/ok-cosmic/rules/coding-preferences.md +4 -4
  13. package/vendor/kingdee-skills/ok-cosmic/rules/constraints.md +3 -3
  14. package/vendor/kingdee-skills/ok-cosmic/rules/decision-matrix.md +8 -8
  15. package/vendor/kingdee-skills/ok-cosmic/rules/intent-routing.md +1 -1
  16. package/vendor/kingdee-skills/ok-cosmic/rules/post-check.md +19 -18
  17. package/vendor/kingdee-skills/ok-ksql/SKILL.md +9 -9
  18. package/vendor/kingdee-skills/ok-ksql/manifest.json +2 -1
  19. package/vendor/kingdee-skills/ok-ksql/references/ksql-datafix.md +2 -2
  20. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/pattern-matcher.py +0 -336
  21. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/review-score-calculator.py +0 -121
  22. package/vendor/kingdee-skills/ok-cosmic/CHANGELOG.md +0 -295
  23. package/vendor/kingdee-skills/ok-cosmic/README.md +0 -460
  24. package/vendor/kingdee-skills/ok-cosmic/requirements.txt +0 -2
  25. package/vendor/kingdee-skills/ok-cosmic/scripts/config_loader.py +0 -204
  26. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-api-knowledge.py +0 -910
  27. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-basedata-query.py +0 -359
  28. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-config-check.py +0 -181
  29. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-extpoints-query.py +0 -389
  30. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-form-metadata.py +0 -856
  31. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-check.py +0 -262
  32. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-lint.py +0 -293
  33. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/__init__.py +0 -2
  34. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/base.py +0 -393
  35. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/resource_check.py +0 -176
  36. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/scene_check.py +0 -375
  37. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/style_check.py +0 -434
  38. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/verify_check.py +0 -36
  39. package/vendor/kingdee-skills/ok-cosmic/scripts/route_client.py +0 -186
  40. package/vendor/kingdee-skills/ok-cosmic/scripts/script_utils.py +0 -40
  41. package/vendor/kingdee-skills/ok-cosmic/scripts/sqlite_cache.py +0 -142
  42. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-commons.jar +0 -0
  43. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-features.jar +0 -0
  44. package/vendor/kingdee-skills/ok-cosmic/setup/setup-mac.sh +0 -18
  45. package/vendor/kingdee-skills/ok-cosmic/setup/setup-windows.bat +0 -53
  46. package/vendor/kingdee-skills/ok-cosmic/setup/setup.jar +0 -0
  47. package/vendor/kingdee-skills/ok-ksql/scripts/ksql_lint.py +0 -363
@@ -1,434 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """编码偏好检查 (STYLE-*) — 来源: coding-preferences.md"""
3
-
4
- import re
5
- from typing import List
6
-
7
- from .base import (
8
- LintIssue,
9
- Severity,
10
- analyze_java_context,
11
- analyze_plugin_context,
12
- classify_lines,
13
- code_for_structure,
14
- detect_plugin_type,
15
- looks_like_loop_header,
16
- strip_line_comment,
17
- )
18
-
19
-
20
- # 编码偏好规则列表
21
- STYLE_RULES = [
22
- {
23
- "pattern": r"\bStringUtils\s*\.\s*(isBlank|isNotBlank|isEmpty|isNotEmpty|equals)\b",
24
- "rule_id": "STYLE-001",
25
- "severity": Severity.WARNING,
26
- "message": "字符串判空应使用 CharSequenceUtils 而非 StringUtils",
27
- "fix_hint": "使用 CharSequenceUtils.isBlank() / isNotBlank()",
28
- },
29
- {
30
- "pattern": r"(?:!=\s*null\s*&&\s*!\w+\.isEmpty\(\))|(?:==\s*null\s*\|\|\s*\w+\.isEmpty\(\))",
31
- "rule_id": "STYLE-002",
32
- "severity": Severity.WARNING,
33
- "message": "手写 null/isEmpty 判空应使用工具类封装",
34
- "fix_hint": "字符串用 CharSequenceUtils.isBlank() / isNotBlank();集合用 CollectionUtils.isEmpty() / isNotEmpty()",
35
- },
36
- {
37
- "pattern": r"\bOperationServiceHelper\s*\.\s*(save|submit|audit)\b",
38
- "rule_id": "STYLE-003",
39
- "severity": Severity.WARNING,
40
- "message": "不应散落调用 OperationServiceHelper,缺少错误聚合",
41
- "fix_hint": "使用 OpUtils.executeOperateOrThrow() 或 OperateChain",
42
- },
43
- {
44
- "pattern": r"new\s+PushArgs\s*\(",
45
- "rule_id": "STYLE-004",
46
- "severity": Severity.WARNING,
47
- "message": "不应手拼 PushArgs,有封装可用",
48
- "fix_hint": "使用 BotpUtils 封装下推逻辑",
49
- },
50
- {
51
- "pattern": r"new\s+DrawArgs\s*\(",
52
- "rule_id": "STYLE-005",
53
- "severity": Severity.WARNING,
54
- "message": "不应手拼 DrawArgs,有封装可用",
55
- "fix_hint": "使用 BotpUtils 封装选单逻辑",
56
- },
57
- {
58
- "pattern": r"\.\s*get\(\s*\"[\w]+\.[\w.]+\"",
59
- "rule_id": "STYLE-006",
60
- "severity": Severity.WARNING,
61
- "message": "不应直接深链式 .get(\"a.b.c\"),中间节点为空时会抛异常",
62
- "fix_hint": "使用 DynamicObjectUtils.nullSafeGet(dynamicObject, \"field\")",
63
- },
64
- {
65
- "pattern": r"\bAttachmentServiceHelper\s*\.",
66
- "rule_id": "STYLE-007",
67
- "severity": Severity.WARNING,
68
- "message": "附件处理应优先使用 AttachmentUtils 封装",
69
- "fix_hint": "使用 AttachmentUtils 和 uploader",
70
- },
71
- {
72
- "pattern": r"\bQueryServiceHelper\s*\.\s*queryOne\s*\([^)]*\)\s*(?:!=|==)\s*null",
73
- "rule_id": "STYLE-008",
74
- "severity": Severity.WARNING,
75
- "message": "判断数据是否存在时,优先使用 QueryServiceHelper.exists(...)",
76
- "fix_hint": "将 queryOne(...) != null / == null 改为 QueryServiceHelper.exists(...)",
77
- },
78
- {
79
- "pattern": r"\bprintStackTrace\s*\(",
80
- "rule_id": "STYLE-009",
81
- "severity": Severity.ERROR,
82
- "message": "不要直接调用 printStackTrace(),应使用统一日志框架",
83
- "fix_hint": "改为 logger.error(\"错误分析描述\", e) 或插件内的 log.error(...)",
84
- },
85
- {
86
- "pattern": r"\bthrow\s+new\s+(?:(?:java\.lang\.)?RuntimeException|(?:java\.lang\.)?IllegalArgumentException|(?:java\.lang\.)?IllegalStateException)\b",
87
- "rule_id": "STYLE-018",
88
- "severity": Severity.ERROR,
89
- "message": "业务异常应统一使用 KDBizException,不要直接抛 RuntimeException/IllegalArgumentException/IllegalStateException",
90
- "fix_hint": "改为 throw new KDBizException(new ErrorCode(...));若为包装异常,保留原始 cause",
91
- },
92
- {
93
- "pattern": r"\bnew\s+Thread\s*\(",
94
- "rule_id": "STYLE-019",
95
- "severity": Severity.WARNING,
96
- "message": "禁止直接 new Thread(),绕开了平台线程池的统一监控与资源回收",
97
- "fix_hint": "使用 kd.bos.threads.ThreadPools.newXxx() 或 ThreadPools.executeOnceXxx()",
98
- },
99
- {
100
- "pattern": r"\bExecutors\s*\.\s*(newFixedThreadPool|newCachedThreadPool|newSingleThreadExecutor|newScheduledThreadPool)\b",
101
- "rule_id": "STYLE-020",
102
- "severity": Severity.WARNING,
103
- "message": "禁止直接使用 JDK Executors 创建线程池,无法遵守平台线程治理约束",
104
- "fix_hint": "使用 kd.bos.threads.ThreadPools.newXxx() 或 ThreadPools.executeOnceXxx()",
105
- },
106
- {
107
- "pattern": r"\bSerializationUtils\s*\.\s*toJsonString\s*\([^)]*\b(args|e|event|evt|dataEntity|dataEntities|view)\b",
108
- "rule_id": "STYLE-021",
109
- "severity": Severity.WARNING,
110
- "message": "不要对页面对象/事件对象/数据对象整包 JSON 序列化打印,成本高且可能打印大对象",
111
- "fix_hint": "只按需提取关键字段打印,如 log.info(\"billNo={}\", data.getString(\"billno\"))",
112
- },
113
- {
114
- "pattern": r"\bBusinessDataServiceHelper\s*\.\s*load\s*\([^,]+,\s*(?:EntityUtils\.getMainEntityType|BusinessDataServiceHelper\.newDynamicObject|EntityMetadataCache\.getDataEntityType)",
115
- "rule_id": "STYLE-025",
116
- "severity": Severity.WARNING,
117
- "message": "加载实体时应指定必要字段,避免全量加载",
118
- "fix_hint": "使用 BusinessDataServiceHelper.load(entityName, selectFields, filters) 指定查询字段",
119
- },
120
- ]
121
-
122
- # 主键/id 判空检测(STYLE-026)
123
- # 检测模式:xxxId/xxxPk 变量与 null/0/0L 比较,或 .get("id"/"xxx_id") 与 null 比较
124
- PK_NULL_CHECK_PATTERNS = [
125
- # xxxId == null / xxxId != null / xxxPk == null / xxxPk != null
126
- re.compile(r'\b\w*(?:[Ii]d|[Pp]k)\s*(?:==|!=)\s*null\b'),
127
- # xxxId == 0L / xxxId == 0 / xxxId <= 0L / xxxId <= 0 / xxxId < 1
128
- re.compile(r'\b\w*(?:[Ii]d|[Pp]k)\s*(?:==|<=|<)\s*(?:0L?|1)\b'),
129
- # .get("id") == null / .get("id") != null
130
- re.compile(r'\.\s*get\s*\(\s*"\w*[Ii]d"\s*\)\s*(?:==|!=)\s*null'),
131
- ]
132
-
133
- # BigDecimal 原生运算检测(STYLE-027)
134
- # 检测加减乘除 + compareTo 比较
135
- BIGDECIMAL_RAW_PATTERNS = [
136
- # 链式 .add/.subtract/.multiply/.divide + BigDecimal/new 上下文
137
- re.compile(r'\.\s*(?:add|subtract|multiply|divide)\s*\(\s*(?:new\s+BigDecimal|BigDecimal\.)'),
138
- # .divide(..., RoundingMode) —— 有 RoundingMode 参数必定是 BigDecimal 除法
139
- re.compile(r'\.\s*divide\s*\([^)]*RoundingMode'),
140
- # .compareTo(BigDecimal → BigDecimalUtils.equals/largeThan
141
- re.compile(r'\.\s*compareTo\s*\(\s*BigDecimal'),
142
- ]
143
-
144
- # QFilter 字符串运算符检测(P0:编译通过但运行时崩溃)
145
- QFILTER_STRING_OP_PATTERN = re.compile(
146
- r'new\s+QFilter\s*\([^,]+,\s*"[^"]*"\s*,'
147
- )
148
-
149
- SQL_CONCAT_PATTERN = re.compile(
150
- r'("[^"]*\b(select|insert|update|delete|from|where)\b[^"]*"\s*\+)'
151
- r'|(\+\s*"[^"]*\b(from|where|and|or|order\s+by|group\s+by)\b[^"]*")',
152
- re.IGNORECASE,
153
- )
154
- SQL_DIALECT_PATTERN = re.compile(
155
- r'"[^"]*\b(select|insert|update|delete)\b[^"]*\b(limit|rownum|nvl|isnull|ifnull|top\s+\d+)\b[^"]*"',
156
- re.IGNORECASE,
157
- )
158
- CHINESE_CONCAT_PATTERN = re.compile(
159
- r'("[^"]*[\u4e00-\u9fff][^"]*"\s*\+)|(\+\s*"[^"]*[\u4e00-\u9fff][^"]*")'
160
- )
161
- LOOP_UPDATE_VIEW_PATTERN = re.compile(r"\bupdateView\s*\(")
162
- LOOP_DB_PATTERN = re.compile(
163
- r"\b(BusinessDataServiceHelper|QueryServiceHelper|SaveServiceHelper)\s*\.\s*"
164
- r"(load|loadSingle|loadSingleFromCache|loadFromCache|query|queryOne|queryDataSet|exists?|save|update)\b"
165
- )
166
- LOOP_DB_HELPER_PATTERN = re.compile(r"\b(BusinessDataServiceHelper|QueryServiceHelper|SaveServiceHelper)\b")
167
- LOOP_DB_METHOD_PATTERN = re.compile(
168
- r"\.\s*(load|loadSingle|loadSingleFromCache|loadFromCache|query|queryOne|queryDataSet|exists?|save|update)\b"
169
- )
170
- LOOP_REDIS_PATTERN = re.compile(r"\b(RedisTemplate|StringRedisTemplate|Jedis|Redisson|redisTemplate|redisClient)\b")
171
- LOOP_ORM_CREATE_PATTERN = re.compile(r"\bORM\s*\.\s*create\s*\(")
172
- LOOP_DISPATCH_PATTERN = re.compile(r"\bDispatchServiceHelper\s*\.\s*invoke\w*\s*\(")
173
- QUERY_RESULT_DECL_PATTERN = re.compile(
174
- r"\b(?:DynamicObjectCollection|var)\s+([A-Za-z_]\w*)\s*=\s*QueryServiceHelper\s*\.\s*query\b"
175
- )
176
- QUERY_RESULT_HELPER_DECL_PATTERN = re.compile(
177
- r"\b(?:DynamicObjectCollection|var)\s+([A-Za-z_]\w*)\s*=\s*QueryServiceHelper\b"
178
- )
179
- QUERY_RESULT_METHOD_PATTERN = re.compile(r"\.\s*query\b")
180
- QUERY_RESULT_GET_ALIAS_PATTERN = re.compile(
181
- r"\b(?:DynamicObject|var)\s+([A-Za-z_]\w*)\s*=\s*([A-Za-z_]\w*)\s*\.\s*get\s*\("
182
- )
183
- ENHANCED_FOR_PATTERN = re.compile(
184
- r"\bfor\s*\(\s*(?:final\s+)?(?:DynamicObject|var)\s+([A-Za-z_]\w*)\s*:\s*([A-Za-z_]\w*)\s*\)"
185
- )
186
- SAVE_SERVICE_PATTERN = re.compile(r"\bSaveServiceHelper\s*\.\s*(save|update)\s*\(")
187
-
188
-
189
- def check(filepath: str, lines: List[str]) -> List[LintIssue]:
190
- """执行编码偏好检查,返回问题列表。"""
191
- issues: List[LintIssue] = []
192
- _, loop_context = analyze_java_context(lines)
193
- plugin_context = analyze_plugin_context(lines)
194
- skip_lines = classify_lines(lines)
195
- query_result_vars: dict[str, int] = {}
196
- query_result_entity_vars: dict[str, int] = {}
197
- active_query_loop_vars: dict[str, int] = {}
198
- single_line_query_loop_vars: dict[str, int] = {}
199
- brace_depth = 0
200
-
201
- for i, line in enumerate(lines):
202
- lineno = i + 1
203
- code_line = code_for_structure(line)
204
- raw_code_line = strip_line_comment(line)
205
- prev_code_line = code_for_structure(lines[i - 1]) if i > 0 else ""
206
- current_depth = brace_depth
207
- active_query_loop_vars = {
208
- var_name: depth
209
- for var_name, depth in active_query_loop_vars.items()
210
- if current_depth >= depth
211
- }
212
- single_line_query_loop_vars = {
213
- var_name: target_line
214
- for var_name, target_line in single_line_query_loop_vars.items()
215
- if lineno <= target_line
216
- }
217
-
218
- decl_match = QUERY_RESULT_DECL_PATTERN.search(code_line)
219
- if decl_match:
220
- query_result_vars.setdefault(decl_match.group(1), lineno)
221
- elif QUERY_RESULT_METHOD_PATTERN.search(code_line):
222
- helper_decl_match = QUERY_RESULT_HELPER_DECL_PATTERN.search(prev_code_line)
223
- if helper_decl_match:
224
- query_result_vars.setdefault(helper_decl_match.group(1), lineno - 1)
225
-
226
- alias_match = QUERY_RESULT_GET_ALIAS_PATTERN.search(code_line)
227
- if alias_match and alias_match.group(2) in query_result_vars:
228
- query_result_entity_vars.setdefault(alias_match.group(1), query_result_vars[alias_match.group(2)])
229
-
230
- for_match = ENHANCED_FOR_PATTERN.search(code_line)
231
- if for_match and for_match.group(2) in query_result_vars:
232
- loop_var = for_match.group(1)
233
- if re.search(rf"\b{re.escape(loop_var)}\s*\.\s*set\s*\(", code_line):
234
- query_result_entity_vars.setdefault(loop_var, query_result_vars[for_match.group(2)])
235
- elif "{" in code_line:
236
- active_query_loop_vars[loop_var] = current_depth + 1
237
- else:
238
- single_line_query_loop_vars[loop_var] = lineno + 1
239
-
240
- if skip_lines[i]:
241
- brace_depth += code_line.count("{") - code_line.count("}")
242
- continue
243
-
244
- loop_line = loop_context[i] or looks_like_loop_header(line)
245
- for rule in STYLE_RULES:
246
- exclude = rule.get("exclude_pattern")
247
- match_source = raw_code_line if rule["rule_id"] == "STYLE-006" else code_line
248
- if exclude and re.search(exclude, match_source):
249
- continue
250
- if re.search(rule["pattern"], match_source):
251
- issues.append(LintIssue(
252
- file=filepath, line=lineno,
253
- severity=rule["severity"],
254
- rule_id=rule["rule_id"],
255
- message=rule["message"],
256
- fix_hint=rule["fix_hint"],
257
- source_line=line.strip(),
258
- ))
259
-
260
- if plugin_context[i] == "op" and re.search(r"\ballFields\s*\(", code_line):
261
- issues.append(LintIssue(
262
- file=filepath, line=lineno,
263
- severity=Severity.WARNING,
264
- rule_id="STYLE-010",
265
- message="操作插件里除非字段很多,否则不要直接使用 allFields()",
266
- fix_hint="按实际业务显式准备字段,优先 e.getFieldKeys().add(...) 或 entryFields(...)",
267
- source_line=line.strip(),
268
- ))
269
-
270
- if SQL_CONCAT_PATTERN.search(line):
271
- # 降低误报:移除所有字符串字面量后,检查 + 附近是否有变量/标识符
272
- # 纯字面量拼接(如 "Please select" + " an option")不构成 SQL 注入风险
273
- line_without_strings = re.sub(r'"[^"]*"', '', line)
274
- has_var_concat = bool(re.search(r'\w\s*\+|\+\s*\w', line_without_strings))
275
- if has_var_concat:
276
- issues.append(LintIssue(
277
- file=filepath, line=lineno,
278
- severity=Severity.ERROR,
279
- rule_id="STYLE-011",
280
- message="SQL/KSQL 传参不要通过字符串拼接构造",
281
- fix_hint="改为参数化查询或平台查询构造,避免手拼 SQL 条件",
282
- source_line=line.strip(),
283
- ))
284
-
285
- if SQL_DIALECT_PATTERN.search(line):
286
- issues.append(LintIssue(
287
- file=filepath, line=lineno,
288
- severity=Severity.ERROR,
289
- rule_id="STYLE-012",
290
- message="检测到可能的数据库方言 SQL 关键字,跨库兼容性差",
291
- fix_hint="改为 KSQL 或平台查询接口,避免 limit/rownum/nvl/isnull/top 等方言写法",
292
- source_line=line.strip(),
293
- ))
294
-
295
- # STYLE-024: QFilter 使用字符串运算符代替 QCP 枚举(编译通过但运行时崩溃)
296
- if QFILTER_STRING_OP_PATTERN.search(raw_code_line):
297
- issues.append(LintIssue(
298
- file=filepath, line=lineno,
299
- severity=Severity.ERROR,
300
- rule_id="STYLE-024",
301
- message="QFilter 第二个参数必须使用 QCP 枚举,不能用字符串(编译通过但运行时崩溃)",
302
- fix_hint='\u5c06 new QFilter(field, "=", value) \u6539\u4e3a new QFilter(field, QCP.equals, value)\uff1b\u5e38\u7528\u679a\u4e3e: QCP.equals / not_equals / large_equals / less_equals / like / in',
303
- source_line=line.strip(),
304
- ))
305
-
306
- # STYLE-027: BigDecimal 原生运算,应优先使用 BigDecimalUtils
307
- if 'BigDecimalUtils' not in code_line:
308
- for bd_pat in BIGDECIMAL_RAW_PATTERNS:
309
- if bd_pat.search(code_line):
310
- issues.append(LintIssue(
311
- file=filepath, line=lineno,
312
- severity=Severity.WARNING,
313
- rule_id="STYLE-027",
314
- message="BigDecimal 原生运算应优先使用 BigDecimalUtils 工具方法",
315
- fix_hint="使用 BigDecimalUtils.valueOf() / add() / subtract() / multiply() / divide() / largeThanZero() / nullToZero() 等",
316
- source_line=line.strip(),
317
- ))
318
- break
319
-
320
- # STYLE-026: 主键/id 用 == null / != null / == 0L 等方式判空
321
- if not any(kw in code_line for kw in ('isEmptyPk', 'isNotEmptyPk')):
322
- # 前两个 pattern 检测变量名,用 code_line;第三个 pattern 检测 .get("id"),用 raw_code_line(保留字符串字面量)
323
- pk_sources = [code_line, code_line, raw_code_line]
324
- for pk_pat, src in zip(PK_NULL_CHECK_PATTERNS, pk_sources):
325
- if pk_pat.search(src):
326
- issues.append(LintIssue(
327
- file=filepath, line=lineno,
328
- severity=Severity.WARNING,
329
- rule_id="STYLE-026",
330
- message="主键/id 判空不应直接用 == null / != null / == 0L,苍穹主键默认值为 0L",
331
- fix_hint="使用 EntityUtils.isEmptyPk(pk) 或 EntityUtils.isNotEmptyPk(pk),兼容 null 和 0L",
332
- source_line=line.strip(),
333
- ))
334
- break
335
-
336
- if "ResManager.loadKDString" not in line and "String.format" not in line and CHINESE_CONCAT_PATTERN.search(line):
337
- issues.append(LintIssue(
338
- file=filepath, line=lineno,
339
- severity=Severity.INFO,
340
- rule_id="STYLE-013",
341
- message="中文提示语存在拼接,后续多语言翻译时语序容易失真",
342
- fix_hint="使用完整句模板 + String.format(ResManager.loadKDString(...), ...)",
343
- source_line=line.strip(),
344
- ))
345
-
346
- if loop_line and LOOP_UPDATE_VIEW_PATTERN.search(code_line):
347
- issues.append(LintIssue(
348
- file=filepath, line=lineno,
349
- severity=Severity.ERROR,
350
- rule_id="STYLE-014",
351
- message="禁止在循环中调用 updateView(),界面刷新成本高",
352
- fix_hint="循环结束后统一执行局部刷新",
353
- source_line=line.strip(),
354
- ))
355
-
356
- loop_db_hit = LOOP_DB_PATTERN.search(code_line) or (
357
- LOOP_DB_METHOD_PATTERN.search(code_line) and LOOP_DB_HELPER_PATTERN.search(prev_code_line)
358
- )
359
- if loop_line and loop_db_hit:
360
- issues.append(LintIssue(
361
- file=filepath, line=lineno,
362
- severity=Severity.ERROR,
363
- rule_id="STYLE-015",
364
- message="在循环中访问数据库(包括 BusinessDataServiceHelper.loadSingle(...) 等),存在明显的 N+1 查询风险",
365
- fix_hint="先看 skills/ok-cosmic/assets/snippets/query/BatchQuerySample.java,按“分组 key -> 批量查询 -> 本地映射”改写,避免循环里逐条查库",
366
- source_line=line.strip(),
367
- ))
368
-
369
- if loop_line and LOOP_REDIS_PATTERN.search(code_line):
370
- issues.append(LintIssue(
371
- file=filepath, line=lineno,
372
- severity=Severity.ERROR,
373
- rule_id="STYLE-016",
374
- message="禁止在循环中频繁访问 Redis,容易造成缓存压力",
375
- fix_hint="减少访问次数,优先批量读取或增加本地缓存",
376
- source_line=line.strip(),
377
- ))
378
-
379
- if loop_line and LOOP_ORM_CREATE_PATTERN.search(code_line):
380
- issues.append(LintIssue(
381
- file=filepath, line=lineno,
382
- severity=Severity.ERROR,
383
- rule_id="STYLE-022",
384
- message="禁止在循环中频繁调用 ORM.create(),容易造成性能问题",
385
- fix_hint="先批量组织数据,按批次处理",
386
- source_line=line.strip(),
387
- ))
388
-
389
- if loop_line and LOOP_DISPATCH_PATTERN.search(code_line):
390
- issues.append(LintIssue(
391
- file=filepath, line=lineno,
392
- severity=Severity.ERROR,
393
- rule_id="STYLE-023",
394
- message="禁止在循环中调用 DispatchServiceHelper.invoke*(),远程调用放进循环容易放大时延和失败面",
395
- fix_hint="合并调用、批量调用或先聚合参数",
396
- source_line=line.strip(),
397
- ))
398
-
399
- query_update_hit = False
400
- query_update_var = ""
401
- if SAVE_SERVICE_PATTERN.search(code_line):
402
- for var_name in list(query_result_vars) + list(query_result_entity_vars) + list(active_query_loop_vars) + list(single_line_query_loop_vars):
403
- if re.search(rf"\b{re.escape(var_name)}\b", code_line):
404
- query_update_hit = True
405
- query_update_var = var_name
406
- break
407
-
408
- if not query_update_hit:
409
- for var_name in list(query_result_entity_vars) + list(active_query_loop_vars) + list(single_line_query_loop_vars):
410
- if re.search(rf"\b{re.escape(var_name)}\s*\.\s*set\s*\(", code_line):
411
- query_update_hit = True
412
- query_update_var = var_name
413
- break
414
-
415
- if not query_update_hit:
416
- for var_name in query_result_vars:
417
- if re.search(rf"\b{re.escape(var_name)}\s*\.\s*get\s*\([^)]*\)\s*\.\s*set\s*\(", code_line):
418
- query_update_hit = True
419
- query_update_var = var_name
420
- break
421
-
422
- if query_update_hit:
423
- issues.append(LintIssue(
424
- file=filepath, line=lineno,
425
- severity=Severity.WARNING,
426
- rule_id="STYLE-017",
427
- message="QueryServiceHelper.query(...) 返回的是扁平查询结果,不应直接当成可更新实体使用",
428
- fix_hint="先看 skills/ok-cosmic/assets/snippets/query/BatchQuerySample.java 的“query -> id -> load -> update”桥接样例,先查 id,再 load 实体包后更新保存",
429
- source_line=line.strip(),
430
- ))
431
-
432
- brace_depth += code_line.count("{") - code_line.count("}")
433
-
434
- return issues
@@ -1,36 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """验证来源注释检查 (VERIFY-*) — 来源: coding-preferences.md C 层治理项(仅严格模式生效)"""
3
-
4
- import re
5
- from typing import List
6
-
7
- from .base import LintIssue, Severity
8
-
9
-
10
- OVERRIDE_PATTERN = re.compile(r"@Override")
11
- VERIFY_PATTERN = re.compile(r"//\s*验证来源:")
12
-
13
-
14
- def check(filepath: str, lines: List[str]) -> List[LintIssue]:
15
- """执行 @Override 验证来源注释检查,返回 C 层治理建议。"""
16
- issues: List[LintIssue] = []
17
-
18
- for i, line in enumerate(lines):
19
- if OVERRIDE_PATTERN.search(line):
20
- # 检查上方 15 行内是否有验证来源注释(兼容被 JavaDoc 隔开的场景)
21
- has_verify = False
22
- for j in range(max(0, i - 15), i):
23
- if VERIFY_PATTERN.search(lines[j]):
24
- has_verify = True
25
- break
26
- if not has_verify:
27
- issues.append(LintIssue(
28
- file=filepath, line=i + 1,
29
- severity=Severity.INFO,
30
- rule_id="VERIFY-001",
31
- message="@Override 方法缺少验证来源注释",
32
- fix_hint="在 @Override 上方添加: // 验证来源: scripts/cosmic-api-knowledge.py detail [ClassName]",
33
- source_line=line.strip(),
34
- ))
35
-
36
- return issues
@@ -1,186 +0,0 @@
1
- #!/usr/bin/env python3
2
- # SPDX-License-Identifier: NOASSERTION
3
- """Shared runtime/route HTTP client helpers for ok-cosmic scripts."""
4
-
5
- import json
6
- import os
7
- import ssl
8
- import sys
9
- import urllib.error
10
- import urllib.parse
11
- import urllib.request
12
- from typing import Any, Callable, Dict, Optional
13
-
14
-
15
- DEFAULT_ROUTE_TIMEOUT_SECONDS = 10.0
16
- ROUTE_PAYLOAD_KEYS = ("data", "result", "respData", "response")
17
-
18
-
19
- def append_query_param(url: str, key: str, value: str) -> str:
20
- """Append a query parameter unless the key already exists."""
21
- if not url or not key or not value:
22
- return url
23
- parsed = urllib.parse.urlsplit(url)
24
- query_pairs = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
25
- if any(k == key for k, _ in query_pairs):
26
- return url
27
- query_pairs.append((key, value))
28
- return urllib.parse.urlunsplit(
29
- (
30
- parsed.scheme,
31
- parsed.netloc,
32
- parsed.path,
33
- urllib.parse.urlencode(query_pairs),
34
- parsed.fragment,
35
- )
36
- )
37
-
38
-
39
- def parse_bool(value: Any, default: bool = False) -> bool:
40
- """Parse bool-like JSON/env values."""
41
- if value is None:
42
- return default
43
- if isinstance(value, bool):
44
- return value
45
- if isinstance(value, (int, float)):
46
- return value != 0
47
- if isinstance(value, str):
48
- normalized = value.strip().lower()
49
- if normalized in {"1", "true", "yes", "y", "on"}:
50
- return True
51
- if normalized in {"0", "false", "no", "n", "off"}:
52
- return False
53
- return bool(value)
54
-
55
-
56
- def _first_non_empty(*values: Any) -> str:
57
- for value in values:
58
- text = str(value or "").strip()
59
- if text:
60
- return text
61
- return ""
62
-
63
-
64
- class RouteClient:
65
- """Small JSON POST client for Cosmic unified runtime/route APIs."""
66
-
67
- def __init__(
68
- self,
69
- route_config: Optional[Dict[str, Any]] = None,
70
- *,
71
- api_url: Optional[str] = None,
72
- debug: bool = False,
73
- default_timeout: float = DEFAULT_ROUTE_TIMEOUT_SECONDS,
74
- missing_message: Optional[str] = None,
75
- ):
76
- if not isinstance(route_config, dict):
77
- route_config = {}
78
-
79
- self.debug = debug
80
- self.api_url = _first_non_empty(
81
- api_url,
82
- route_config.get("apiUrl"),
83
- os.getenv("COSMIC_ROUTE_API"),
84
- os.getenv("COSMIC_RUNTIME_ROUTE_API"),
85
- )
86
- open_api_sign = _first_non_empty(
87
- route_config.get("openApiSign"),
88
- route_config.get("openapiSign"),
89
- os.getenv("COSMIC_ROUTE_OPEN_API_SIGN"),
90
- os.getenv("COSMIC_OPEN_API_SIGN"),
91
- )
92
- if open_api_sign:
93
- self.api_url = append_query_param(self.api_url, "openApiSign", open_api_sign)
94
-
95
- self.api_token = _first_non_empty(
96
- route_config.get("apiToken"),
97
- route_config.get("token"),
98
- os.getenv("COSMIC_ROUTE_TOKEN"),
99
- )
100
- self.timeout = float(
101
- route_config.get("timeoutSeconds")
102
- or os.getenv("COSMIC_ROUTE_TIMEOUT")
103
- or default_timeout
104
- )
105
- self.skip_ssl_verify = parse_bool(route_config.get("skipSslVerify"), default=True)
106
- self.missing_message = missing_message or (
107
- "未配置统一路由 API。请在 ok-cosmic.json 的 route.apiUrl 中配置统一路由,"
108
- "或设置 COSMIC_ROUTE_API / COSMIC_RUNTIME_ROUTE_API 环境变量。"
109
- )
110
-
111
- def _log_debug(self, msg: str) -> None:
112
- if self.debug:
113
- print(f" (DEBUG) {msg}", file=sys.stderr)
114
-
115
- def post(self, payload: Dict[str, Any]) -> Dict[str, Any]:
116
- """POST JSON payload and return the decoded JSON object."""
117
- if not self.api_url:
118
- raise RuntimeError(self.missing_message)
119
-
120
- headers = {"Content-Type": "application/json"}
121
- if self.api_token:
122
- headers["Authorization"] = f"Bearer {self.api_token}"
123
-
124
- req_body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
125
- self._log_debug(f"POST {self.api_url}")
126
- self._log_debug(f"payload={json.dumps(payload, ensure_ascii=False)}")
127
- req = urllib.request.Request(
128
- self.api_url,
129
- data=req_body,
130
- headers=headers,
131
- method="POST",
132
- )
133
-
134
- try:
135
- ssl_context = ssl.create_default_context()
136
- if self.skip_ssl_verify:
137
- ssl_context.check_hostname = False
138
- ssl_context.verify_mode = ssl.CERT_NONE
139
- with urllib.request.urlopen(req, timeout=self.timeout, context=ssl_context) as resp:
140
- raw = json.loads(resp.read().decode("utf-8", errors="replace"))
141
- if not isinstance(raw, dict):
142
- raise RuntimeError("远程接口返回的根对象不是 JSON Object。")
143
- return raw
144
- except urllib.error.HTTPError as e:
145
- body = ""
146
- try:
147
- body = e.read().decode("utf-8", errors="replace").strip()
148
- except Exception:
149
- pass
150
- detail = f": {body}" if body else f": {e.reason}"
151
- raise RuntimeError(f"HTTP {e.code}{detail}") from e
152
- except urllib.error.URLError as e:
153
- raise RuntimeError(f"Network error: {e.reason}") from e
154
- except json.JSONDecodeError as e:
155
- raise RuntimeError(f"响应 JSON 解析失败: {e}") from e
156
-
157
-
158
- def unwrap_route_payload(data: Any, payload_matcher: Callable[[Any], bool]) -> Any:
159
- """Unwrap common route wrapper keys and return the domain payload."""
160
- if not isinstance(data, dict):
161
- return data
162
- if payload_matcher(data):
163
- return data
164
- for key in ROUTE_PAYLOAD_KEYS:
165
- value = data.get(key)
166
- if payload_matcher(value):
167
- return value
168
- return data
169
-
170
-
171
- def unwrap_route_raw(raw: Dict[str, Any], payload_matcher: Callable[[Any], bool]) -> Dict[str, Any]:
172
- """Unwrap one level of route envelope while preserving the original root shape."""
173
- data = raw.get("data")
174
- if not isinstance(data, dict):
175
- return raw
176
- if "status" in data and "data" in data:
177
- return data
178
- if payload_matcher(data):
179
- return raw
180
- for key in ROUTE_PAYLOAD_KEYS:
181
- value = data.get(key)
182
- if payload_matcher(value):
183
- cloned = dict(raw)
184
- cloned["data"] = value
185
- return cloned
186
- return raw