kcode-pi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/README.md +358 -0
  2. package/dist/cli/kcode.d.ts +15 -0
  3. package/dist/cli/kcode.js +153 -0
  4. package/dist/cli/main.d.ts +2 -0
  5. package/dist/cli/main.js +7 -0
  6. package/docs/KCODE_DISTRIBUTION.md +91 -0
  7. package/extensions/kingdee-harness.ts +180 -0
  8. package/extensions/kingdee-header.ts +122 -0
  9. package/extensions/kingdee-tools.ts +379 -0
  10. package/knowledge/.backup/v1.0.0/version.json +10 -0
  11. package/knowledge/cangqiong/product-notes.md +15 -0
  12. package/knowledge/common/business-flows.md +115 -0
  13. package/knowledge/common/config-guides.md +110 -0
  14. package/knowledge/common/error-patterns.md +170 -0
  15. package/knowledge/common/implementation.md +144 -0
  16. package/knowledge/cosmic/hard-constraints.md +38 -0
  17. package/knowledge/cosmic/ksql-datafix.md +34 -0
  18. package/knowledge/cosmic/platform-baseline.md +32 -0
  19. package/knowledge/cosmic/plugin-decision-matrix.md +40 -0
  20. package/knowledge/cosmic/review-checklist.md +40 -0
  21. package/knowledge/cosmic/unittest.md +35 -0
  22. package/knowledge/enterprise/api-reference.md +186 -0
  23. package/knowledge/enterprise/code-patterns.md +217 -0
  24. package/knowledge/enterprise/plugin-lifecycle.md +188 -0
  25. package/knowledge/enterprise/tables.json +159 -0
  26. package/knowledge/flagship/api-reference.md +237 -0
  27. package/knowledge/flagship/code-patterns.md +246 -0
  28. package/knowledge/flagship/cosmic-platform-note.md +15 -0
  29. package/knowledge/flagship/plugin-lifecycle.md +248 -0
  30. package/knowledge/flagship/tables.json +159 -0
  31. package/knowledge/version.json +10 -0
  32. package/knowledge/xinghan/product-notes.md +15 -0
  33. package/package.json +71 -0
  34. package/prompts/kd-discuss.md +11 -0
  35. package/prompts/kd-execute.md +12 -0
  36. package/prompts/kd-plan.md +12 -0
  37. package/prompts/kd-ship.md +12 -0
  38. package/prompts/kd-spec.md +12 -0
  39. package/prompts/kd-verify.md +12 -0
  40. package/skills/kd-check/SKILL.md +26 -0
  41. package/skills/kd-cosmic-dev/SKILL.md +82 -0
  42. package/skills/kd-cosmic-review/SKILL.md +90 -0
  43. package/skills/kd-cosmic-unittest/SKILL.md +92 -0
  44. package/skills/kd-debug/SKILL.md +30 -0
  45. package/skills/kd-discuss/SKILL.md +24 -0
  46. package/skills/kd-execute/SKILL.md +22 -0
  47. package/skills/kd-gen/SKILL.md +34 -0
  48. package/skills/kd-ksql/SKILL.md +86 -0
  49. package/skills/kd-plan/SKILL.md +24 -0
  50. package/skills/kd-ship/SKILL.md +22 -0
  51. package/skills/kd-spec/SKILL.md +24 -0
  52. package/skills/kd-verify/SKILL.md +22 -0
  53. package/themes/kcode-dark.json +81 -0
  54. package/vendor/kingdee-skills/cosmic-unittest/SKILL.md +788 -0
  55. package/vendor/kingdee-skills/cosmic-unittest/author-cache.json +5 -0
  56. package/vendor/kingdee-skills/cosmic-unittest/cosmic-unittest-skill-overview.html +746 -0
  57. package/vendor/kingdee-skills/cosmic-unittest/examples/business-test.md +205 -0
  58. package/vendor/kingdee-skills/cosmic-unittest/examples/common-test.md +257 -0
  59. package/vendor/kingdee-skills/cosmic-unittest/examples/formplugin-test.md +560 -0
  60. package/vendor/kingdee-skills/cosmic-unittest/examples/op-plugin-test.md +231 -0
  61. package/vendor/kingdee-skills/cosmic-unittest/examples/validator-test.md +232 -0
  62. package/vendor/kingdee-skills/cosmic-unittest/patterns/business-helper.md +184 -0
  63. package/vendor/kingdee-skills/cosmic-unittest/patterns/common-module.md +355 -0
  64. package/vendor/kingdee-skills/cosmic-unittest/patterns/convert-plugin.md +130 -0
  65. package/vendor/kingdee-skills/cosmic-unittest/patterns/formplugin.md +235 -0
  66. package/vendor/kingdee-skills/cosmic-unittest/patterns/op-plugin.md +226 -0
  67. package/vendor/kingdee-skills/cosmic-unittest/patterns/validator.md +206 -0
  68. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/SKILL.md +674 -0
  69. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/advanced-scenario-checklist.md +307 -0
  70. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/algox-performance-checklist.md +129 -0
  71. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/coding-standard-checklist.md +491 -0
  72. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/cosmic-api-checklist.md +285 -0
  73. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/data-access-checklist.md +261 -0
  74. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/data-transaction-checklist.md +390 -0
  75. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/domain-logic-checklist.md +295 -0
  76. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/form-plugin-checklist.md +508 -0
  77. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/infra-checklist.md +254 -0
  78. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/ksql-checklist.md +305 -0
  79. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/lifecycle-checklist.md +298 -0
  80. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/operation-plugin-checklist.md +442 -0
  81. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/test-mock-checklist.md +120 -0
  82. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/ui-performance-checklist.md +320 -0
  83. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/pattern-matcher.py +336 -0
  84. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/review-score-calculator.py +121 -0
  85. package/vendor/kingdee-skills/ok-cosmic/CHANGELOG.md +295 -0
  86. package/vendor/kingdee-skills/ok-cosmic/README.md +460 -0
  87. package/vendor/kingdee-skills/ok-cosmic/SKILL.md +287 -0
  88. package/vendor/kingdee-skills/ok-cosmic/agents/openai.yaml +17 -0
  89. package/vendor/kingdee-skills/ok-cosmic/assets/BatchImportPluginTemplate.java +93 -0
  90. package/vendor/kingdee-skills/ok-cosmic/assets/BillPlugInTemplate.java +156 -0
  91. package/vendor/kingdee-skills/ok-cosmic/assets/ConvertPlugInTemplate.java +255 -0
  92. package/vendor/kingdee-skills/ok-cosmic/assets/FormPluginTemplate.java +597 -0
  93. package/vendor/kingdee-skills/ok-cosmic/assets/IWorkflowPluginTemplate.java +91 -0
  94. package/vendor/kingdee-skills/ok-cosmic/assets/ListPluginTemplate.java +194 -0
  95. package/vendor/kingdee-skills/ok-cosmic/assets/OpPluginTemplate.java +201 -0
  96. package/vendor/kingdee-skills/ok-cosmic/assets/OpenApiControllerTemplate.java +103 -0
  97. package/vendor/kingdee-skills/ok-cosmic/assets/PrintPluginTemplate.java +95 -0
  98. package/vendor/kingdee-skills/ok-cosmic/assets/ReportFormPluginTemplate.java +257 -0
  99. package/vendor/kingdee-skills/ok-cosmic/assets/ReportListDataPluginTemplate.java +70 -0
  100. package/vendor/kingdee-skills/ok-cosmic/assets/StandardTreeListPluginTemplate.java +130 -0
  101. package/vendor/kingdee-skills/ok-cosmic/assets/TaskTemplate.java +80 -0
  102. package/vendor/kingdee-skills/ok-cosmic/assets/TreeListPluginTemplate.java +152 -0
  103. package/vendor/kingdee-skills/ok-cosmic/assets/WriteBackPlugInTemplate.java +286 -0
  104. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/attachment/AttachmentUploadBindSample.java +93 -0
  105. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/botp/BotpTracePushSample.java +168 -0
  106. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/botp/SampleConvertPlugin.java +223 -0
  107. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/cache/SampleCacheUsage.java +218 -0
  108. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/concurrent/SampleThreadPoolBatch.java +156 -0
  109. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/data/DynamicObjectCrudSample.java +205 -0
  110. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/data/DynamicObjectOpsSample.java +100 -0
  111. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/BeforeOperationConfirmSample.java +217 -0
  112. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/ConfirmDialogSample.java +131 -0
  113. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/EntryRowCalculateSample.java +116 -0
  114. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/F7FilterSample.java +134 -0
  115. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/GetAndSetValueSample.java +176 -0
  116. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/HyperlinkJumpSample.java +124 -0
  117. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/OpenBillModalSample.java +253 -0
  118. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/ReturnParentDataSample.java +295 -0
  119. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/TreeControlSample.java +140 -0
  120. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/ViewControlOpsSample.java +132 -0
  121. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/list/ListPluginBasicSample.java +170 -0
  122. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/list/ListPreOpenFilterSample.java +68 -0
  123. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/message/MessageNotifySample.java +95 -0
  124. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/mq/SampleMQConsumer.java +198 -0
  125. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/mq/sample_mq.xml +15 -0
  126. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/operation/OpAddValidatorsSample.java +137 -0
  127. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/operation/OperationOptionBridgeSample.java +228 -0
  128. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/package-info.java +19 -0
  129. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/query/BaseDataQuerySample.java +194 -0
  130. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/query/BatchQuerySample.java +368 -0
  131. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/query/DataSetQueryStatSample.java +131 -0
  132. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/report/SampleReportFormPlugin.java +179 -0
  133. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/report/SampleReportListDataPlugin.java +616 -0
  134. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/snippets-guide.md +64 -0
  135. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/task/ScheduleTaskSample.java +160 -0
  136. package/vendor/kingdee-skills/ok-cosmic/assets/snippets/workflow/SampleWorkflowPlugin.java +302 -0
  137. package/vendor/kingdee-skills/ok-cosmic/manifest.json +78 -0
  138. package/vendor/kingdee-skills/ok-cosmic/ok-cosmic-intro.html +903 -0
  139. package/vendor/kingdee-skills/ok-cosmic/references/adv/attachment-api.md +114 -0
  140. package/vendor/kingdee-skills/ok-cosmic/references/adv/botp-convert.md +98 -0
  141. package/vendor/kingdee-skills/ok-cosmic/references/adv/dynamic-object.md +113 -0
  142. package/vendor/kingdee-skills/ok-cosmic/references/adv/entity-metadata.md +123 -0
  143. package/vendor/kingdee-skills/ok-cosmic/references/adv/event-lifecycle.md +184 -0
  144. package/vendor/kingdee-skills/ok-cosmic/references/adv/flex-prop.md +114 -0
  145. package/vendor/kingdee-skills/ok-cosmic/references/adv/form-utils.md +133 -0
  146. package/vendor/kingdee-skills/ok-cosmic/references/adv/operate-chain.md +159 -0
  147. package/vendor/kingdee-skills/ok-cosmic/references/adv/plugin-base.md +218 -0
  148. package/vendor/kingdee-skills/ok-cosmic/references/adv/query-dataset.md +149 -0
  149. package/vendor/kingdee-skills/ok-cosmic/references/adv/request-context.md +88 -0
  150. package/vendor/kingdee-skills/ok-cosmic/references/adv/view-handler.md +157 -0
  151. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-bill.md +76 -0
  152. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-botp.md +70 -0
  153. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-form.md +165 -0
  154. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-import.md +69 -0
  155. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-list.md +227 -0
  156. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-openapi.md +112 -0
  157. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-operation.md +135 -0
  158. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-print.md +65 -0
  159. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-report-data.md +64 -0
  160. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-report-form.md +90 -0
  161. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-task.md +62 -0
  162. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-tree-list.md +71 -0
  163. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-workflow.md +82 -0
  164. package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-writeback.md +71 -0
  165. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-algo.md +67 -0
  166. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-cache.md +63 -0
  167. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-dynamic-model-svc.md +82 -0
  168. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-dynamic-object.md +70 -0
  169. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-entity-model.md +61 -0
  170. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-exception.md +64 -0
  171. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-file.md +63 -0
  172. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-id.md +47 -0
  173. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-lock.md +61 -0
  174. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-log.md +63 -0
  175. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-network-control.md +70 -0
  176. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-orm-access.md +78 -0
  177. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-request-context.md +62 -0
  178. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-threadpool.md +63 -0
  179. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-tx.md +64 -0
  180. package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-utils.md +67 -0
  181. package/vendor/kingdee-skills/ok-cosmic/requirements.txt +2 -0
  182. package/vendor/kingdee-skills/ok-cosmic/rules/a-layer-rules.json +24 -0
  183. package/vendor/kingdee-skills/ok-cosmic/rules/anti-patterns.md +48 -0
  184. package/vendor/kingdee-skills/ok-cosmic/rules/cheat-sheet.md +256 -0
  185. package/vendor/kingdee-skills/ok-cosmic/rules/coding-preferences.md +140 -0
  186. package/vendor/kingdee-skills/ok-cosmic/rules/constraints.md +61 -0
  187. package/vendor/kingdee-skills/ok-cosmic/rules/decision-matrix.md +222 -0
  188. package/vendor/kingdee-skills/ok-cosmic/rules/intent-routing.md +94 -0
  189. package/vendor/kingdee-skills/ok-cosmic/rules/platform-baseline.md +69 -0
  190. package/vendor/kingdee-skills/ok-cosmic/rules/post-check.md +109 -0
  191. package/vendor/kingdee-skills/ok-cosmic/scripts/config_loader.py +204 -0
  192. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-api-knowledge.py +910 -0
  193. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-basedata-query.py +359 -0
  194. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-config-check.py +181 -0
  195. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-extpoints-query.py +389 -0
  196. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-form-metadata.py +856 -0
  197. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-check.py +262 -0
  198. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-lint.py +293 -0
  199. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/__init__.py +2 -0
  200. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/base.py +393 -0
  201. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/resource_check.py +176 -0
  202. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/scene_check.py +375 -0
  203. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/style_check.py +434 -0
  204. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/verify_check.py +36 -0
  205. package/vendor/kingdee-skills/ok-cosmic/scripts/route_client.py +186 -0
  206. package/vendor/kingdee-skills/ok-cosmic/scripts/script_utils.py +40 -0
  207. package/vendor/kingdee-skills/ok-cosmic/scripts/sqlite_cache.py +142 -0
  208. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-commons.jar +0 -0
  209. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-features.jar +0 -0
  210. package/vendor/kingdee-skills/ok-cosmic/setup/ok-cosmic-docs.db +0 -0
  211. package/vendor/kingdee-skills/ok-cosmic/setup/ok-cosmic.json +13 -0
  212. package/vendor/kingdee-skills/ok-cosmic/setup/setup-mac.sh +18 -0
  213. package/vendor/kingdee-skills/ok-cosmic/setup/setup-windows.bat +53 -0
  214. package/vendor/kingdee-skills/ok-cosmic/setup/setup.jar +0 -0
  215. package/vendor/kingdee-skills/ok-ksql/SKILL.md +81 -0
  216. package/vendor/kingdee-skills/ok-ksql/agents/openai.yaml +7 -0
  217. package/vendor/kingdee-skills/ok-ksql/manifest.json +14 -0
  218. package/vendor/kingdee-skills/ok-ksql/references/ksql-datafix.md +452 -0
  219. package/vendor/kingdee-skills/ok-ksql/scripts/ksql_lint.py +363 -0
@@ -0,0 +1,856 @@
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))