kcode-pi 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -5
- package/docs/DEVELOPMENT.md +2 -0
- package/extensions/kingdee-harness.ts +139 -1
- package/extensions/kingdee-tools.ts +43 -1
- package/package.json +2 -1
- package/skills/kd-check/SKILL.md +1 -2
- package/skills/kd-cosmic-dev/SKILL.md +3 -3
- package/skills/kd-cosmic-review/SKILL.md +2 -2
- package/skills/kd-cosmic-unittest/SKILL.md +2 -2
- package/skills/kd-debug/SKILL.md +1 -2
- package/skills/kd-execute/SKILL.md +2 -2
- package/skills/kd-gen/SKILL.md +2 -1
- package/skills/kd-plan/SKILL.md +3 -3
- package/src/harness/gates.ts +14 -1
- package/src/harness/state.ts +44 -1
- package/src/harness/types.ts +14 -0
- package/src/official/kingdee-skills.ts +60 -13
- package/src/rules/checker.ts +143 -0
- package/src/tools/sdk-signature.ts +309 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/SKILL.md +2 -2
- package/vendor/kingdee-skills/ok-cosmic/SKILL.md +52 -101
- package/vendor/kingdee-skills/ok-cosmic/agents/openai.yaml +4 -4
- package/vendor/kingdee-skills/ok-cosmic/manifest.json +21 -20
- package/vendor/kingdee-skills/ok-cosmic/ok-cosmic-intro.html +1 -1
- package/vendor/kingdee-skills/ok-cosmic/rules/a-layer-rules.json +1 -1
- package/vendor/kingdee-skills/ok-cosmic/rules/anti-patterns.md +2 -2
- package/vendor/kingdee-skills/ok-cosmic/rules/coding-preferences.md +4 -4
- package/vendor/kingdee-skills/ok-cosmic/rules/constraints.md +3 -3
- package/vendor/kingdee-skills/ok-cosmic/rules/decision-matrix.md +8 -8
- package/vendor/kingdee-skills/ok-cosmic/rules/intent-routing.md +1 -1
- package/vendor/kingdee-skills/ok-cosmic/rules/post-check.md +19 -18
- package/vendor/kingdee-skills/ok-ksql/SKILL.md +9 -9
- package/vendor/kingdee-skills/ok-ksql/manifest.json +2 -1
- package/vendor/kingdee-skills/ok-ksql/references/ksql-datafix.md +2 -2
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/pattern-matcher.py +0 -336
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/review-score-calculator.py +0 -121
- package/vendor/kingdee-skills/ok-cosmic/CHANGELOG.md +0 -295
- package/vendor/kingdee-skills/ok-cosmic/README.md +0 -460
- package/vendor/kingdee-skills/ok-cosmic/requirements.txt +0 -2
- package/vendor/kingdee-skills/ok-cosmic/scripts/config_loader.py +0 -204
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-api-knowledge.py +0 -910
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-basedata-query.py +0 -359
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-config-check.py +0 -181
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-extpoints-query.py +0 -389
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-form-metadata.py +0 -856
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-check.py +0 -262
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-lint.py +0 -293
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/__init__.py +0 -2
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/base.py +0 -393
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/resource_check.py +0 -176
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/scene_check.py +0 -375
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/style_check.py +0 -434
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/verify_check.py +0 -36
- package/vendor/kingdee-skills/ok-cosmic/scripts/route_client.py +0 -186
- package/vendor/kingdee-skills/ok-cosmic/scripts/script_utils.py +0 -40
- package/vendor/kingdee-skills/ok-cosmic/scripts/sqlite_cache.py +0 -142
- package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-commons.jar +0 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-features.jar +0 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/setup-mac.sh +0 -18
- package/vendor/kingdee-skills/ok-cosmic/setup/setup-windows.bat +0 -53
- package/vendor/kingdee-skills/ok-cosmic/setup/setup.jar +0 -0
- package/vendor/kingdee-skills/ok-ksql/scripts/ksql_lint.py +0 -363
|
@@ -1,856 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# SPDX-License-Identifier: NOASSERTION
|
|
3
|
-
"""
|
|
4
|
-
cosmic-form-metadata.py — Cosmic form metadata query and cache tool.
|
|
5
|
-
|
|
6
|
-
Usage:
|
|
7
|
-
python3 cosmic-form-metadata.py --config ok-cosmic.json get <formIdOrName>
|
|
8
|
-
python3 cosmic-form-metadata.py --config ok-cosmic.json get <formId1>,<formId2>,<中文名> # batch, auto-detect
|
|
9
|
-
python3 cosmic-form-metadata.py --config ok-cosmic.json get <formId> --fuzzy qty price amount
|
|
10
|
-
python3 cosmic-form-metadata.py --config ok-cosmic.json get <formId> --fuzzy "组织 物料 批号"
|
|
11
|
-
python3 cosmic-form-metadata.py --config ok-cosmic.json get <formId> --fuzzy "数量|金额"
|
|
12
|
-
python3 cosmic-form-metadata.py --config ok-cosmic.json get <formId> --fuzzy status --show-detail
|
|
13
|
-
python3 cosmic-form-metadata.py --config ok-cosmic.json get <formId> --type BaseData
|
|
14
|
-
python3 cosmic-form-metadata.py --config ok-cosmic.json get <formId> --type "combo|check"
|
|
15
|
-
python3 cosmic-form-metadata.py --config ok-cosmic.json get <formId> --type decimal --fuzzy amount
|
|
16
|
-
python3 cosmic-form-metadata.py --config ok-cosmic.json get <formId> --tree
|
|
17
|
-
python3 cosmic-form-metadata.py --config ok-cosmic.json get <formId> --op
|
|
18
|
-
python3 cosmic-form-metadata.py --config ok-cosmic.json get <formId> --op 审核
|
|
19
|
-
python3 cosmic-form-metadata.py --config ok-cosmic.json get <formId> --refresh
|
|
20
|
-
|
|
21
|
-
What it provides:
|
|
22
|
-
1. Form metadata lookup by formId or billName
|
|
23
|
-
2. Local SQLite cache for metadata payloads
|
|
24
|
-
3. Fuzzy field filtering for keys / names
|
|
25
|
-
4. Type-based fuzzy filtering (--type) for field type matching
|
|
26
|
-
5. Optional detail mode for enum mappings and reference types
|
|
27
|
-
|
|
28
|
-
What it does NOT do:
|
|
29
|
-
- It does not query the form_metadata_cache table directly for user-facing output semantics
|
|
30
|
-
- It does not infer field meaning beyond metadata returned by the configured API
|
|
31
|
-
- It does not rebuild the API knowledge graph database
|
|
32
|
-
|
|
33
|
-
Prerequisites:
|
|
34
|
-
- A valid ok-cosmic.json project config
|
|
35
|
-
- A reachable route.apiUrl (`runtime/route`) for cache misses
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
import sys
|
|
39
|
-
import argparse
|
|
40
|
-
import re
|
|
41
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
42
|
-
from typing import Any, Dict, List, Optional
|
|
43
|
-
|
|
44
|
-
from config_loader import load_project_config
|
|
45
|
-
from route_client import RouteClient, unwrap_route_payload
|
|
46
|
-
from sqlite_cache import JsonSqliteCache, resolve_graph_db_path
|
|
47
|
-
from script_utils import FriendlyArgumentParser, run_cli
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _looks_like_meta_payload(value: Any) -> bool:
|
|
51
|
-
return isinstance(value, dict) and (
|
|
52
|
-
"form" in value
|
|
53
|
-
or "formFields" in value
|
|
54
|
-
or "entityFields" in value
|
|
55
|
-
or value.get("code") in ("MULTI_MATCH", "BILL_NOT_FOUND")
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _unwrap_route_payload(data: Any) -> Any:
|
|
60
|
-
return unwrap_route_payload(data, _looks_like_meta_payload)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class MetadataDbCache:
|
|
64
|
-
def __init__(self, db_path: str, ttl: int = 600):
|
|
65
|
-
self._cache = JsonSqliteCache(
|
|
66
|
-
db_path,
|
|
67
|
-
table_name="form_metadata_cache",
|
|
68
|
-
key_column="form_id",
|
|
69
|
-
create_sql="""
|
|
70
|
-
CREATE TABLE IF NOT EXISTS form_metadata_cache (
|
|
71
|
-
form_id TEXT PRIMARY KEY,
|
|
72
|
-
payload TEXT,
|
|
73
|
-
updated_at INTEGER
|
|
74
|
-
)
|
|
75
|
-
""",
|
|
76
|
-
ttl=ttl,
|
|
77
|
-
check_same_thread=False,
|
|
78
|
-
init_error_message="初始化数据库失败",
|
|
79
|
-
write_error_message="写入数据库失败",
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
def get(self, form_id: str) -> Optional[Dict[str, Any]]:
|
|
83
|
-
return self._cache.get(form_id)
|
|
84
|
-
|
|
85
|
-
def set(self, form_id: str, payload: Dict[str, Any]):
|
|
86
|
-
self._cache.set_payload(form_id, payload)
|
|
87
|
-
|
|
88
|
-
def remove(self, form_id: str):
|
|
89
|
-
self._cache.remove(form_id)
|
|
90
|
-
|
|
91
|
-
def close(self):
|
|
92
|
-
"""显式关闭数据库连接。"""
|
|
93
|
-
self._cache.close()
|
|
94
|
-
|
|
95
|
-
def __del__(self):
|
|
96
|
-
self.close()
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
class FormMetadata:
|
|
100
|
-
def __init__(self, config: Dict[str, Any], debug: bool = False):
|
|
101
|
-
self.debug = debug
|
|
102
|
-
route_config = config.get("route", {})
|
|
103
|
-
if not isinstance(route_config, dict):
|
|
104
|
-
route_config = {}
|
|
105
|
-
self.db_path = resolve_graph_db_path(
|
|
106
|
-
config,
|
|
107
|
-
"未配置 graph.dbPath,请在 ok-cosmic.json 中指定数据库全路径",
|
|
108
|
-
)
|
|
109
|
-
self.cache = MetadataDbCache(self.db_path)
|
|
110
|
-
self.route_client = RouteClient(
|
|
111
|
-
route_config,
|
|
112
|
-
debug=debug,
|
|
113
|
-
missing_message=(
|
|
114
|
-
"未配置表单元数据查询 API。请在 ok-cosmic.json 的 route.apiUrl 中配置统一路由,"
|
|
115
|
-
"或设置 COSMIC_ROUTE_API / COSMIC_RUNTIME_ROUTE_API 环境变量。"
|
|
116
|
-
),
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
def _log_debug(self, msg: str):
|
|
120
|
-
if self.debug:
|
|
121
|
-
print(f" (DEBUG) {msg}", file=sys.stderr)
|
|
122
|
-
|
|
123
|
-
def _post(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
124
|
-
return self.route_client.post(payload)
|
|
125
|
-
|
|
126
|
-
@staticmethod
|
|
127
|
-
def _normalize_operates(raw_operates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
128
|
-
"""
|
|
129
|
-
兼容旧版 `buttons` 与新版 `operateMetas` 结构:
|
|
130
|
-
- buttons: {name, key, ...}
|
|
131
|
-
- operateMetas: {opName, opKey, opType}
|
|
132
|
-
统一输出为: {name, key, type}
|
|
133
|
-
"""
|
|
134
|
-
normalized: List[Dict[str, Any]] = []
|
|
135
|
-
for op in raw_operates:
|
|
136
|
-
if not isinstance(op, dict):
|
|
137
|
-
continue
|
|
138
|
-
key = op.get("key") or op.get("opKey")
|
|
139
|
-
name = op.get("name") or op.get("opName")
|
|
140
|
-
op_type = op.get("type") or op.get("opType")
|
|
141
|
-
if not key and not name:
|
|
142
|
-
continue
|
|
143
|
-
normalized.append({
|
|
144
|
-
"name": name or "-",
|
|
145
|
-
"key": key or "-",
|
|
146
|
-
"type": op_type or "-"
|
|
147
|
-
})
|
|
148
|
-
return normalized
|
|
149
|
-
|
|
150
|
-
@staticmethod
|
|
151
|
-
def _field_sort_key(field: Dict[str, Any], sort_by: str = "key") -> tuple:
|
|
152
|
-
value = str(field.get(sort_by, "") or "").lower()
|
|
153
|
-
key = str(field.get("key", "") or "").lower()
|
|
154
|
-
name = str(field.get("name", "") or "").lower()
|
|
155
|
-
node_type = str(field.get("type", "") or "").lower()
|
|
156
|
-
db_key = str(field.get("dbKey", "") or "").lower()
|
|
157
|
-
return (value, key, name, node_type, db_key)
|
|
158
|
-
|
|
159
|
-
@classmethod
|
|
160
|
-
def _sort_fields(cls, fields: List[Dict[str, Any]], sort_by: str = "key") -> List[Dict[str, Any]]:
|
|
161
|
-
return sorted(fields, key=lambda f: cls._field_sort_key(f, sort_by=sort_by))
|
|
162
|
-
|
|
163
|
-
@classmethod
|
|
164
|
-
def _build_tree_lines(
|
|
165
|
-
cls,
|
|
166
|
-
fields: List[Dict[str, Any]],
|
|
167
|
-
filter_patterns: Optional[List[str]] = None,
|
|
168
|
-
sort_by: str = "key"
|
|
169
|
-
) -> List[str]:
|
|
170
|
-
def is_hit(f: Dict[str, Any]) -> bool:
|
|
171
|
-
if not filter_patterns:
|
|
172
|
-
return True
|
|
173
|
-
target_text = (
|
|
174
|
-
str(f.get("key", ""))
|
|
175
|
-
+ "|"
|
|
176
|
-
+ str(f.get("name", ""))
|
|
177
|
-
+ "|"
|
|
178
|
-
+ str(f.get("type", ""))
|
|
179
|
-
+ "|"
|
|
180
|
-
+ str(f.get("dbKey", ""))
|
|
181
|
-
+ "|"
|
|
182
|
-
+ str(f.get("refType", ""))
|
|
183
|
-
).lower()
|
|
184
|
-
for p in filter_patterns:
|
|
185
|
-
try:
|
|
186
|
-
if re.search(p, target_text, re.IGNORECASE):
|
|
187
|
-
return True
|
|
188
|
-
except Exception:
|
|
189
|
-
if p.lower() in target_text:
|
|
190
|
-
return True
|
|
191
|
-
return False
|
|
192
|
-
|
|
193
|
-
by_key: Dict[str, Dict[str, Any]] = {}
|
|
194
|
-
for f in fields:
|
|
195
|
-
key = str(f.get("key", "")).strip()
|
|
196
|
-
if key and key not in by_key:
|
|
197
|
-
by_key[key] = f
|
|
198
|
-
|
|
199
|
-
if not by_key:
|
|
200
|
-
return []
|
|
201
|
-
|
|
202
|
-
children: Dict[Optional[str], List[str]] = {}
|
|
203
|
-
for k, f in by_key.items():
|
|
204
|
-
parent = f.get("parentKey")
|
|
205
|
-
parent_key = str(parent).strip() if parent is not None else None
|
|
206
|
-
if parent_key == "":
|
|
207
|
-
parent_key = None
|
|
208
|
-
children.setdefault(parent_key, []).append(k)
|
|
209
|
-
|
|
210
|
-
for parent_key, ks in children.items():
|
|
211
|
-
children[parent_key] = sorted(
|
|
212
|
-
ks,
|
|
213
|
-
key=lambda k: cls._field_sort_key(by_key.get(k, {}), sort_by=sort_by)
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
include_keys = set(by_key.keys())
|
|
217
|
-
if filter_patterns:
|
|
218
|
-
include_keys = {k for k, f in by_key.items() if is_hit(f)}
|
|
219
|
-
# 补齐祖先节点,便于阅读路径
|
|
220
|
-
queue = list(include_keys)
|
|
221
|
-
while queue:
|
|
222
|
-
cur = queue.pop()
|
|
223
|
-
parent = by_key.get(cur, {}).get("parentKey")
|
|
224
|
-
parent_key = str(parent).strip() if parent is not None else None
|
|
225
|
-
if parent_key and parent_key in by_key and parent_key not in include_keys:
|
|
226
|
-
include_keys.add(parent_key)
|
|
227
|
-
queue.append(parent_key)
|
|
228
|
-
|
|
229
|
-
def fmt_node(key: str) -> str:
|
|
230
|
-
f = by_key[key]
|
|
231
|
-
name = f.get("name", "-")
|
|
232
|
-
node_type = f.get("type", "-")
|
|
233
|
-
db_key = f.get("dbKey", "-")
|
|
234
|
-
return f"{name} (`{key}`) [{node_type}] dbKey=`{db_key}`"
|
|
235
|
-
|
|
236
|
-
lines: List[str] = []
|
|
237
|
-
visited: set = set()
|
|
238
|
-
|
|
239
|
-
def walk(node_key: str, prefix: str, is_last: bool):
|
|
240
|
-
if node_key in visited or node_key not in include_keys:
|
|
241
|
-
return
|
|
242
|
-
visited.add(node_key)
|
|
243
|
-
branch = "`- " if is_last else "|- "
|
|
244
|
-
lines.append(f"{prefix}{branch}{fmt_node(node_key)}")
|
|
245
|
-
child_keys = [ck for ck in children.get(node_key, []) if ck in include_keys]
|
|
246
|
-
for i, ck in enumerate(child_keys):
|
|
247
|
-
next_prefix = prefix + (" " if is_last else "| ")
|
|
248
|
-
walk(ck, next_prefix, i == len(child_keys) - 1)
|
|
249
|
-
|
|
250
|
-
roots = []
|
|
251
|
-
for k in sorted(include_keys, key=lambda x: cls._field_sort_key(by_key.get(x, {}), sort_by=sort_by)):
|
|
252
|
-
parent = by_key.get(k, {}).get("parentKey")
|
|
253
|
-
parent_key = str(parent).strip() if parent is not None else None
|
|
254
|
-
if not parent_key or parent_key not in include_keys:
|
|
255
|
-
roots.append(k)
|
|
256
|
-
|
|
257
|
-
roots = sorted(set(roots), key=lambda x: cls._field_sort_key(by_key.get(x, {}), sort_by=sort_by))
|
|
258
|
-
for i, rk in enumerate(roots):
|
|
259
|
-
walk(rk, "", i == len(roots) - 1)
|
|
260
|
-
|
|
261
|
-
return lines
|
|
262
|
-
|
|
263
|
-
def get_meta_fields(
|
|
264
|
-
self,
|
|
265
|
-
formId: Optional[str] = None,
|
|
266
|
-
billName: Optional[str] = None,
|
|
267
|
-
filter_patterns: Optional[List[str]] = None,
|
|
268
|
-
raw_patterns: Optional[List[str]] = None,
|
|
269
|
-
type_patterns: Optional[List[str]] = None,
|
|
270
|
-
show_detail: bool = False,
|
|
271
|
-
tree_view: bool = False,
|
|
272
|
-
sql_mode: bool = False,
|
|
273
|
-
sort_by: str = "key",
|
|
274
|
-
view: str = "all",
|
|
275
|
-
op_mode: bool = False,
|
|
276
|
-
op_patterns: Optional[List[str]] = None
|
|
277
|
-
) -> str:
|
|
278
|
-
if not formId and not billName:
|
|
279
|
-
return "✖️ 错误: 必须提供 formId 或 billName"
|
|
280
|
-
|
|
281
|
-
target_payload = None
|
|
282
|
-
if formId:
|
|
283
|
-
target_payload = self.cache.get(formId)
|
|
284
|
-
if target_payload: self._log_debug(f"命中缓存 (FormId: {formId})")
|
|
285
|
-
|
|
286
|
-
if not target_payload:
|
|
287
|
-
self._log_debug("数据库无有效缓存,发起远程全量拉取...")
|
|
288
|
-
# 始终拉取全量用于本地缓存
|
|
289
|
-
payload = {
|
|
290
|
-
"data": {
|
|
291
|
-
"type": "meta",
|
|
292
|
-
"reqData": {
|
|
293
|
-
"entityId": formId or "",
|
|
294
|
-
"formId": formId or "",
|
|
295
|
-
"billName": billName or "",
|
|
296
|
-
"full": True,
|
|
297
|
-
},
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
try:
|
|
301
|
-
resp = self._post(payload)
|
|
302
|
-
except RuntimeError as e:
|
|
303
|
-
return f"✖️ {e}"
|
|
304
|
-
if not resp.get("status"):
|
|
305
|
-
return f"✖️ 接口请求失败: {resp.get('message', '未知错误')}"
|
|
306
|
-
|
|
307
|
-
data = _unwrap_route_payload(resp.get("data", {}))
|
|
308
|
-
if data.get("code") in ("MULTI_MATCH", "BILL_NOT_FOUND"):
|
|
309
|
-
msg = data.get("message", "单据未找到")
|
|
310
|
-
cand_str = "\n".join([f"- {c.get('formName') or c.get('name') or '-'} (`{c.get('formId') or c.get('id') or '-'}`)" for c in data.get("candidates", [])])
|
|
311
|
-
return f"✖️ {msg}\n{cand_str}"
|
|
312
|
-
|
|
313
|
-
target_payload = data
|
|
314
|
-
real_form_id = data.get("form", {}).get("formId")
|
|
315
|
-
if real_form_id:
|
|
316
|
-
self.cache.set(real_form_id, data)
|
|
317
|
-
|
|
318
|
-
# 数据提取
|
|
319
|
-
form = target_payload.get("form", {})
|
|
320
|
-
form_fields = target_payload.get("formFields") or []
|
|
321
|
-
entity_fields = target_payload.get("entityFields") or []
|
|
322
|
-
raw_operates = (
|
|
323
|
-
target_payload.get("operateMetas")
|
|
324
|
-
or target_payload.get("buttons")
|
|
325
|
-
or []
|
|
326
|
-
)
|
|
327
|
-
buttons = self._normalize_operates(raw_operates)
|
|
328
|
-
|
|
329
|
-
# ── --op 模式:只输出操作按钮 ──
|
|
330
|
-
if op_mode:
|
|
331
|
-
_form_name = form.get('formName') or form.get('name') or form.get('title') or '-'
|
|
332
|
-
_form_id = form.get('formId') or form.get('id') or form.get('key') or '-'
|
|
333
|
-
md = [f"## [Op] 操作查询: {_form_name} ({_form_id})"]
|
|
334
|
-
|
|
335
|
-
if op_patterns:
|
|
336
|
-
def _op_hit(b):
|
|
337
|
-
target = (str(b.get('name', '')) + '|' + str(b.get('key', '')) + '|' + str(b.get('type', ''))).lower()
|
|
338
|
-
for p in op_patterns:
|
|
339
|
-
try:
|
|
340
|
-
if re.search(p, target, re.IGNORECASE):
|
|
341
|
-
return True
|
|
342
|
-
except Exception:
|
|
343
|
-
if p.lower() in target:
|
|
344
|
-
return True
|
|
345
|
-
return False
|
|
346
|
-
matched = [b for b in buttons if _op_hit(b)]
|
|
347
|
-
md.append(f"筛选: `{', '.join(op_patterns)}`,匹配 {len(matched)}/{len(buttons)} 个操作\n")
|
|
348
|
-
else:
|
|
349
|
-
matched = buttons
|
|
350
|
-
md.append(f"共 {len(matched)} 个操作\n")
|
|
351
|
-
|
|
352
|
-
if matched:
|
|
353
|
-
md.append("| 名称 | 标识 (opKey) | 类型 (opType) |")
|
|
354
|
-
md.append("| :--- | :--- | :--- |")
|
|
355
|
-
for b in matched:
|
|
356
|
-
md.append(f"| {b.get('name')} | `{b.get('key')}` | {b.get('type')} |")
|
|
357
|
-
else:
|
|
358
|
-
md.append("> 未找到匹配的操作按钮")
|
|
359
|
-
return "\n".join(md)
|
|
360
|
-
|
|
361
|
-
# 过滤与搜索逻辑
|
|
362
|
-
def is_hit(f):
|
|
363
|
-
if not filter_patterns: return True
|
|
364
|
-
target_text = (
|
|
365
|
-
str(f.get('key', ''))
|
|
366
|
-
+ "|"
|
|
367
|
-
+ str(f.get('name', ''))
|
|
368
|
-
+ "|"
|
|
369
|
-
+ str(f.get('type', ''))
|
|
370
|
-
).lower()
|
|
371
|
-
for p in filter_patterns:
|
|
372
|
-
try:
|
|
373
|
-
if re.search(p, target_text, re.IGNORECASE): return True
|
|
374
|
-
except Exception:
|
|
375
|
-
if p.lower() in target_text: return True
|
|
376
|
-
return False
|
|
377
|
-
|
|
378
|
-
def is_type_hit(f):
|
|
379
|
-
"""按 type 字段进行模糊匹配筛选。"""
|
|
380
|
-
if not type_patterns:
|
|
381
|
-
return True
|
|
382
|
-
field_type = str(f.get('type', '')).lower()
|
|
383
|
-
for p in type_patterns:
|
|
384
|
-
try:
|
|
385
|
-
if re.search(p, field_type, re.IGNORECASE):
|
|
386
|
-
return True
|
|
387
|
-
except Exception:
|
|
388
|
-
if p.lower() in field_type:
|
|
389
|
-
return True
|
|
390
|
-
return False
|
|
391
|
-
|
|
392
|
-
def is_combined_hit(f):
|
|
393
|
-
"""同时满足 fuzzy 和 type 两个筛选条件。"""
|
|
394
|
-
return is_hit(f) and is_type_hit(f)
|
|
395
|
-
|
|
396
|
-
# 应用过滤
|
|
397
|
-
biz_form_fields = self._sort_fields(form_fields, sort_by=sort_by)
|
|
398
|
-
biz_entity_fields = self._sort_fields(entity_fields, sort_by=sort_by)
|
|
399
|
-
|
|
400
|
-
def get_match_score(f):
|
|
401
|
-
if not filter_patterns: return 0
|
|
402
|
-
k = str(f.get('key', '')).lower()
|
|
403
|
-
n = str(f.get('name', '')).lower()
|
|
404
|
-
t = str(f.get('type', '')).lower()
|
|
405
|
-
db = str(f.get('dbKey', '')).lower()
|
|
406
|
-
ref = str(f.get('refType', '')).lower()
|
|
407
|
-
|
|
408
|
-
score = 99
|
|
409
|
-
for p in filter_patterns:
|
|
410
|
-
p_lower = p.lower()
|
|
411
|
-
if p_lower == k or p_lower == n:
|
|
412
|
-
return 0
|
|
413
|
-
if k.startswith(p_lower) or n.startswith(p_lower):
|
|
414
|
-
score = min(score, 1)
|
|
415
|
-
elif p_lower in k or p_lower in n:
|
|
416
|
-
score = min(score, 2)
|
|
417
|
-
else:
|
|
418
|
-
try:
|
|
419
|
-
if re.search(p_lower, f"{k}|{n}", re.IGNORECASE):
|
|
420
|
-
score = min(score, 3)
|
|
421
|
-
elif p_lower in t or p_lower in db or p_lower in ref or re.search(p_lower, f"{t}|{db}|{ref}", re.IGNORECASE):
|
|
422
|
-
score = min(score, 4)
|
|
423
|
-
except Exception:
|
|
424
|
-
pass
|
|
425
|
-
return score
|
|
426
|
-
|
|
427
|
-
display_form_fields = sorted([f for f in biz_form_fields if is_combined_hit(f)], key=get_match_score)
|
|
428
|
-
display_entity_fields = sorted([f for f in biz_entity_fields if is_combined_hit(f)], key=get_match_score)
|
|
429
|
-
display_buttons = sorted([b for b in buttons if is_combined_hit(b)], key=get_match_score)
|
|
430
|
-
|
|
431
|
-
# ── 三级降级机制 ────────────────────────────────────────
|
|
432
|
-
_used_fallback = False
|
|
433
|
-
_fallback_hint = ""
|
|
434
|
-
_no_results = lambda: not display_form_fields and not display_entity_fields and not display_buttons
|
|
435
|
-
|
|
436
|
-
# 降级 1: 规范化后查不到 → 用原始输入重查
|
|
437
|
-
# 场景: "物料 编码" 被拆成 ['物料','编码'],但字段名确实是 "物料 编码"
|
|
438
|
-
if (filter_patterns and raw_patterns
|
|
439
|
-
and filter_patterns != raw_patterns
|
|
440
|
-
and _no_results()):
|
|
441
|
-
filter_patterns = raw_patterns
|
|
442
|
-
display_form_fields = sorted([f for f in biz_form_fields if is_combined_hit(f)], key=get_match_score)
|
|
443
|
-
display_entity_fields = sorted([f for f in biz_entity_fields if is_combined_hit(f)], key=get_match_score)
|
|
444
|
-
display_buttons = sorted([b for b in buttons if is_combined_hit(b)], key=get_match_score)
|
|
445
|
-
if not _no_results():
|
|
446
|
-
_used_fallback = True
|
|
447
|
-
_fallback_hint = f"已降级为原始输入重查: `{' '.join(raw_patterns)}`"
|
|
448
|
-
|
|
449
|
-
# 降级 2: 仍查不到 → 转义为纯文本匹配
|
|
450
|
-
# 场景: "数量(基本)" 括号被当正则 / "C++标记" 加号被当量词
|
|
451
|
-
if filter_patterns and _no_results():
|
|
452
|
-
escaped = [re.escape(p) for p in (raw_patterns or filter_patterns)]
|
|
453
|
-
if escaped != filter_patterns:
|
|
454
|
-
filter_patterns = escaped
|
|
455
|
-
display_form_fields = sorted([f for f in biz_form_fields if is_combined_hit(f)], key=get_match_score)
|
|
456
|
-
display_entity_fields = sorted([f for f in biz_entity_fields if is_combined_hit(f)], key=get_match_score)
|
|
457
|
-
display_buttons = sorted([b for b in buttons if is_combined_hit(b)], key=get_match_score)
|
|
458
|
-
if not _no_results():
|
|
459
|
-
_used_fallback = True
|
|
460
|
-
_src = raw_patterns or filter_patterns
|
|
461
|
-
_fallback_hint = f"已降级为纯文本匹配: `{' '.join(_src)}`"
|
|
462
|
-
|
|
463
|
-
view = (view or "all").strip()
|
|
464
|
-
show_form = view in ("form", "all")
|
|
465
|
-
show_entity = view in ("entity", "all")
|
|
466
|
-
show_operate = view in ("operate", "all")
|
|
467
|
-
|
|
468
|
-
selected_biz_form_fields = biz_form_fields if show_form else []
|
|
469
|
-
selected_biz_entity_fields = biz_entity_fields if show_entity else []
|
|
470
|
-
selected_display_form_fields = display_form_fields if show_form else []
|
|
471
|
-
selected_display_entity_fields = display_entity_fields if show_entity else []
|
|
472
|
-
selected_display_buttons = display_buttons if show_operate else []
|
|
473
|
-
selected_buttons = buttons if show_operate else []
|
|
474
|
-
|
|
475
|
-
_form_name = form.get('formName') or form.get('name') or form.get('title') or '-'
|
|
476
|
-
_form_id = form.get('formId') or form.get('id') or form.get('key') or '-'
|
|
477
|
-
_db_name = form.get('dbName') or target_payload.get('dbName') or '-'
|
|
478
|
-
md = [f"## [Form] 单据: {_form_name} ({_form_id})"]
|
|
479
|
-
md.append(
|
|
480
|
-
"**模型信息**: "
|
|
481
|
-
f"dbName=`{_db_name}`, "
|
|
482
|
-
f"dbTableKey=`{form.get('dbTableKey', '-')}`, "
|
|
483
|
-
f"dbRoute=`{form.get('dbRoute', '-')}`, "
|
|
484
|
-
f"modelType=`{form.get('modelType', '-')}`"
|
|
485
|
-
)
|
|
486
|
-
md.append(f"**视图**: `{view}`")
|
|
487
|
-
md.append(f"**元数据状态**: 已缓存本地 (DB驱动)")
|
|
488
|
-
if _used_fallback:
|
|
489
|
-
md.append(f"> [Warn] {_fallback_hint}")
|
|
490
|
-
md.append("")
|
|
491
|
-
|
|
492
|
-
# 根据是否有过滤词切换视图
|
|
493
|
-
_has_filter = bool(filter_patterns or type_patterns)
|
|
494
|
-
_filter_desc_parts = []
|
|
495
|
-
if filter_patterns:
|
|
496
|
-
_filter_desc_parts.append(f"fuzzy: {', '.join(filter_patterns)}")
|
|
497
|
-
if type_patterns:
|
|
498
|
-
_filter_desc_parts.append(f"type: {', '.join(type_patterns)}")
|
|
499
|
-
_filter_desc = ';'.join(_filter_desc_parts)
|
|
500
|
-
|
|
501
|
-
if tree_view:
|
|
502
|
-
all_biz = []
|
|
503
|
-
seen = set()
|
|
504
|
-
for f in selected_biz_form_fields + selected_biz_entity_fields:
|
|
505
|
-
k = f.get("key")
|
|
506
|
-
if k and k not in seen:
|
|
507
|
-
all_biz.append(f)
|
|
508
|
-
seen.add(k)
|
|
509
|
-
|
|
510
|
-
# tree 视图下 type 过滤: 先筛后建树
|
|
511
|
-
if type_patterns:
|
|
512
|
-
all_biz = [f for f in all_biz if is_type_hit(f)]
|
|
513
|
-
tree_lines = self._build_tree_lines(all_biz, filter_patterns=filter_patterns, sort_by=sort_by) if all_biz else []
|
|
514
|
-
if _has_filter:
|
|
515
|
-
md.append(f"### [Tree] 字段树 (按条件筛选: {_filter_desc},排序: {sort_by})")
|
|
516
|
-
else:
|
|
517
|
-
md.append(f"### [Tree] 字段树 (按 parentKey,排序: {sort_by})")
|
|
518
|
-
|
|
519
|
-
if tree_lines:
|
|
520
|
-
md.extend(tree_lines)
|
|
521
|
-
else:
|
|
522
|
-
md.append("> 未找到匹配字段(当前 view 可能不包含字段类型)")
|
|
523
|
-
|
|
524
|
-
if selected_display_buttons:
|
|
525
|
-
md.append("\n### [Op] 操作按钮 (匹配)")
|
|
526
|
-
md.append(", ".join([f"{b.get('name')}(`{b.get('key')}`/{b.get('type')})" for b in selected_display_buttons]))
|
|
527
|
-
elif _has_filter:
|
|
528
|
-
md.append(f"### [Detail] 字段详情 (按条件筛选: {_filter_desc},排序: {sort_by})")
|
|
529
|
-
|
|
530
|
-
all_fields_by_key = {str(f.get('key', '')).lower(): f for f in form_fields + entity_fields}
|
|
531
|
-
|
|
532
|
-
def get_entity_info(f):
|
|
533
|
-
parent_key = f.get('parentKey')
|
|
534
|
-
if not parent_key:
|
|
535
|
-
return "表头"
|
|
536
|
-
parent_key_lower = str(parent_key).lower()
|
|
537
|
-
parent = all_fields_by_key.get(parent_key_lower)
|
|
538
|
-
if not parent:
|
|
539
|
-
return f"未知 (`{parent_key}`)"
|
|
540
|
-
ptype = str(parent.get('type', '')).lower()
|
|
541
|
-
if 'subentry' in ptype:
|
|
542
|
-
grandparent_key = parent.get('parentKey')
|
|
543
|
-
if grandparent_key:
|
|
544
|
-
return f"子表体 (`{grandparent_key}` -> `{parent_key}`)"
|
|
545
|
-
return f"子表体 (`{parent_key}`)"
|
|
546
|
-
elif 'entry' in ptype:
|
|
547
|
-
return f"表体 (`{parent_key}`)"
|
|
548
|
-
else:
|
|
549
|
-
return f"容器 (`{parent_key}`)"
|
|
550
|
-
|
|
551
|
-
def get_db_table(f):
|
|
552
|
-
parent_key = f.get('parentKey')
|
|
553
|
-
if not parent_key:
|
|
554
|
-
ftype = str(f.get('type', '')).lower()
|
|
555
|
-
if 'entry' in ftype and f.get('dbKey'):
|
|
556
|
-
return f.get('dbKey')
|
|
557
|
-
return form.get('dbTableKey') or '-'
|
|
558
|
-
parent = all_fields_by_key.get(str(parent_key).lower())
|
|
559
|
-
return parent.get('dbKey') or '-' if parent else '-'
|
|
560
|
-
|
|
561
|
-
def get_table_entity_info(f):
|
|
562
|
-
parent_key = f.get('parentKey')
|
|
563
|
-
ftype = str(f.get('type', '')).lower()
|
|
564
|
-
if not parent_key and 'subentry' in ftype:
|
|
565
|
-
return f"子表体 (`{f.get('key')}`)"
|
|
566
|
-
if not parent_key and 'entry' in ftype:
|
|
567
|
-
return f"表体 (`{f.get('key')}`)"
|
|
568
|
-
return get_entity_info(f)
|
|
569
|
-
|
|
570
|
-
if selected_display_form_fields or selected_display_entity_fields:
|
|
571
|
-
if sql_mode:
|
|
572
|
-
md.append("### [SQL] 表所在数据库")
|
|
573
|
-
md.append(f"> dbName 来自 FormMeta;分录/子分录表一般与单头表位于同一数据库 `{_db_name}`。")
|
|
574
|
-
md.append("| 所属实体 | 表名 (dbTableName) | 数据库名 (dbName) |")
|
|
575
|
-
md.append("| :--- | :--- | :--- |")
|
|
576
|
-
seen_tables = set()
|
|
577
|
-
for tf in selected_display_form_fields + selected_display_entity_fields:
|
|
578
|
-
table_entity = get_table_entity_info(tf)
|
|
579
|
-
table_name = get_db_table(tf)
|
|
580
|
-
table_sig = (table_entity, table_name)
|
|
581
|
-
if table_sig in seen_tables:
|
|
582
|
-
continue
|
|
583
|
-
md.append(f"| {table_entity} | `{table_name}` | `{_db_name}` |")
|
|
584
|
-
seen_tables.add(table_sig)
|
|
585
|
-
md.append("")
|
|
586
|
-
md.append("### [SQL] 字段与数据库字段")
|
|
587
|
-
md.append("| 名称 | 标识 (Key) | 所属实体 | 表名 (dbTableName) | 数据库字段 (dbKey) |")
|
|
588
|
-
md.append("| :--- | :--- | :--- | :--- | :--- |")
|
|
589
|
-
elif show_detail:
|
|
590
|
-
md.append("| 名称 | 标识 (Key) | 类型 | 所属实体 | 详情 (枚举/基础资料引用) |")
|
|
591
|
-
md.append("| :--- | :--- | :--- | :--- | :--- |")
|
|
592
|
-
else:
|
|
593
|
-
md.append("| 名称 | 标识 (Key) | 类型 | 所属实体 | 附加信息 (Ext) |")
|
|
594
|
-
md.append("| :--- | :--- | :--- | :--- | :--- |")
|
|
595
|
-
|
|
596
|
-
# 优先显示表单字段,再显示实体字段(去重)
|
|
597
|
-
seen_keys = set()
|
|
598
|
-
for f in selected_display_form_fields + selected_display_entity_fields:
|
|
599
|
-
if f.get('key') not in seen_keys:
|
|
600
|
-
ent_info = get_entity_info(f)
|
|
601
|
-
db_key = f.get('dbKey', '-')
|
|
602
|
-
|
|
603
|
-
if sql_mode:
|
|
604
|
-
db_table = get_db_table(f)
|
|
605
|
-
md.append(f"| {f.get('name')} | `{f.get('key')}` | {ent_info} | `{db_table}` | `{db_key}` |")
|
|
606
|
-
elif show_detail:
|
|
607
|
-
detail_parts = []
|
|
608
|
-
ext_map = f.get('extMap')
|
|
609
|
-
ref_type = f.get('refType')
|
|
610
|
-
key = f.get('key')
|
|
611
|
-
ftype = str(f.get('type', ''))
|
|
612
|
-
if ext_map:
|
|
613
|
-
detail_parts.append("枚举: " + ", ".join([f"{k}:{v}" for k, v in ext_map.items()]))
|
|
614
|
-
if ref_type:
|
|
615
|
-
if ftype == 'BasedataPropField':
|
|
616
|
-
detail_parts.append(f"[Note] 取值路径: `{ref_type}`(禁止用控件标识 `{f.get('key')}` 取值)")
|
|
617
|
-
else:
|
|
618
|
-
detail_parts.append(f"基础资料引用: `{ref_type}`")
|
|
619
|
-
if ftype == 'LargeTextField' and key:
|
|
620
|
-
detail_parts.append(f"[Note] 完整取值: `{key}_tag`(LargeTextField 大文本禁止只用 `{key}` 获取完整内容)")
|
|
621
|
-
detail_str = ";".join(detail_parts) if detail_parts else "-"
|
|
622
|
-
md.append(f"| {f.get('name')} | `{f.get('key')}` | {f.get('type')} | {ent_info} | {detail_str} |")
|
|
623
|
-
else:
|
|
624
|
-
ext_map = f.get('extMap')
|
|
625
|
-
ref_type = f.get('refType')
|
|
626
|
-
key = f.get('key')
|
|
627
|
-
ftype = str(f.get('type', ''))
|
|
628
|
-
ext_parts = []
|
|
629
|
-
if ext_map:
|
|
630
|
-
ext_parts.append(", ".join([f"{k}:{v}" for k, v in ext_map.items()]))
|
|
631
|
-
if ref_type:
|
|
632
|
-
if ftype == 'BasedataPropField':
|
|
633
|
-
ext_parts.append(f"[Note] 取值路径:`{ref_type}`(禁用`{f.get('key')}`)")
|
|
634
|
-
else:
|
|
635
|
-
ext_parts.append(f"ref:`{ref_type}`")
|
|
636
|
-
if ftype == 'LargeTextField' and key:
|
|
637
|
-
ext_parts.append(f"[Note] 完整取值:`{key}_tag`(大文本禁用`{key}`取完整内容)")
|
|
638
|
-
ext_str = ";".join(ext_parts) if ext_parts else "-"
|
|
639
|
-
md.append(f"| {f.get('name')} | `{f.get('key')}` | {f.get('type')} | {ent_info} | {ext_str} |")
|
|
640
|
-
seen_keys.add(f.get('key'))
|
|
641
|
-
else:
|
|
642
|
-
md.append("> 当前 view 不包含字段类型或无匹配字段")
|
|
643
|
-
|
|
644
|
-
if selected_display_buttons:
|
|
645
|
-
md.append("\n### [Op] 操作按钮 (匹配)")
|
|
646
|
-
md.append(", ".join([f"{b.get('name')}(`{b.get('key')}`/{b.get('type')})" for b in selected_display_buttons]))
|
|
647
|
-
else:
|
|
648
|
-
# 概览模式:只输出紧凑的 Key-Name 映射
|
|
649
|
-
md.append(f"### [List] 字段概览 (共 {len(selected_biz_form_fields) + len(selected_biz_entity_fields)} 个字段,排序: {sort_by})")
|
|
650
|
-
# 合并展示
|
|
651
|
-
all_biz = []
|
|
652
|
-
seen = set()
|
|
653
|
-
for f in selected_biz_form_fields + selected_biz_entity_fields:
|
|
654
|
-
if f.get('key') not in seen:
|
|
655
|
-
all_biz.append(f)
|
|
656
|
-
seen.add(f.get('key'))
|
|
657
|
-
|
|
658
|
-
chunk_size = 3
|
|
659
|
-
for i in range(0, min(len(all_biz), 120), chunk_size):
|
|
660
|
-
chunk = all_biz[i:i+chunk_size]
|
|
661
|
-
line = " ".join([f"• {f.get('name')}: `{f.get('key')}`" for f in chunk])
|
|
662
|
-
md.append(line)
|
|
663
|
-
|
|
664
|
-
if len(all_biz) > 120:
|
|
665
|
-
md.append(f"\n> *提示: 字段较多已截断。本地已缓存全量,请传入关键词(支持正则)获取特定字段详情。*")
|
|
666
|
-
|
|
667
|
-
if selected_buttons:
|
|
668
|
-
md.append("\n### [Op] 全部操作按钮")
|
|
669
|
-
md.append(", ".join([f"{b.get('name')}(`{b.get('key')}`/{b.get('type')})" for b in selected_buttons]))
|
|
670
|
-
|
|
671
|
-
return "\n".join(md)
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
_RE_META = re.compile(r'[|*+?\[\]()\\{}^$]')
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
def _normalize_fuzzy_patterns(raw_patterns: List[str]) -> List[str]:
|
|
678
|
-
"""
|
|
679
|
-
智能拆分 fuzzy 参数,兼容各种 AI/Agent 的传参习惯:
|
|
680
|
-
--fuzzy 组织 物料 批号 → ['组织', '物料', '批号'] (nargs 原生)
|
|
681
|
-
--fuzzy "组织 物料 批号" → ['组织', '物料', '批号'] (引号包裹,按空格拆)
|
|
682
|
-
--fuzzy "数量|金额" → ['数量|金额'] (正则,保持原样)
|
|
683
|
-
--fuzzy "qty.*amount" → ['qty.*amount'] (正则,保持原样)
|
|
684
|
-
--fuzzy "组织" "物料" → ['组织', '物料'] (多引号,逐个处理)
|
|
685
|
-
判定规则:含正则元字符 ``|*+?[](){}^$`` 的 token 视为正则,否则按空格拆分。
|
|
686
|
-
"""
|
|
687
|
-
result: List[str] = []
|
|
688
|
-
for p in raw_patterns:
|
|
689
|
-
p = p.strip()
|
|
690
|
-
if not p:
|
|
691
|
-
continue
|
|
692
|
-
if _RE_META.search(p):
|
|
693
|
-
# 含正则元字符,保持原样
|
|
694
|
-
result.append(p)
|
|
695
|
-
else:
|
|
696
|
-
# 纯文本:可能是 "组织 物料 批号" 这样的引号包裹,按空格拆
|
|
697
|
-
result.extend(p.split())
|
|
698
|
-
return [x for x in result if x]
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
def main():
|
|
702
|
-
parser = FriendlyArgumentParser(
|
|
703
|
-
description="Cosmic Form Metadata CLI — 苍穹表单元数据字段查询",
|
|
704
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
705
|
-
epilog="""\
|
|
706
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
707
|
-
推荐用法
|
|
708
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
709
|
-
# 按 formId 查字段,模糊匹配多个关键词(空格分隔)
|
|
710
|
-
%(prog)s --config ok-cosmic.json get <formId> --fuzzy qty price amount
|
|
711
|
-
|
|
712
|
-
# 按中文名称确认基础资料的真实英文标识
|
|
713
|
-
%(prog)s --config ok-cosmic.json get <中文名>
|
|
714
|
-
|
|
715
|
-
# 批量查询:formId 与中文名可混合,逗号分隔,自动识别
|
|
716
|
-
%(prog)s --config ok-cosmic.json get "ap_finapbill,物料,bd_supplier" --fuzzy number
|
|
717
|
-
|
|
718
|
-
# 查枚举映射 / 基础资料引用类型 (refType)
|
|
719
|
-
%(prog)s --config ok-cosmic.json get <formId> --fuzzy <字段> --show-detail
|
|
720
|
-
|
|
721
|
-
# 按字段类型模糊筛选(如筛出所有 BaseData 类型字段)
|
|
722
|
-
%(prog)s --config ok-cosmic.json get <formId> --type BaseData
|
|
723
|
-
|
|
724
|
-
# 按多种类型筛选(正则 OR)
|
|
725
|
-
%(prog)s --config ok-cosmic.json get <formId> --type "combo|check"
|
|
726
|
-
|
|
727
|
-
# 类型 + 关键词交集筛选(如筛出 Decimal 类型中含 amount 的字段)
|
|
728
|
-
%(prog)s --config ok-cosmic.json get <formId> --type decimal --fuzzy amount
|
|
729
|
-
|
|
730
|
-
# 按字段树结构输出
|
|
731
|
-
%(prog)s --config ok-cosmic.json get <formId> --tree
|
|
732
|
-
|
|
733
|
-
# 查看所有操作按钮
|
|
734
|
-
%(prog)s --config ok-cosmic.json get <formId> --op
|
|
735
|
-
|
|
736
|
-
# 按关键词筛选操作
|
|
737
|
-
%(prog)s --config ok-cosmic.json get <formId> --op 审核 提交
|
|
738
|
-
|
|
739
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
740
|
-
AI 调用约束(摘要,完整约束见 SKILL.md 跨脚本硬约束与 coding-preferences.md B2)
|
|
741
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
742
|
-
- formIdOrName 支持逗号分隔批量查询,英文标识与中文名可混合,自动识别。
|
|
743
|
-
- --fuzzy 多关键词用空格分隔(如 --fuzzy qty price amount),严禁逐个字段多次查询。
|
|
744
|
-
- ≥ 3 个关键词时自动升级为详情模式;编写枚举判断前必须带 --show-detail。
|
|
745
|
-
""",
|
|
746
|
-
)
|
|
747
|
-
parser.add_argument("--config", help="Path to ok-cosmic.json")
|
|
748
|
-
|
|
749
|
-
sub_parser = parser.add_subparsers(dest="command")
|
|
750
|
-
get_parser = sub_parser.add_parser("get")
|
|
751
|
-
get_parser.add_argument("formIdOrName", help="formId 或中文名,支持逗号分隔批量查询(英文标识与中文名可混合,自动识别)")
|
|
752
|
-
get_parser.add_argument("--fuzzy", nargs="*", help="筛选关键词或正则模式,触发详情视图。支持多种传参方式: --fuzzy a b c / --fuzzy 'a b c' / --fuzzy 'a|b'。≥ 3 个关键词时自动升级为详情模式(含枚举/refType)")
|
|
753
|
-
get_parser.add_argument("--type", nargs="*", dest="type_filter", help="按字段类型模糊筛选,触发详情视图。支持正则: --type BaseData / --type 'combo|check' / --type decimal text。可与 --fuzzy 联用做交集筛选")
|
|
754
|
-
get_parser.add_argument("--show-detail", action="store_true", help="显示枚举值映射(extMap)或基础资料引用类型(refType)")
|
|
755
|
-
get_parser.add_argument("--tree", action="store_true", help="按 parentKey 输出字段树(可与 --fuzzy 联用)")
|
|
756
|
-
get_parser.add_argument("--op", nargs="*", help="操作查询模式。不带值列出全部操作;带关键词按 name/key 模糊筛选(如 --op 审核 提交)")
|
|
757
|
-
get_parser.add_argument("--sql", action="store_true", help="启用 SQL 模式,展示库名、表名与数据库字段名 (dbName/dbTableName/dbKey)")
|
|
758
|
-
get_parser.add_argument("--sort", choices=["key", "name", "type", "dbKey"], default="key", help="字段排序方式")
|
|
759
|
-
get_parser.add_argument("--view", choices=["form", "entity", "operate", "all"], default="all", help="元数据视图范围")
|
|
760
|
-
get_parser.add_argument("--debug", action="store_true")
|
|
761
|
-
get_parser.add_argument("--refresh", action="store_true")
|
|
762
|
-
|
|
763
|
-
args = parser.parse_args()
|
|
764
|
-
config = load_project_config(args.config)
|
|
765
|
-
fm = FormMetadata(config, debug=getattr(args, 'debug', False))
|
|
766
|
-
|
|
767
|
-
if args.command == "get":
|
|
768
|
-
# 智能拆分 fuzzy 参数:纯关键词按空格拆开,正则模式保持原样
|
|
769
|
-
raw_fuzzy = list(args.fuzzy) if args.fuzzy else None
|
|
770
|
-
if args.fuzzy:
|
|
771
|
-
args.fuzzy = _normalize_fuzzy_patterns(args.fuzzy)
|
|
772
|
-
# 智能拆分 type 参数
|
|
773
|
-
type_patterns = None
|
|
774
|
-
if args.type_filter:
|
|
775
|
-
type_patterns = _normalize_fuzzy_patterns(args.type_filter)
|
|
776
|
-
# 当 fuzzy 关键词 ≥3 个时,自动升级为详情模式(含枚举/refType)
|
|
777
|
-
auto_detail = False
|
|
778
|
-
show_detail = args.show_detail
|
|
779
|
-
if not show_detail and args.fuzzy and len(args.fuzzy) >= 3:
|
|
780
|
-
show_detail = True
|
|
781
|
-
auto_detail = True
|
|
782
|
-
|
|
783
|
-
# ── 批量查询支持:逗号分隔,自动识别 formId / billName ──
|
|
784
|
-
_RE_HAS_CJK = re.compile(r'[\u4e00-\u9fff\u3400-\u4dbf]')
|
|
785
|
-
raw_targets = [t.strip() for t in re.split(r'[,\uff0c]', args.formIdOrName) if t.strip()] if args.formIdOrName else []
|
|
786
|
-
|
|
787
|
-
if not raw_targets:
|
|
788
|
-
print("✖️ 错误: 必须提供 formIdOrName")
|
|
789
|
-
fm.cache.close()
|
|
790
|
-
sys.exit(1)
|
|
791
|
-
|
|
792
|
-
# 分类:含中文 → billName,否则 → formId
|
|
793
|
-
query_targets = []
|
|
794
|
-
for t in raw_targets:
|
|
795
|
-
if _RE_HAS_CJK.search(t):
|
|
796
|
-
query_targets.append({"formId": None, "billName": t})
|
|
797
|
-
else:
|
|
798
|
-
query_targets.append({"formId": t, "billName": None})
|
|
799
|
-
|
|
800
|
-
# refresh 清缓存(串行,SQLite 写操作)
|
|
801
|
-
if args.refresh:
|
|
802
|
-
for qt in query_targets:
|
|
803
|
-
if qt["formId"]:
|
|
804
|
-
fm.cache.remove(qt["formId"])
|
|
805
|
-
|
|
806
|
-
# ── --op 模式 ──
|
|
807
|
-
op_mode = args.op is not None
|
|
808
|
-
op_patterns = None
|
|
809
|
-
if op_mode and args.op:
|
|
810
|
-
op_patterns = _normalize_fuzzy_patterns(args.op)
|
|
811
|
-
|
|
812
|
-
def _query_one(qt):
|
|
813
|
-
return fm.get_meta_fields(
|
|
814
|
-
formId=qt["formId"],
|
|
815
|
-
billName=qt["billName"],
|
|
816
|
-
filter_patterns=args.fuzzy,
|
|
817
|
-
raw_patterns=raw_fuzzy,
|
|
818
|
-
type_patterns=type_patterns,
|
|
819
|
-
show_detail=show_detail,
|
|
820
|
-
tree_view=args.tree,
|
|
821
|
-
sql_mode=args.sql,
|
|
822
|
-
sort_by=args.sort,
|
|
823
|
-
view=args.view,
|
|
824
|
-
op_mode=op_mode,
|
|
825
|
-
op_patterns=op_patterns
|
|
826
|
-
)
|
|
827
|
-
|
|
828
|
-
if len(query_targets) == 1:
|
|
829
|
-
results = [_query_one(query_targets[0])]
|
|
830
|
-
else:
|
|
831
|
-
results = [None] * len(query_targets)
|
|
832
|
-
with ThreadPoolExecutor(max_workers=min(len(query_targets), 4)) as pool:
|
|
833
|
-
future_to_idx = {
|
|
834
|
-
pool.submit(_query_one, qt): i
|
|
835
|
-
for i, qt in enumerate(query_targets)
|
|
836
|
-
}
|
|
837
|
-
for future in as_completed(future_to_idx):
|
|
838
|
-
idx = future_to_idx[future]
|
|
839
|
-
try:
|
|
840
|
-
results[idx] = future.result()
|
|
841
|
-
except Exception as e:
|
|
842
|
-
t = query_targets[idx]
|
|
843
|
-
label = t["formId"] or t["billName"]
|
|
844
|
-
results[idx] = f"✖️ 查询 `{label}` 失败: {e}"
|
|
845
|
-
|
|
846
|
-
output = "\n\n---\n\n".join(results)
|
|
847
|
-
if auto_detail:
|
|
848
|
-
output += "\n\n> [Tip] 已自动启用详情模式(fuzzy 关键词 ≥3),包含枚举映射和基础资料引用类型。"
|
|
849
|
-
print(output)
|
|
850
|
-
else:
|
|
851
|
-
parser.print_help()
|
|
852
|
-
|
|
853
|
-
fm.cache.close()
|
|
854
|
-
|
|
855
|
-
if __name__ == "__main__":
|
|
856
|
-
sys.exit(run_cli(main))
|