kcode-pi 0.1.5 → 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 (50) hide show
  1. package/README.md +35 -2
  2. package/dist/cli/kcode.d.ts +1 -0
  3. package/dist/cli/kcode.js +27 -4
  4. package/package.json +1 -1
  5. package/src/cli/kcode.ts +29 -4
  6. package/src/official/kingdee-skills.ts +60 -13
  7. package/src/rules/checker.ts +143 -0
  8. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/SKILL.md +2 -2
  9. package/vendor/kingdee-skills/ok-cosmic/SKILL.md +52 -101
  10. package/vendor/kingdee-skills/ok-cosmic/agents/openai.yaml +4 -4
  11. package/vendor/kingdee-skills/ok-cosmic/manifest.json +21 -20
  12. package/vendor/kingdee-skills/ok-cosmic/ok-cosmic-intro.html +1 -1
  13. package/vendor/kingdee-skills/ok-cosmic/rules/a-layer-rules.json +1 -1
  14. package/vendor/kingdee-skills/ok-cosmic/rules/anti-patterns.md +2 -2
  15. package/vendor/kingdee-skills/ok-cosmic/rules/coding-preferences.md +4 -4
  16. package/vendor/kingdee-skills/ok-cosmic/rules/constraints.md +3 -3
  17. package/vendor/kingdee-skills/ok-cosmic/rules/decision-matrix.md +8 -8
  18. package/vendor/kingdee-skills/ok-cosmic/rules/intent-routing.md +1 -1
  19. package/vendor/kingdee-skills/ok-cosmic/rules/post-check.md +19 -18
  20. package/vendor/kingdee-skills/ok-ksql/SKILL.md +9 -9
  21. package/vendor/kingdee-skills/ok-ksql/manifest.json +2 -1
  22. package/vendor/kingdee-skills/ok-ksql/references/ksql-datafix.md +2 -2
  23. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/pattern-matcher.py +0 -336
  24. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/review-score-calculator.py +0 -121
  25. package/vendor/kingdee-skills/ok-cosmic/CHANGELOG.md +0 -295
  26. package/vendor/kingdee-skills/ok-cosmic/README.md +0 -460
  27. package/vendor/kingdee-skills/ok-cosmic/requirements.txt +0 -2
  28. package/vendor/kingdee-skills/ok-cosmic/scripts/config_loader.py +0 -204
  29. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-api-knowledge.py +0 -910
  30. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-basedata-query.py +0 -359
  31. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-config-check.py +0 -181
  32. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-extpoints-query.py +0 -389
  33. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-form-metadata.py +0 -856
  34. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-check.py +0 -262
  35. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-lint.py +0 -293
  36. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/__init__.py +0 -2
  37. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/base.py +0 -393
  38. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/resource_check.py +0 -176
  39. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/scene_check.py +0 -375
  40. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/style_check.py +0 -434
  41. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/verify_check.py +0 -36
  42. package/vendor/kingdee-skills/ok-cosmic/scripts/route_client.py +0 -186
  43. package/vendor/kingdee-skills/ok-cosmic/scripts/script_utils.py +0 -40
  44. package/vendor/kingdee-skills/ok-cosmic/scripts/sqlite_cache.py +0 -142
  45. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-commons.jar +0 -0
  46. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-features.jar +0 -0
  47. package/vendor/kingdee-skills/ok-cosmic/setup/setup-mac.sh +0 -18
  48. package/vendor/kingdee-skills/ok-cosmic/setup/setup-windows.bat +0 -53
  49. package/vendor/kingdee-skills/ok-cosmic/setup/setup.jar +0 -0
  50. package/vendor/kingdee-skills/ok-ksql/scripts/ksql_lint.py +0 -363
@@ -1,910 +0,0 @@
1
- #!/usr/bin/env python3
2
- # SPDX-License-Identifier: NOASSERTION
3
- """
4
- cosmic-api-knowledge.py — Offline Cosmic API knowledge graph query tool.
5
-
6
- Usage:
7
- python3 cosmic-api-knowledge.py --config ok-cosmic.json search <queryA>,<queryB> # batch search
8
- python3 cosmic-api-knowledge.py --config ok-cosmic.json search <query...>
9
- python3 cosmic-api-knowledge.py --config ok-cosmic.json search <query...> --class-prefix kd.bos.servicehelper
10
- python3 cosmic-api-knowledge.py --config ok-cosmic.json search-method <queryA>,<queryB> # batch search-method
11
- python3 cosmic-api-knowledge.py --config ok-cosmic.json search-method <query...>
12
- python3 cosmic-api-knowledge.py --config ok-cosmic.json detail <full.class.Name1>,<full.class.Name2> # batch detail
13
- python3 cosmic-api-knowledge.py --config ok-cosmic.json detail <full.class.Name>
14
- python3 cosmic-api-knowledge.py --config ok-cosmic.json detail <full.class.Name> --method <keyword>
15
- python3 cosmic-api-knowledge.py --config ok-cosmic.json detail <full.class.Name> --method <keyword> --declared-only
16
- python3 cosmic-api-knowledge.py --config ok-cosmic.json detail <full.class.Name> --method <keyword> --compact
17
-
18
- What it provides:
19
- 1. Fuzzy / regex class search against ok-cosmic-knowledge.db
20
- 2. Cross-class method search with ranking and package/category filters
21
- 3. Class detail lookup with signatures, return types, comments and inherited methods
22
- 4. Compact method fact output for downstream AI consumption
23
-
24
- What it does NOT do:
25
- - It does not inspect live jars directly; it only queries the built knowledge database
26
- - It does not verify runtime availability of a class or method in the current environment
27
- - It does not rebuild the knowledge database; use ok-cosmic-knowledge for that
28
-
29
- Prerequisites:
30
- - A valid ok-cosmic.json or equivalent graph config
31
- - A built ok-cosmic-knowledge.db reachable from config / environment / project paths
32
- """
33
-
34
- import json
35
- import sys
36
- import os
37
- import argparse
38
- import re
39
- from concurrent.futures import ThreadPoolExecutor, as_completed
40
- from pathlib import Path
41
- from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
42
-
43
- from config_loader import load_project_config
44
- from script_utils import FriendlyArgumentParser, run_cli
45
-
46
-
47
- CHEAT_SHEET_PATH = Path(__file__).resolve().parent.parent / "rules" / "cheat-sheet.md"
48
-
49
-
50
- def _load_cheat_sheet_index() -> Tuple[Set[str], Set[Tuple[str, str]]]:
51
- """解析 cheat-sheet.md,提取已收录的类简名和 (类简名, 方法名) 对。"""
52
- classes: Set[str] = set()
53
- methods: Set[Tuple[str, str]] = set()
54
- try:
55
- text = CHEAT_SHEET_PATH.read_text(encoding="utf-8")
56
- except (FileNotFoundError, OSError):
57
- return classes, methods
58
-
59
- in_code_block = False
60
- for line in text.splitlines():
61
- stripped = line.strip()
62
- if stripped.startswith("```"):
63
- in_code_block = not in_code_block
64
- continue
65
- if not in_code_block or stripped.startswith("//") or stripped.startswith("import "):
66
- continue
67
- # 匹配 ClassName.methodName( 模式
68
- for m in re.finditer(r'\b([A-Z][A-Za-z0-9_]+)\.([a-z][A-Za-z0-9_]*)\s*\(', stripped):
69
- cls_name, method_name = m.group(1), m.group(2)
70
- classes.add(cls_name)
71
- methods.add((cls_name, method_name))
72
- # 匹配 new ClassName( 模式
73
- for m in re.finditer(r'\bnew\s+([A-Z][A-Za-z0-9_]+)\s*\(', stripped):
74
- classes.add(m.group(1))
75
- return classes, methods
76
-
77
-
78
- PRIMITIVE_DESCRIPTOR_TYPES = {
79
- "V": "void",
80
- "Z": "boolean",
81
- "B": "byte",
82
- "C": "char",
83
- "S": "short",
84
- "I": "int",
85
- "F": "float",
86
- "J": "long",
87
- "D": "double",
88
- }
89
-
90
-
91
- class ApiGraph:
92
- def __init__(self, config: Dict[str, Any]):
93
- graph_config = config.get("graph") if isinstance(config.get("graph"), dict) else config
94
- self.config_dir = str(config.get("__config_dir__", "")).strip()
95
- self.script_dir = str(Path(__file__).resolve().parent)
96
- db_path = str(graph_config.get("dbPath", "")).strip()
97
-
98
- self.db_path = ""
99
- if db_path:
100
- raw_db_path = os.path.expanduser(db_path)
101
- if os.path.isabs(raw_db_path):
102
- self.db_path = os.path.normpath(raw_db_path)
103
- else:
104
- base_dir = self.config_dir or os.getcwd()
105
- self.db_path = os.path.normpath(os.path.abspath(os.path.join(base_dir, raw_db_path)))
106
-
107
- self._cheat_classes, self._cheat_methods = _load_cheat_sheet_index()
108
-
109
- self.sqlite_error: Optional[str] = None
110
-
111
-
112
-
113
- def _get_conn(self):
114
- if hasattr(self, '_conn_cache') and self._conn_cache is not None:
115
- return self._conn_cache
116
- if self.sqlite_error:
117
- return None
118
- try:
119
- import sqlite3
120
- import re
121
- except Exception as e:
122
- self.sqlite_error = f"sqlite3 or re unavailable: {e}"
123
- return None
124
-
125
- if not self.db_path or not os.path.exists(self.db_path):
126
- return None
127
- conn = sqlite3.connect(self.db_path, check_same_thread=False)
128
- conn.execute("PRAGMA journal_mode=WAL")
129
- conn.execute("PRAGMA busy_timeout=5000")
130
-
131
- def regexp(expr, item):
132
- try:
133
- reg = re.compile(expr, re.IGNORECASE)
134
- return reg.search(item) is not None
135
- except Exception:
136
- return False
137
- conn.create_function("REGEXP", 2, regexp)
138
-
139
- conn.row_factory = sqlite3.Row
140
- self._conn_cache = conn
141
- return conn
142
-
143
- @staticmethod
144
- def _decode_descriptor_type(descriptor: str, index: int) -> Tuple[str, int]:
145
- array_depth = 0
146
- while index < len(descriptor) and descriptor[index] == "[":
147
- array_depth += 1
148
- index += 1
149
-
150
- if index >= len(descriptor):
151
- return "Object" + "[]" * array_depth, index
152
-
153
- token = descriptor[index]
154
- if token == "L":
155
- end = descriptor.find(";", index)
156
- if end == -1:
157
- return "Object" + "[]" * array_depth, len(descriptor)
158
- class_name = descriptor[index + 1:end].replace("/", ".").replace("$", ".")
159
- return class_name + "[]" * array_depth, end + 1
160
-
161
- pretty = PRIMITIVE_DESCRIPTOR_TYPES.get(token, token)
162
- return pretty + "[]" * array_depth, index + 1
163
-
164
- @classmethod
165
- def _normalize_type_name(cls, type_name: Optional[str], *, keep_full_path: bool = False) -> str:
166
- raw = (type_name or "").strip()
167
- if not raw:
168
- return "void"
169
- if raw.endswith("[]"):
170
- return cls._normalize_type_name(raw[:-2], keep_full_path=keep_full_path) + "[]"
171
- if raw.startswith("("):
172
- return raw
173
- normalized = raw.replace("$", ".")
174
- if keep_full_path:
175
- return normalized
176
- return normalized.split(".")[-1]
177
-
178
- @classmethod
179
- def _format_method_signature(cls, method_name: str, method_signature: Optional[str], return_type: Optional[str] = None) -> str:
180
- signature = (method_signature or "").strip()
181
- if not signature:
182
- return f"{method_name}()"
183
- if signature.startswith("(") and ("/" not in signature and ";" not in signature):
184
- suffix = f" -> {cls._normalize_type_name(return_type, keep_full_path=True)}" if return_type else ""
185
- return f"{signature}{suffix}"
186
- if not signature.startswith("("):
187
- return signature
188
-
189
- index = 1
190
- params: List[str] = []
191
- while index < len(signature) and signature[index] != ")":
192
- param_type, index = cls._decode_descriptor_type(signature, index)
193
- params.append(param_type)
194
-
195
- if index >= len(signature) or signature[index] != ")":
196
- return f"{method_name}{signature}"
197
-
198
- decoded_return_type, _ = cls._decode_descriptor_type(signature, index + 1)
199
- return f"{method_name}({', '.join(params)}) -> {decoded_return_type}"
200
-
201
- @classmethod
202
- def _format_method_signature_for_search(cls, method_name: str, method_signature: Optional[str], return_type: Optional[str] = None) -> str:
203
- signature = cls._format_method_signature(method_name, method_signature, return_type)
204
- if " -> " not in signature:
205
- return signature
206
- prefix, suffix = signature.rsplit(" -> ", 1)
207
- if suffix.startswith("java."):
208
- return f"{prefix} -> {cls._normalize_type_name(suffix)}"
209
- return signature
210
-
211
- @staticmethod
212
- def _parse_method_comment(comment: Optional[str]) -> Tuple[str, List[Tuple[str, str]], Optional[str], List[str]]:
213
- text = (comment or "").strip()
214
- if not text:
215
- return "", [], None, []
216
-
217
- description_lines: List[str] = []
218
- params: List[Tuple[str, str]] = []
219
- return_desc: Optional[str] = None
220
- throws_desc: List[str] = []
221
-
222
- for raw_line in text.splitlines():
223
- line = raw_line.strip()
224
- if not line:
225
- continue
226
- if line.startswith("@param"):
227
- match = re.match(r"@param\s+`?([A-Za-z_$][A-Za-z0-9_$]*)`?\s*(.*)", line)
228
- if match:
229
- params.append((match.group(1), match.group(2).strip()))
230
- else:
231
- params.append(("", line[len("@param"):].strip()))
232
- continue
233
- if line.startswith("@return"):
234
- return_desc = line[len("@return"):].strip() or None
235
- continue
236
- if line.startswith("@throws"):
237
- throws_desc.append(line[len("@throws"):].strip())
238
- continue
239
- description_lines.append(line)
240
-
241
- description = "\n".join(description_lines).strip()
242
- return description, params, return_desc, throws_desc
243
-
244
- @classmethod
245
- def _summarize_method_comment(cls, comment: Optional[str], limit: int = 48) -> str:
246
- description, _, _, _ = cls._parse_method_comment(comment)
247
- summary = description.replace("\n", " ").strip()
248
- if len(summary) > limit:
249
- summary = summary[:limit].rstrip() + "..."
250
- return summary
251
-
252
- @staticmethod
253
- def _normalize_terms(query: Sequence[str] | str) -> List[str]:
254
- if isinstance(query, str):
255
- raw_terms = [query]
256
- else:
257
- raw_terms = list(query)
258
-
259
- terms: List[str] = []
260
- for item in raw_terms:
261
- if item is None:
262
- continue
263
- text = str(item).strip()
264
- if not text:
265
- continue
266
- for part in text.split():
267
- part = part.strip()
268
- if part:
269
- terms.append(part)
270
- return terms
271
-
272
- @staticmethod
273
- def _build_term_clause(
274
- column_expr: str,
275
- terms: List[str],
276
- *,
277
- match_all: bool,
278
- use_regex: bool = False,
279
- ) -> Tuple[str, List[str]]:
280
- if not terms:
281
- return "", []
282
-
283
- operator = "REGEXP" if use_regex else "LIKE"
284
- glue = " AND " if match_all else " OR "
285
- clause_parts: List[str] = []
286
- params: List[str] = []
287
- for term in terms:
288
- clause_parts.append(f"{column_expr} {operator} ?")
289
- params.append(term if use_regex else f"%{term}%")
290
- return "(" + glue.join(clause_parts) + ")", params
291
-
292
- @staticmethod
293
- def _build_prefix_clause(column_expr: str, prefixes: Optional[Sequence[str]]) -> Tuple[str, List[str]]:
294
- if not prefixes:
295
- return "", []
296
- clean_prefixes = [str(prefix).strip() for prefix in prefixes if str(prefix).strip()]
297
- if not clean_prefixes:
298
- return "", []
299
-
300
- parts = [f"{column_expr} LIKE ?" for _ in clean_prefixes]
301
- params = [f"{prefix}%" for prefix in clean_prefixes]
302
- return "(" + " OR ".join(parts) + ")", params
303
-
304
- @staticmethod
305
- def _build_exact_keyword_clause(column_expr: str, values: Optional[Sequence[str]]) -> Tuple[str, List[str]]:
306
- if not values:
307
- return "", []
308
- clean_values = [str(value).strip() for value in values if str(value).strip()]
309
- if not clean_values:
310
- return "", []
311
-
312
- parts = [f"LOWER({column_expr}) LIKE ?" for _ in clean_values]
313
- params = [f"%{value.lower()}%" for value in clean_values]
314
- return "(" + " OR ".join(parts) + ")", params
315
-
316
- def _compose_class_filters(
317
- self,
318
- *,
319
- term_column_expr: str,
320
- query_terms: Sequence[str] | str,
321
- match_all_terms: bool = False,
322
- use_regex: bool = False,
323
- class_prefixes: Optional[Sequence[str]] = None,
324
- class_regex: Optional[str] = None,
325
- class_keywords: Optional[Sequence[str]] = None,
326
- ) -> Tuple[str, List[str]]:
327
- clauses: List[str] = []
328
- params: List[str] = []
329
-
330
- normalized_terms = self._normalize_terms(query_terms)
331
- term_clause, term_params = self._build_term_clause(
332
- term_column_expr,
333
- normalized_terms,
334
- match_all=match_all_terms,
335
- use_regex=use_regex,
336
- )
337
- if term_clause:
338
- clauses.append(term_clause)
339
- params.extend(term_params)
340
-
341
- prefix_clause, prefix_params = self._build_prefix_clause("class_name", class_prefixes)
342
- if prefix_clause:
343
- clauses.append(prefix_clause)
344
- params.extend(prefix_params)
345
-
346
- if class_regex and str(class_regex).strip():
347
- clauses.append("(class_name REGEXP ?)")
348
- params.append(str(class_regex).strip())
349
-
350
- keyword_clause, keyword_params = self._build_exact_keyword_clause("class_name", class_keywords)
351
- if keyword_clause:
352
- clauses.append(keyword_clause)
353
- params.extend(keyword_params)
354
-
355
- where_sql = " AND ".join(clauses) if clauses else "1=1"
356
- return where_sql, params
357
-
358
- @staticmethod
359
- def _build_method_relevance_expressions(normalized_terms: Sequence[str], query_label: str) -> Tuple[str, str, List[str]]:
360
- select_parts: List[str] = [
361
- "CASE WHEN LOWER(method_name) = LOWER(?) THEN 0 ELSE 1 END AS exact_rank",
362
- "CASE WHEN LOWER(method_name) LIKE LOWER(?) THEN 0 ELSE 1 END AS prefix_rank",
363
- ]
364
- params: List[str] = [query_label, f"{query_label}%"]
365
-
366
- if normalized_terms:
367
- all_match_parts = ["LOWER(method_name) LIKE LOWER(?)" for _ in normalized_terms]
368
- select_parts.append(
369
- "CASE WHEN " + " AND ".join(all_match_parts) + " THEN 0 ELSE 1 END AS all_terms_rank"
370
- )
371
- params.extend([f"%{term}%" for term in normalized_terms])
372
-
373
- ordered_pattern = "%" + "%".join(normalized_terms) + "%"
374
- select_parts.append("CASE WHEN LOWER(method_name) LIKE LOWER(?) THEN 0 ELSE 1 END AS ordered_rank")
375
- params.append(ordered_pattern)
376
-
377
- comment_match_parts = ["LOWER(COALESCE(method_comment, '')) LIKE LOWER(?)" for _ in normalized_terms]
378
- select_parts.append(
379
- "CASE WHEN " + " AND ".join(comment_match_parts) + " THEN 0 ELSE 1 END AS comment_rank"
380
- )
381
- params.extend([f"%{term}%" for term in normalized_terms])
382
- else:
383
- select_parts.extend([
384
- "0 AS all_terms_rank",
385
- "0 AS ordered_rank",
386
- "0 AS comment_rank",
387
- ])
388
-
389
- order_sql = ", ".join([
390
- "exact_rank ASC",
391
- "all_terms_rank ASC",
392
- "ordered_rank ASC",
393
- "prefix_rank ASC",
394
- "comment_rank ASC",
395
- "LENGTH(method_name) ASC",
396
- "class_name ASC",
397
- "method_name ASC",
398
- ])
399
- return ", ".join(select_parts), order_sql, params
400
-
401
- def search_methods(
402
- self,
403
- method_query,
404
- page=1,
405
- page_size=20,
406
- *,
407
- match_all_terms: bool = False,
408
- class_prefixes: Optional[Sequence[str]] = None,
409
- class_regex: Optional[str] = None,
410
- class_keywords: Optional[Sequence[str]] = None,
411
- ):
412
- conn = self._get_conn()
413
- if not conn: return f"✖️ 未配置 graph.dbPath,或未在路径 '{self.db_path}' 下找到有效数据库"
414
-
415
- offset = (page - 1) * page_size
416
- try:
417
- with conn:
418
- normalized_terms = self._normalize_terms(method_query)
419
- where_sql, where_params = self._compose_class_filters(
420
- term_column_expr="method_name",
421
- query_terms=normalized_terms,
422
- match_all_terms=match_all_terms,
423
- class_prefixes=class_prefixes,
424
- class_regex=class_regex,
425
- class_keywords=class_keywords,
426
- )
427
-
428
- count_sql = f"SELECT COUNT(*) as total FROM method_node WHERE {where_sql}"
429
- total = conn.execute(count_sql, tuple(where_params)).fetchone()['total']
430
- query_label = " ".join(normalized_terms) or str(method_query).strip()
431
- relevance_select_sql, order_sql, order_params = self._build_method_relevance_expressions(normalized_terms, query_label)
432
-
433
- sql = """
434
- SELECT class_name, method_name, method_signature, return_type, method_comment,
435
- {relevance_select_sql}
436
- FROM method_node
437
- WHERE {where_sql}
438
- ORDER BY {order_sql}
439
- LIMIT ? OFFSET ?
440
- """
441
- rows = conn.execute(
442
- sql.format(where_sql=where_sql, order_sql=order_sql, relevance_select_sql=relevance_select_sql),
443
- tuple(order_params + where_params + [page_size, offset]),
444
- ).fetchall()
445
-
446
- if not rows:
447
- extra = []
448
- if class_prefixes:
449
- extra.append(f"class-prefix={','.join(class_prefixes)}")
450
- if class_keywords:
451
- extra.append(f"kind={','.join(class_keywords)}")
452
- if class_regex:
453
- extra.append(f"class-regex={class_regex}")
454
- suffix = f"(过滤: {'; '.join(extra)})" if extra else ""
455
- return f"[Search] 未找到匹配方法 `{query_label}`{suffix}。"
456
-
457
- def relevance_bucket(row):
458
- simple_cls = row['class_name'].rsplit('.', 1)[-1]
459
- cheat_bonus = 0 if (simple_cls, row['method_name']) in self._cheat_methods else 32
460
- return (
461
- cheat_bonus
462
- + int(row["exact_rank"]) * 16
463
- + int(row["all_terms_rank"]) * 8
464
- + int(row["ordered_rank"]) * 4
465
- + int(row["prefix_rank"]) * 2
466
- + int(row["comment_rank"])
467
- )
468
-
469
- if page == 1 and len(rows) >= 3 and all(relevance_bucket(row) <= 4 for row in rows[:3]):
470
- strong_rows = [row for row in rows if relevance_bucket(row) <= 4]
471
- if strong_rows and len(strong_rows) < len(rows):
472
- rows = strong_rows
473
-
474
- total_pages = (total + page_size - 1) // page_size
475
- md = [f"### [Method] 方法搜索结果: {query_label} ({page}/{total_pages} 页, 共 {total} 条)\n"]
476
- if class_prefixes or class_keywords or class_regex:
477
- filter_desc: List[str] = []
478
- if class_prefixes:
479
- filter_desc.append(f"class-prefix={','.join(class_prefixes)}")
480
- if class_keywords:
481
- filter_desc.append(f"kind={','.join(class_keywords)}")
482
- if class_regex:
483
- filter_desc.append(f"class-regex={class_regex}")
484
- md.append(f"> 过滤条件: {'; '.join(filter_desc)}")
485
- md.append("")
486
- full_match_rows = []
487
- partial_match_rows = []
488
- if len(normalized_terms) >= 2:
489
- for row in rows:
490
- if int(row["all_terms_rank"]) == 0:
491
- full_match_rows.append(row)
492
- else:
493
- partial_match_rows.append(row)
494
- else:
495
- full_match_rows = list(rows)
496
-
497
- def append_rows(title: Optional[str], result_rows):
498
- if not result_rows:
499
- return
500
- if title:
501
- md.append(title)
502
- md.append("| 定义类 (Class) | 签名 (Signature) | 说明 (Comment) |")
503
- md.append("| :--- | :--- | :--- |")
504
- for row in result_rows:
505
- comment = self._summarize_method_comment(row["method_comment"])
506
- pretty_signature = self._format_method_signature_for_search(row["method_name"], row["method_signature"], row["return_type"])
507
- simple_cls = row['class_name'].rsplit('.', 1)[-1]
508
- cheat_tag = " ✔️" if (simple_cls, row['method_name']) in self._cheat_methods else ""
509
- md.append(f"| `{row['class_name']}` | `#{row['method_name']}{pretty_signature}`{cheat_tag} | {comment or '-'} |")
510
- md.append("")
511
-
512
- if len(normalized_terms) >= 2 and full_match_rows:
513
- append_rows("#### 全关键词命中", full_match_rows)
514
- if partial_match_rows:
515
- append_rows("#### 部分关键词命中", partial_match_rows)
516
- else:
517
- append_rows(None, rows)
518
-
519
- md.append("\n*提示: 使用 `detail <class_name>` 查看类完整继承树和注释。*")
520
- return "\n".join(md)
521
- except Exception as e: return f"✖️ 搜索失败: {str(e)}"
522
-
523
- def fuzzy_search_classes(
524
- self,
525
- query,
526
- page=1,
527
- page_size=20,
528
- use_regex=False,
529
- *,
530
- match_all_terms: bool = False,
531
- class_prefixes: Optional[Sequence[str]] = None,
532
- class_regex: Optional[str] = None,
533
- class_keywords: Optional[Sequence[str]] = None,
534
- ):
535
- conn = self._get_conn()
536
- if not conn:
537
- if self.sqlite_error:
538
- return f"✖️ 运行环境缺少依赖: {self.sqlite_error}"
539
- return f"✖️ 未配置 graph.dbPath,或未在路径 '{self.db_path}' 下找到有效数据库"
540
-
541
- offset = (page - 1) * page_size
542
-
543
- try:
544
- with conn:
545
- where_sql, where_params = self._compose_class_filters(
546
- term_column_expr="class_name",
547
- query_terms=query,
548
- match_all_terms=match_all_terms,
549
- use_regex=use_regex,
550
- class_prefixes=class_prefixes,
551
- class_regex=class_regex,
552
- class_keywords=class_keywords,
553
- )
554
- count_sql = f"SELECT COUNT(*) as total FROM class_node WHERE {where_sql}"
555
- total_row = conn.execute(count_sql, tuple(where_params)).fetchone()
556
- total = total_row['total']
557
-
558
- data_sql = """
559
- SELECT class_name, class_comment
560
- FROM class_node
561
- WHERE {where_sql}
562
- ORDER BY class_name ASC LIMIT ? OFFSET ?
563
- """
564
- rows = conn.execute(data_sql.format(where_sql=where_sql), tuple(where_params + [page_size, offset])).fetchall()
565
-
566
- query_label = " ".join(self._normalize_terms(query)) or str(query).strip()
567
- if not rows:
568
- msg = f"[Search] 未找到匹配 `{query_label}` 的类名"
569
- if use_regex: msg += " (使用正则模式)"
570
- extra = []
571
- if class_prefixes:
572
- extra.append(f"class-prefix={','.join(class_prefixes)}")
573
- if class_keywords:
574
- extra.append(f"kind={','.join(class_keywords)}")
575
- if class_regex:
576
- extra.append(f"class-regex={class_regex}")
577
- if extra:
578
- msg += f"(过滤: {'; '.join(extra)})"
579
- return msg + f"(第 {page} 页)。"
580
-
581
- total_pages = (total + page_size - 1) // page_size
582
- mode_str = "正则" if use_regex else "模糊"
583
- md = [f"### [Search] {mode_str}搜索结果 (关键字: {query_label}, 第 {page}/{total_pages} 页, 共 {total} 条)\n"]
584
- if class_prefixes or class_keywords or class_regex:
585
- filter_desc: List[str] = []
586
- if class_prefixes:
587
- filter_desc.append(f"class-prefix={','.join(class_prefixes)}")
588
- if class_keywords:
589
- filter_desc.append(f"kind={','.join(class_keywords)}")
590
- if class_regex:
591
- filter_desc.append(f"class-regex={class_regex}")
592
- md.append(f"> 过滤条件: {'; '.join(filter_desc)}")
593
- md.append("")
594
- for row in rows:
595
- class_comment = (row['class_comment'] or '').strip()
596
- simple_name = row['class_name'].rsplit('.', 1)[-1]
597
- cheat_tag = " `[速查已收录]`" if simple_name in self._cheat_classes else ""
598
- class_line = f"- **`{row['class_name']}`**{cheat_tag}"
599
- if class_comment:
600
- class_line += f" \n > {class_comment}"
601
- md.append(class_line)
602
-
603
- if page < total_pages:
604
- md.append(f"\n*提示: 还有更多结果 (page={page+1})。使用 `detail <classname>` 查看详情。*")
605
- else:
606
- md.append("\n*提示: 已显示全部结果。使用 `detail <classname>` 查看详情。*")
607
-
608
- return "\n".join(md)
609
- except Exception as e: return f"✖️ 搜索失败: {str(e)}"
610
-
611
- def get_class_details(self, class_name, method_filter: Optional[str] = None, declared_only: bool = False, compact: bool = False):
612
- conn = self._get_conn()
613
- if not conn:
614
- if self.sqlite_error:
615
- return f"✖️ 运行环境缺少 sqlite3 依赖: {self.sqlite_error}"
616
- return f"✖️ 未配置 graph.dbPath,或未在路径 '{self.db_path}' 下找到有效数据库"
617
-
618
- # SQL with optional method filtering
619
- method_clause = ""
620
- params = [class_name]
621
- if method_filter:
622
- method_clause = "AND m.method_name LIKE ?"
623
- params.append(f"%{method_filter}%")
624
-
625
- hierarchy_cte = """
626
- WITH RECURSIVE hierarchy(class_name, super_class_name, depth) AS (
627
- SELECT class_name, super_class_name, 0 FROM class_node WHERE class_name = ?
628
- """
629
- if not declared_only:
630
- hierarchy_cte += """
631
- UNION ALL
632
- SELECT c.class_name, c.super_class_name, h.depth + 1
633
- FROM class_node c JOIN hierarchy h ON c.class_name = h.super_class_name
634
- WHERE c.class_name != 'java.lang.Object' AND c.class_name IS NOT NULL
635
- """
636
- hierarchy_cte += ")"
637
-
638
- sql = f"""
639
- {hierarchy_cte}
640
- SELECT h.class_name, h.depth, c.class_comment, m.method_name, m.method_signature, m.return_type, m.method_comment
641
- FROM hierarchy h JOIN class_node c ON h.class_name = c.class_name
642
- LEFT JOIN method_node m ON h.class_name = m.class_name {method_clause}
643
- ORDER BY h.depth ASC, m.method_name ASC;
644
- """
645
- try:
646
- with conn:
647
- rows = conn.execute(sql, tuple(params)).fetchall()
648
- if not rows:
649
- return f"✖️ 未找到类 `{class_name}`" + (f" 或匹配的方法 `{method_filter}`" if method_filter else "") + "。"
650
-
651
- # Check if any methods were found if a filter was provided
652
- has_methods = any(r['method_name'] for r in rows)
653
- if method_filter and not has_methods:
654
- return f"✖️ 类 `{class_name}` 及其父类中未找到匹配 `{method_filter}` 的方法。"
655
-
656
- chain = []
657
- seen_classes = set()
658
- for r in rows:
659
- if r['class_name'] not in seen_classes:
660
- chain.append(r['class_name'])
661
- seen_classes.add(r['class_name'])
662
-
663
- method_names = [r['method_name'] for r in rows if r['method_name']]
664
- unique_method_names = []
665
- seen_method_names = set()
666
- for name in method_names:
667
- if name not in seen_method_names:
668
- unique_method_names.append(name)
669
- seen_method_names.add(name)
670
-
671
- md = [f"## `{class_name}`"]
672
- class_comment = (rows[0]['class_comment'] or '').strip()
673
- if class_comment:
674
- md.append(f"> {class_comment}")
675
- if not compact:
676
- if declared_only:
677
- md.append("范围: 仅当前类声明的方法")
678
- elif len(chain) > 1:
679
- md.append(f"继承链: `{' -> '.join(reversed(chain))}`")
680
- if method_filter and len(unique_method_names) == 1:
681
- md.append(f"方法: `{unique_method_names[0]}`")
682
-
683
- last_class = None
684
- single_class = len(chain) == 1
685
- single_filtered_method = method_filter and len(unique_method_names) == 1
686
- for row in rows:
687
- if row['class_name'] != last_class:
688
- if not compact and not single_class:
689
- title = "### 当前类" if row['depth'] == 0 else f"### 父类 L{row['depth']}"
690
- md.append(title)
691
- if row['class_name'] != class_name:
692
- class_comment = (row['class_comment'] or '').strip()
693
- md.append(f"`{row['class_name']}`")
694
- if class_comment:
695
- md.append(f"> {class_comment}")
696
- last_class = row['class_name']
697
- if row['method_name']:
698
- pretty_signature = self._format_method_signature(row["method_name"], row["method_signature"], row["return_type"])
699
- detail_class_simple = row['class_name'].rsplit('.', 1)[-1]
700
- cheat_tag = " `[速查已收录·免验证]`" if (detail_class_simple, row['method_name']) in self._cheat_methods else ""
701
- if compact:
702
- method_label = row['method_name'] if not (single_filtered_method and len(unique_method_names) == 1) else None
703
- if method_label:
704
- md.append(f"- `{method_label}{pretty_signature}`{cheat_tag}")
705
- else:
706
- md.append(f"- `{pretty_signature}`{cheat_tag}")
707
- elif single_filtered_method:
708
- md.append(f"- `{pretty_signature}`{cheat_tag}")
709
- else:
710
- md.append(f"- **{row['method_name']}** `{pretty_signature}`{cheat_tag}")
711
- description, params, return_desc, throws_desc = self._parse_method_comment(row["method_comment"])
712
- if description:
713
- md.append(f" - 说明: {description}")
714
- for param_name, param_desc in params:
715
- if param_name and param_desc:
716
- md.append(f" - 参数 `{param_name}`: {param_desc}")
717
- elif param_name:
718
- md.append(f" - 参数 `{param_name}`")
719
- elif param_desc:
720
- md.append(f" - 参数: {param_desc}")
721
- if return_desc:
722
- md.append(f" - 返回: {return_desc}")
723
- for throws_item in throws_desc:
724
- if throws_item:
725
- md.append(f" - 异常: {throws_item}")
726
- return "\n".join(md)
727
- except Exception as e: return f"✖️ 查询失败: {str(e)}"
728
-
729
-
730
- def _split_batch_queries(query_args: list) -> list:
731
- """检测逗号分隔的批量查询,返回查询组列表。
732
-
733
- - 无逗号 → [["kw1", "kw2"]] (单次搜索)
734
- - 有逗号 → [["kwA"], ["kwB", "kwC"]] (批量搜索)
735
- """
736
- joined = " ".join(query_args)
737
- if "," not in joined:
738
- return [query_args]
739
- groups = [g.strip() for g in joined.split(",") if g.strip()]
740
- return [g.split() for g in groups]
741
-
742
-
743
- def main():
744
- parser = FriendlyArgumentParser(
745
- description="Cosmic API Knowledge CLI — 苍穹 SDK 类名/方法签名离线查询",
746
- formatter_class=argparse.RawDescriptionHelpFormatter,
747
- epilog="""\
748
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
749
- 推荐用法
750
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
751
- # 查某个领域的 helper
752
- %(prog)s --config ok-cosmic.json search Helper --class-prefix kd.bos.servicehelper --kind helper
753
-
754
- # 查某个包下的方法
755
- %(prog)s --config ok-cosmic.json search-method send email --class-prefix kd.bos.servicehelper.message
756
-
757
- # 查插件相关类(全部关键词都要命中)
758
- %(prog)s --config ok-cosmic.json search plugin operation --kind plugin --all
759
-
760
- # 精确确认某个类有没有目标方法
761
- %(prog)s --config ok-cosmic.json detail "类全限定名" --method "关键词"
762
-
763
- # 确认方法是否由当前类自己声明(而非继承)
764
- %(prog)s --config ok-cosmic.json detail "类全限定名" --method "关键词" --declared-only
765
-
766
- # 低噪音事实块,适合继续喂给 AI 生成代码
767
- %(prog)s --config ok-cosmic.json detail "类全限定名" --method "关键词" --compact
768
-
769
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
770
- AI 调用约束(摘要,完整约束见 SKILL.md 跨脚本硬约束)
771
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
772
- - 搜索过宽时优先补 --class-prefix 或 --kind 收窄,不要连续无过滤重复搜索。
773
- - BOS 核心包前缀: kd.bos.servicehelper / kd.bos.entity / kd.bos.form / kd.bd
774
- """,
775
- )
776
- parser.add_argument("--config", help="Path to ok-cosmic.json config file")
777
-
778
- subparsers = parser.add_subparsers(dest="command", help="Commands")
779
-
780
- # Search class command
781
- search_parser = subparsers.add_parser("search", help="Search for classes (fuzzy or regex)")
782
- search_parser.add_argument("query", nargs="+", help="Search keyword(s) or regex pattern")
783
- search_parser.add_argument("--regex", action="store_true", help="Use regex for searching")
784
- search_parser.add_argument("--all", action="store_true", help="Require all keywords to match. Default is any keyword match")
785
- search_parser.add_argument("--class-prefix", action="append", default=[], help="Only include classes under package/class prefix, e.g. kd.bos.servicehelper")
786
- search_parser.add_argument("--class-regex", help="Further filter classes by regex on full class name")
787
- search_parser.add_argument(
788
- "--kind",
789
- action="append",
790
- default=[],
791
- choices=["helper", "servicehelper", "plugin", "service", "utils", "runtime", "entity", "const", "enum", "controller"],
792
- help="Common class category filter on class name",
793
- )
794
- search_parser.add_argument("--page", type=int, default=1, help="Page number")
795
- search_parser.add_argument("--page-size", type=int, default=20, help="Items per page")
796
-
797
- # Search method command
798
- msearch_parser = subparsers.add_parser("search-method", help="Search for methods across all classes")
799
- msearch_parser.add_argument("query", nargs="+", help="Method name keyword(s)")
800
- msearch_parser.add_argument("--all", action="store_true", help="Require all keywords to match. Default is any keyword match")
801
- msearch_parser.add_argument("--class-prefix", action="append", default=[], help="Only include methods defined in classes under this prefix")
802
- msearch_parser.add_argument("--class-regex", help="Further filter method results by class-name regex")
803
- msearch_parser.add_argument(
804
- "--kind",
805
- action="append",
806
- default=[],
807
- choices=["helper", "servicehelper", "plugin", "service", "utils", "runtime", "entity", "const", "enum", "controller"],
808
- help="Common class category filter on class name",
809
- )
810
- msearch_parser.add_argument("--page", type=int, default=1, help="Page number")
811
- msearch_parser.add_argument("--page-size", type=int, default=20, help="Items per page")
812
-
813
- # Detail command
814
- detail_parser = subparsers.add_parser("detail", help="Get class details")
815
- detail_parser.add_argument("classname", help="Full class name(s), comma-separated for batch query")
816
- detail_parser.add_argument("--method", help="Filter methods by name (fuzzy)")
817
- detail_parser.add_argument("--declared-only", action="store_true", help="Only show methods declared on the target class")
818
- detail_parser.add_argument("--compact", action="store_true", help="Compact output for AI consumption")
819
-
820
- args = parser.parse_args()
821
-
822
- config = load_project_config(args.config)
823
- api = ApiGraph(config)
824
-
825
- if args.command == "search":
826
- query_groups = _split_batch_queries(args.query)
827
-
828
- def _search_one(q):
829
- return api.fuzzy_search_classes(
830
- q, args.page, args.page_size,
831
- use_regex=args.regex,
832
- match_all_terms=args.all,
833
- class_prefixes=args.class_prefix,
834
- class_regex=args.class_regex,
835
- class_keywords=args.kind,
836
- )
837
-
838
- if len(query_groups) == 1:
839
- results = [_search_one(query_groups[0])]
840
- else:
841
- results = [None] * len(query_groups)
842
- with ThreadPoolExecutor(max_workers=min(len(query_groups), 4)) as pool:
843
- fut = {pool.submit(_search_one, q): i for i, q in enumerate(query_groups)}
844
- for f in as_completed(fut):
845
- idx = fut[f]
846
- try:
847
- results[idx] = f.result()
848
- except Exception as e:
849
- results[idx] = f"✖️ 搜索 `{' '.join(query_groups[idx])}` 失败: {e}"
850
- print("\n\n---\n\n".join(results))
851
-
852
- elif args.command == "search-method":
853
- query_groups = _split_batch_queries(args.query)
854
-
855
- def _msearch_one(q):
856
- return api.search_methods(
857
- q, args.page, args.page_size,
858
- match_all_terms=args.all,
859
- class_prefixes=args.class_prefix,
860
- class_regex=args.class_regex,
861
- class_keywords=args.kind,
862
- )
863
-
864
- if len(query_groups) == 1:
865
- results = [_msearch_one(query_groups[0])]
866
- else:
867
- results = [None] * len(query_groups)
868
- with ThreadPoolExecutor(max_workers=min(len(query_groups), 4)) as pool:
869
- fut = {pool.submit(_msearch_one, q): i for i, q in enumerate(query_groups)}
870
- for f in as_completed(fut):
871
- idx = fut[f]
872
- try:
873
- results[idx] = f.result()
874
- except Exception as e:
875
- results[idx] = f"✖️ 搜索 `{' '.join(query_groups[idx])}` 失败: {e}"
876
- print("\n\n---\n\n".join(results))
877
- elif args.command == "detail":
878
- class_names = [c.strip() for c in args.classname.split(",") if c.strip()]
879
-
880
- def _detail_one(cn):
881
- return api.get_class_details(
882
- cn,
883
- method_filter=args.method,
884
- declared_only=args.declared_only,
885
- compact=args.compact,
886
- )
887
-
888
- if len(class_names) == 1:
889
- results = [_detail_one(class_names[0])]
890
- else:
891
- results = [None] * len(class_names)
892
- with ThreadPoolExecutor(max_workers=min(len(class_names), 4)) as pool:
893
- future_to_idx = {
894
- pool.submit(_detail_one, cn): i
895
- for i, cn in enumerate(class_names)
896
- }
897
- for future in as_completed(future_to_idx):
898
- idx = future_to_idx[future]
899
- try:
900
- results[idx] = future.result()
901
- except Exception as e:
902
- results[idx] = f"✖️ 查询 `{class_names[idx]}` 失败: {e}"
903
-
904
- print("\n\n---\n\n".join(results))
905
- else:
906
- parser.print_help()
907
-
908
-
909
- if __name__ == "__main__":
910
- sys.exit(run_cli(main))