dsh-crwu-workbench 0.0.10

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 (479) hide show
  1. package/CHANGELOG.md +1516 -0
  2. package/LICENSE +26 -0
  3. package/README.en.md +204 -0
  4. package/README.md +521 -0
  5. package/SECURITY.md +39 -0
  6. package/bin/darwin-arm64/crwu +0 -0
  7. package/bin/darwin-arm64/dws +0 -0
  8. package/bin/darwin-arm64/ossutil +0 -0
  9. package/bin/manifest.json +82 -0
  10. package/bin/win32-x64/crwu.exe +0 -0
  11. package/bin/win32-x64/dws.exe +4 -0
  12. package/bin/win32-x64/ossutil.exe +0 -0
  13. package/common/skills/.synced.json +8 -0
  14. package/common/skills/crwu-dws/SKILL.md +208 -0
  15. package/common/skills/crwu-dws/references/00-/347/233/256/345/275/225/345/277/253/347/205/247schema.md +113 -0
  16. package/common/skills/crwu-dws/references/01-/345/256/241/346/240/270/344/270/213/350/275/275/344/270/216manifest/350/247/204/350/214/203.md +115 -0
  17. package/common/skills/crwu-dws/references/02-/347/274/223/345/255/230/344/270/216/345/205/234/345/272/225/346/237/245/346/211/276/350/247/204/350/214/203.md +119 -0
  18. package/common/skills/crwu-dws/scripts/test_dws_source_contract.py +493 -0
  19. package/common/skills/crwu-h3yun-login/SKILL.md +130 -0
  20. package/common/skills/crwu-h3yun-query/SKILL.md +192 -0
  21. package/config/crwu-workbench.yml +26 -0
  22. package/cordis.patch.yml +46 -0
  23. package/lib/client.js +10346 -0
  24. package/lib/index.js +11584 -0
  25. package/package.json +119 -0
  26. package/scripts/prepare.mjs +59 -0
  27. package/skills/README.md +26 -0
  28. package/skills/crwu/crwu-audit/SKILL.md +152 -0
  29. package/skills/crwu/crwu-audit/references/00-input-and-route-profile.md +232 -0
  30. package/skills/crwu/crwu-audit/references/01-report-id-resolution.md +13 -0
  31. package/skills/crwu/crwu-audit/references/02-scope-classification.md +25 -0
  32. package/skills/crwu/crwu-audit/references/03-asset-classification.md +27 -0
  33. package/skills/crwu/crwu-audit/references/04-business-classification.md +63 -0
  34. package/skills/crwu/crwu-audit/references/05-method-classification.md +66 -0
  35. package/skills/crwu/crwu-audit/references/06-overlay-classification.md +42 -0
  36. package/skills/crwu/crwu-audit/references/07-skill-registry.md +57 -0
  37. package/skills/crwu/crwu-audit/references/08-union-dispatch-rules.md +142 -0
  38. package/skills/crwu/crwu-audit/references/09-review-risk-classification.md +85 -0
  39. package/skills/crwu/crwu-audit/references/10-capability-gap-proposal.md +68 -0
  40. package/skills/crwu/crwu-audit/references/11-html-delivery-spec.md +881 -0
  41. package/skills/crwu/crwu-audit/references/12-leaf-common-contract.md +202 -0
  42. package/skills/crwu/crwu-audit/references/13-dingtalk-result-publish.md +88 -0
  43. package/skills/crwu/crwu-audit/references/14-orchestration-workflow.md +52 -0
  44. package/skills/crwu/crwu-audit/references/99-maintenance.md +62 -0
  45. package/skills/crwu/crwu-audit/scripts/README.md +157 -0
  46. package/skills/crwu/crwu-audit/scripts/audit_delivery.py +2736 -0
  47. package/skills/crwu/crwu-audit/scripts/audit_result.schema.json +1659 -0
  48. package/skills/crwu/crwu-audit/scripts/examples/audit-result.sample.json +739 -0
  49. package/skills/crwu/crwu-audit/scripts/fetch_review_records.py +85 -0
  50. package/skills/crwu/crwu-audit/scripts/media_extract.py +513 -0
  51. package/skills/crwu/crwu-audit/scripts/prepare_materials.py +1252 -0
  52. package/skills/crwu/crwu-audit/scripts/test_audit_delivery.py +1678 -0
  53. package/skills/crwu/crwu-audit/scripts/test_audit_multiaxis_router.py +1339 -0
  54. package/skills/crwu/crwu-audit/scripts/test_fetch_review_records.py +77 -0
  55. package/skills/crwu/crwu-audit/scripts/test_media_extract.py +332 -0
  56. package/skills/crwu/crwu-audit/scripts/test_prepare_materials.py +1081 -0
  57. package/skills/crwu/crwu-audit/template/audit-report.html +435 -0
  58. package/skills/crwu/crwu-audit-asset-asset-group-goodwill/SKILL.md +38 -0
  59. package/skills/crwu/crwu-audit-asset-asset-group-goodwill/references/00-applicability.md +28 -0
  60. package/skills/crwu/crwu-audit-asset-asset-group-goodwill/references/01-kb-assembly.md +31 -0
  61. package/skills/crwu/crwu-audit-asset-asset-group-goodwill/references/02-review-focus.md +21 -0
  62. package/skills/crwu/crwu-audit-asset-asset-group-goodwill/references/03-common-contract.md +202 -0
  63. package/skills/crwu/crwu-audit-asset-debt/SKILL.md +38 -0
  64. package/skills/crwu/crwu-audit-asset-debt/references/00-applicability.md +40 -0
  65. package/skills/crwu/crwu-audit-asset-debt/references/01-kb-assembly.md +41 -0
  66. package/skills/crwu/crwu-audit-asset-debt/references/02-review-focus.md +54 -0
  67. package/skills/crwu/crwu-audit-asset-debt/references/03-common-contract.md +202 -0
  68. package/skills/crwu/crwu-audit-asset-enterprise-value/SKILL.md +38 -0
  69. package/skills/crwu/crwu-audit-asset-enterprise-value/references/00-applicability.md +31 -0
  70. package/skills/crwu/crwu-audit-asset-enterprise-value/references/01-kb-assembly.md +35 -0
  71. package/skills/crwu/crwu-audit-asset-enterprise-value/references/02-review-focus.md +33 -0
  72. package/skills/crwu/crwu-audit-asset-enterprise-value/references/03-common-contract.md +202 -0
  73. package/skills/crwu/crwu-audit-asset-equipment/SKILL.md +38 -0
  74. package/skills/crwu/crwu-audit-asset-equipment/references/00-applicability.md +40 -0
  75. package/skills/crwu/crwu-audit-asset-equipment/references/01-kb-assembly.md +39 -0
  76. package/skills/crwu/crwu-audit-asset-equipment/references/02-review-focus.md +52 -0
  77. package/skills/crwu/crwu-audit-asset-equipment/references/03-common-contract.md +202 -0
  78. package/skills/crwu/crwu-audit-asset-intangible/SKILL.md +38 -0
  79. package/skills/crwu/crwu-audit-asset-intangible/references/00-applicability.md +36 -0
  80. package/skills/crwu/crwu-audit-asset-intangible/references/01-kb-assembly.md +40 -0
  81. package/skills/crwu/crwu-audit-asset-intangible/references/02-review-focus.md +68 -0
  82. package/skills/crwu/crwu-audit-asset-intangible/references/03-common-contract.md +202 -0
  83. package/skills/crwu/crwu-audit-asset-inventory/SKILL.md +38 -0
  84. package/skills/crwu/crwu-audit-asset-inventory/references/00-applicability.md +28 -0
  85. package/skills/crwu/crwu-audit-asset-inventory/references/01-kb-assembly.md +31 -0
  86. package/skills/crwu/crwu-audit-asset-inventory/references/02-review-focus.md +21 -0
  87. package/skills/crwu/crwu-audit-asset-inventory/references/03-common-contract.md +202 -0
  88. package/skills/crwu/crwu-audit-asset-mining-right/SKILL.md +38 -0
  89. package/skills/crwu/crwu-audit-asset-mining-right/references/00-applicability.md +31 -0
  90. package/skills/crwu/crwu-audit-asset-mining-right/references/01-kb-assembly.md +35 -0
  91. package/skills/crwu/crwu-audit-asset-mining-right/references/02-review-focus.md +33 -0
  92. package/skills/crwu/crwu-audit-asset-mining-right/references/03-common-contract.md +202 -0
  93. package/skills/crwu/crwu-audit-asset-other/SKILL.md +38 -0
  94. package/skills/crwu/crwu-audit-asset-other/references/00-applicability.md +28 -0
  95. package/skills/crwu/crwu-audit-asset-other/references/01-kb-assembly.md +31 -0
  96. package/skills/crwu/crwu-audit-asset-other/references/02-review-focus.md +21 -0
  97. package/skills/crwu/crwu-audit-asset-other/references/03-common-contract.md +202 -0
  98. package/skills/crwu/crwu-audit-asset-portfolio/SKILL.md +38 -0
  99. package/skills/crwu/crwu-audit-asset-portfolio/references/00-applicability.md +28 -0
  100. package/skills/crwu/crwu-audit-asset-portfolio/references/01-kb-assembly.md +31 -0
  101. package/skills/crwu/crwu-audit-asset-portfolio/references/02-review-focus.md +21 -0
  102. package/skills/crwu/crwu-audit-asset-portfolio/references/03-common-contract.md +202 -0
  103. package/skills/crwu/crwu-audit-asset-realestate/SKILL.md +38 -0
  104. package/skills/crwu/crwu-audit-asset-realestate/references/00-applicability.md +43 -0
  105. package/skills/crwu/crwu-audit-asset-realestate/references/01-kb-assembly.md +75 -0
  106. package/skills/crwu/crwu-audit-asset-realestate/references/02-review-focus.md +93 -0
  107. package/skills/crwu/crwu-audit-asset-realestate/references/03-common-contract.md +202 -0
  108. package/skills/crwu/crwu-audit-asset-scrap-materials/SKILL.md +38 -0
  109. package/skills/crwu/crwu-audit-asset-scrap-materials/references/00-applicability.md +28 -0
  110. package/skills/crwu/crwu-audit-asset-scrap-materials/references/01-kb-assembly.md +31 -0
  111. package/skills/crwu/crwu-audit-asset-scrap-materials/references/02-review-focus.md +21 -0
  112. package/skills/crwu/crwu-audit-asset-scrap-materials/references/03-common-contract.md +202 -0
  113. package/skills/crwu/crwu-audit-asset-transport-equipment/SKILL.md +38 -0
  114. package/skills/crwu/crwu-audit-asset-transport-equipment/references/00-applicability.md +31 -0
  115. package/skills/crwu/crwu-audit-asset-transport-equipment/references/01-kb-assembly.md +35 -0
  116. package/skills/crwu/crwu-audit-asset-transport-equipment/references/02-review-focus.md +33 -0
  117. package/skills/crwu/crwu-audit-asset-transport-equipment/references/03-common-contract.md +202 -0
  118. package/skills/crwu/crwu-audit-biz-asset-operation/SKILL.md +38 -0
  119. package/skills/crwu/crwu-audit-biz-asset-operation/references/00-applicability.md +30 -0
  120. package/skills/crwu/crwu-audit-biz-asset-operation/references/01-kb-assembly.md +32 -0
  121. package/skills/crwu/crwu-audit-biz-asset-operation/references/02-review-focus.md +42 -0
  122. package/skills/crwu/crwu-audit-biz-asset-operation/references/03-common-contract.md +202 -0
  123. package/skills/crwu/crwu-audit-biz-consulting-review/SKILL.md +38 -0
  124. package/skills/crwu/crwu-audit-biz-consulting-review/references/00-applicability.md +30 -0
  125. package/skills/crwu/crwu-audit-biz-consulting-review/references/01-kb-assembly.md +32 -0
  126. package/skills/crwu/crwu-audit-biz-consulting-review/references/02-review-focus.md +40 -0
  127. package/skills/crwu/crwu-audit-biz-consulting-review/references/03-common-contract.md +202 -0
  128. package/skills/crwu/crwu-audit-biz-financial-reporting/SKILL.md +38 -0
  129. package/skills/crwu/crwu-audit-biz-financial-reporting/references/00-applicability.md +41 -0
  130. package/skills/crwu/crwu-audit-biz-financial-reporting/references/01-kb-assembly.md +40 -0
  131. package/skills/crwu/crwu-audit-biz-financial-reporting/references/02-review-focus.md +64 -0
  132. package/skills/crwu/crwu-audit-biz-financial-reporting/references/03-common-contract.md +202 -0
  133. package/skills/crwu/crwu-audit-biz-financing-debt/SKILL.md +38 -0
  134. package/skills/crwu/crwu-audit-biz-financing-debt/references/00-applicability.md +31 -0
  135. package/skills/crwu/crwu-audit-biz-financing-debt/references/01-kb-assembly.md +34 -0
  136. package/skills/crwu/crwu-audit-biz-financing-debt/references/02-review-focus.md +47 -0
  137. package/skills/crwu/crwu-audit-biz-financing-debt/references/03-common-contract.md +202 -0
  138. package/skills/crwu/crwu-audit-biz-investment-capital/SKILL.md +38 -0
  139. package/skills/crwu/crwu-audit-biz-investment-capital/references/00-applicability.md +31 -0
  140. package/skills/crwu/crwu-audit-biz-investment-capital/references/01-kb-assembly.md +34 -0
  141. package/skills/crwu/crwu-audit-biz-investment-capital/references/02-review-focus.md +47 -0
  142. package/skills/crwu/crwu-audit-biz-investment-capital/references/03-common-contract.md +202 -0
  143. package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/SKILL.md +38 -0
  144. package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/references/00-applicability.md +30 -0
  145. package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/references/01-kb-assembly.md +32 -0
  146. package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/references/02-review-focus.md +40 -0
  147. package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/references/03-common-contract.md +202 -0
  148. package/skills/crwu/crwu-audit-biz-tax-history/SKILL.md +38 -0
  149. package/skills/crwu/crwu-audit-biz-tax-history/references/00-applicability.md +29 -0
  150. package/skills/crwu/crwu-audit-biz-tax-history/references/01-kb-assembly.md +30 -0
  151. package/skills/crwu/crwu-audit-biz-tax-history/references/02-review-focus.md +33 -0
  152. package/skills/crwu/crwu-audit-biz-tax-history/references/03-common-contract.md +202 -0
  153. package/skills/crwu/crwu-audit-biz-transaction-disposal/SKILL.md +38 -0
  154. package/skills/crwu/crwu-audit-biz-transaction-disposal/references/00-applicability.md +31 -0
  155. package/skills/crwu/crwu-audit-biz-transaction-disposal/references/01-kb-assembly.md +34 -0
  156. package/skills/crwu/crwu-audit-biz-transaction-disposal/references/02-review-focus.md +47 -0
  157. package/skills/crwu/crwu-audit-biz-transaction-disposal/references/03-common-contract.md +202 -0
  158. package/skills/crwu/crwu-audit-datacheck/SKILL.md +59 -0
  159. package/skills/crwu/crwu-audit-datacheck/references/00-KB/350/243/205/351/205/215/350/241/250.md +25 -0
  160. package/skills/crwu/crwu-audit-datacheck/references/01-runtime-and-check-catalog.md +139 -0
  161. package/skills/crwu/crwu-audit-datacheck/scripts/recalc_check.py +521 -0
  162. package/skills/crwu/crwu-audit-datacheck/scripts/test_recalc_check.py +419 -0
  163. package/skills/crwu/crwu-audit-datacheck/scripts/test_skill_self_containment.py +44 -0
  164. package/skills/crwu/crwu-audit-external-data/SKILL.md +73 -0
  165. package/skills/crwu/crwu-audit-external-data/references/00-KB/350/243/205/351/205/215/350/241/250.md +38 -0
  166. package/skills/crwu/crwu-audit-external-data/references/01-connector-access.md +116 -0
  167. package/skills/crwu/crwu-audit-external-data/references/02-verification-workflow.md +78 -0
  168. package/skills/crwu/crwu-audit-external-data/scripts/connector_probe.py +374 -0
  169. package/skills/crwu/crwu-audit-external-data/scripts/test_connector_probe.py +264 -0
  170. package/skills/crwu/crwu-audit-output-filter/SKILL.md +34 -0
  171. package/skills/crwu/crwu-audit-output-filter/references/00-applicability.md +7 -0
  172. package/skills/crwu/crwu-audit-output-filter/references/01-kb-assembly.md +11 -0
  173. package/skills/crwu/crwu-audit-output-filter/references/02-review-focus.md +25 -0
  174. package/skills/crwu/crwu-audit-public-general-standards/SKILL.md +46 -0
  175. package/skills/crwu/crwu-audit-public-general-standards/references/00-applicability.md +41 -0
  176. package/skills/crwu/crwu-audit-public-general-standards/references/01-kb-assembly.md +66 -0
  177. package/skills/crwu/crwu-audit-public-general-standards/references/02-review-focus.md +111 -0
  178. package/skills/crwu/crwu-audit-public-general-standards/references/03-execution-contract.md +89 -0
  179. package/skills/crwu/crwu-dev-audit-optimize/SKILL.md +98 -0
  180. package/skills/crwu/crwu-dev-audit-optimize/references/00-/344/274/230/345/214/226/350/247/204/350/214/203/344/270/216/346/226/207/344/273/266/350/220/275/347/202/271.md +132 -0
  181. package/skills/crwu/crwu-dev-audit-optimize/references/01-/345/217/215/351/246/210/345/256/232/344/275/215/344/270/216/347/224/273/345/203/217/346/265/201/347/250/213.md +108 -0
  182. package/skills/crwu/crwu-dev-audit-optimize/references/02-/346/226/271/346/241/210/346/250/241/346/235/277/344/270/216/347/241/256/350/256/244/351/227/250/347/246/201.md +92 -0
  183. package/skills/crwu/crwu-dev-audit-optimize/references/03-AI/344/272/272/345/267/245/345/267/256/350/267/235/345/210/206/346/236/220/346/265/201/347/250/213.md +95 -0
  184. package/skills/crwu/crwu-dev-audit-optimize/scripts/examples/gap-analysis.sample.json +116 -0
  185. package/skills/crwu/crwu-dev-audit-optimize/scripts/gap_analysis.schema.json +33 -0
  186. package/skills/crwu/crwu-dev-audit-optimize/scripts/gap_analysis_delivery.py +475 -0
  187. package/skills/crwu/crwu-dev-audit-optimize/scripts/test_gap_analysis_delivery.py +855 -0
  188. package/skills/crwu/crwu-dev-audit-optimize/template/NOTICE.echarts.txt +7 -0
  189. package/skills/crwu/crwu-dev-audit-optimize/template/echarts.min.js +45 -0
  190. package/skills/crwu/crwu-dev-audit-optimize/template/gap-analysis-report.html +71 -0
  191. package/skills/crwu/crwu-dev-audit-skill-maintainer/SKILL.md +74 -0
  192. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/00-responsibility-and-modes.md +62 -0
  193. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/01-kb-source-discovery.md +89 -0
  194. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/02-child-skill-contract.md +99 -0
  195. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/03-review-item-execution-contract.md +51 -0
  196. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/04-registry-and-mapping-update.md +73 -0
  197. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/05-validation-and-delivery.md +127 -0
  198. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/06-live-routing-reconciliation.md +136 -0
  199. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/07-kb-skill-map.md +411 -0
  200. package/skills/crwu/crwu-dev-audit-skill-maintainer/scripts/README.md +200 -0
  201. package/skills/crwu/crwu-dev-audit-skill-maintainer/scripts/check_audit_skill_mappings.py +2477 -0
  202. package/skills/crwu/crwu-dev-audit-skill-maintainer/scripts/kb_tool.py +577 -0
  203. package/skills/crwu/crwu-dev-audit-skill-maintainer/scripts/test_audit_skill_maintainer.py +5097 -0
  204. package/skills/dws/LICENSE +178 -0
  205. package/skills/dws/NOTICE +17 -0
  206. package/skills/dws/dingtalk-aisearch/SKILL.md +83 -0
  207. package/skills/dws/dingtalk-aisearch/references/aisearch.md +249 -0
  208. package/skills/dws/dingtalk-aisearch/references/intent-guide.md +15 -0
  209. package/skills/dws/dingtalk-aisearch/references/lite-recipes.md +31 -0
  210. package/skills/dws/dingtalk-aitable/SKILL.md +139 -0
  211. package/skills/dws/dingtalk-aitable/references/06-data-analytics.md +11 -0
  212. package/skills/dws/dingtalk-aitable/references/aitable/aitable-advperm.md +251 -0
  213. package/skills/dws/dingtalk-aitable/references/aitable/aitable-attachment.md +49 -0
  214. package/skills/dws/dingtalk-aitable/references/aitable/aitable-best-practices.md +48 -0
  215. package/skills/dws/dingtalk-aitable/references/aitable/aitable-cell-value.md +346 -0
  216. package/skills/dws/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md +46 -0
  217. package/skills/dws/dingtalk-aitable/references/aitable/aitable-data-analysis-sop.md +131 -0
  218. package/skills/dws/dingtalk-aitable/references/aitable/aitable-datasource.md +272 -0
  219. package/skills/dws/dingtalk-aitable/references/aitable/aitable-error-recovery.md +128 -0
  220. package/skills/dws/dingtalk-aitable/references/aitable/aitable-export-import.md +113 -0
  221. package/skills/dws/dingtalk-aitable/references/aitable/aitable-field-properties.md +238 -0
  222. package/skills/dws/dingtalk-aitable/references/aitable/aitable-field.md +186 -0
  223. package/skills/dws/dingtalk-aitable/references/aitable/aitable-filter-sort.md +135 -0
  224. package/skills/dws/dingtalk-aitable/references/aitable/aitable-form.md +130 -0
  225. package/skills/dws/dingtalk-aitable/references/aitable/aitable-formula-guide.md +224 -0
  226. package/skills/dws/dingtalk-aitable/references/aitable/aitable-primary-doc.md +58 -0
  227. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-create.md +52 -0
  228. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-delete.md +20 -0
  229. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-history.md +97 -0
  230. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-name-key.md +47 -0
  231. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-query.md +120 -0
  232. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-share.md +54 -0
  233. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-stats.md +59 -0
  234. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-update.md +66 -0
  235. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-upsert.md +89 -0
  236. package/skills/dws/dingtalk-aitable/references/aitable/aitable-view-config.md +140 -0
  237. package/skills/dws/dingtalk-aitable/references/aitable/aitable-view-extras.md +195 -0
  238. package/skills/dws/dingtalk-aitable/references/aitable/aitable-view-types.md +95 -0
  239. package/skills/dws/dingtalk-aitable/references/aitable/aitable-workflow.md +384 -0
  240. package/skills/dws/dingtalk-aitable/references/aitable-record-ops.md +80 -0
  241. package/skills/dws/dingtalk-aitable/references/aitable-section.md +30 -0
  242. package/skills/dws/dingtalk-aitable/references/aitable.md +59 -0
  243. package/skills/dws/dingtalk-aitable/references/field-rules.md +17 -0
  244. package/skills/dws/dingtalk-aitable/references/intent-guide.md +17 -0
  245. package/skills/dws/dingtalk-aitable/references/recipes/conventions.md +44 -0
  246. package/skills/dws/dingtalk-aitable/references/url-patterns.md +144 -0
  247. package/skills/dws/dingtalk-aitable/scripts/aitable_export_via_task.py +210 -0
  248. package/skills/dws/dingtalk-aitable/scripts/aitable_import_via_task.py +173 -0
  249. package/skills/dws/dingtalk-aitable/scripts/bulk_add_fields.py +273 -0
  250. package/skills/dws/dingtalk-aitable/scripts/import_records.py +333 -0
  251. package/skills/dws/dingtalk-aitable/scripts/upload_attachment.py +190 -0
  252. package/skills/dws/dingtalk-calendar/SKILL.md +137 -0
  253. package/skills/dws/dingtalk-calendar/references/03-meeting.md +56 -0
  254. package/skills/dws/dingtalk-calendar/references/calendar.md +760 -0
  255. package/skills/dws/dingtalk-calendar/references/intent-guide.md +9 -0
  256. package/skills/dws/dingtalk-calendar/references/lite-recipes.md +25 -0
  257. package/skills/dws/dingtalk-calendar/scripts/calendar_free_slot_finder.py +199 -0
  258. package/skills/dws/dingtalk-calendar/scripts/calendar_schedule_meeting.py +233 -0
  259. package/skills/dws/dingtalk-calendar/scripts/calendar_today_agenda.py +139 -0
  260. package/skills/dws/dingtalk-chat/SKILL.md +127 -0
  261. package/skills/dws/dingtalk-chat/references/01-messaging.md +103 -0
  262. package/skills/dws/dingtalk-chat/references/card/callback.md +10 -0
  263. package/skills/dws/dingtalk-chat/references/card/create.md +27 -0
  264. package/skills/dws/dingtalk-chat/references/card/schema.md +14 -0
  265. package/skills/dws/dingtalk-chat/references/card/update.md +13 -0
  266. package/skills/dws/dingtalk-chat/references/chat/chat-bot.md +187 -0
  267. package/skills/dws/dingtalk-chat/references/chat/chat-conversation.md +165 -0
  268. package/skills/dws/dingtalk-chat/references/chat/group-admin.md +144 -0
  269. package/skills/dws/dingtalk-chat/references/chat/group-discovery.md +121 -0
  270. package/skills/dws/dingtalk-chat/references/chat/message-actions.md +132 -0
  271. package/skills/dws/dingtalk-chat/references/chat/message-media.md +76 -0
  272. package/skills/dws/dingtalk-chat/references/chat/message-query.md +135 -0
  273. package/skills/dws/dingtalk-chat/references/chat/thread.md +46 -0
  274. package/skills/dws/dingtalk-chat/references/chat-emoji-list.md +216 -0
  275. package/skills/dws/dingtalk-chat/references/chat.md +140 -0
  276. package/skills/dws/dingtalk-chat/references/contracts.md +70 -0
  277. package/skills/dws/dingtalk-chat/references/intent-guide.md +56 -0
  278. package/skills/dws/dingtalk-contact/SKILL.md +122 -0
  279. package/skills/dws/dingtalk-contact/references/08-directory.md +51 -0
  280. package/skills/dws/dingtalk-contact/references/contact.md +595 -0
  281. package/skills/dws/dingtalk-contact/references/intent-guide.md +14 -0
  282. package/skills/dws/dingtalk-contact/references/lite-recipes.md +30 -0
  283. package/skills/dws/dingtalk-contact/references/recipes/conventions.md +44 -0
  284. package/skills/dws/dingtalk-contact/scripts/contact_dept_members.py +130 -0
  285. package/skills/dws/dingtalk-doc/SKILL.md +112 -0
  286. package/skills/dws/dingtalk-doc/references/04-document.md +57 -0
  287. package/skills/dws/dingtalk-doc/references/contracts.md +40 -0
  288. package/skills/dws/dingtalk-doc/references/doc/doc-block.md +39 -0
  289. package/skills/dws/dingtalk-doc/references/doc/doc-comment.md +29 -0
  290. package/skills/dws/dingtalk-doc/references/doc/doc-create.md +54 -0
  291. package/skills/dws/dingtalk-doc/references/doc/doc-export.md +30 -0
  292. package/skills/dws/dingtalk-doc/references/doc/doc-import.md +28 -0
  293. package/skills/dws/dingtalk-doc/references/doc/doc-info.md +12 -0
  294. package/skills/dws/dingtalk-doc/references/doc/doc-media.md +46 -0
  295. package/skills/dws/dingtalk-doc/references/doc/doc-read.md +56 -0
  296. package/skills/dws/dingtalk-doc/references/doc/doc-update.md +87 -0
  297. package/skills/dws/dingtalk-doc/references/doc/format/doc-jsonml-cookbook.md +469 -0
  298. package/skills/dws/dingtalk-doc/references/doc/format/doc-jsonml-schema.json +526 -0
  299. package/skills/dws/dingtalk-doc/references/doc/format/doc-jsonml-schema.md +711 -0
  300. package/skills/dws/dingtalk-doc/references/doc/style/doc-create-workflow.md +414 -0
  301. package/skills/dws/dingtalk-doc/references/doc/style/doc-style-guideline.md +404 -0
  302. package/skills/dws/dingtalk-doc/references/doc/style/doc-update-workflow.md +316 -0
  303. package/skills/dws/dingtalk-doc/references/doc.md +82 -0
  304. package/skills/dws/dingtalk-doc/references/intent-guide.md +20 -0
  305. package/skills/dws/dingtalk-drive/SKILL.md +128 -0
  306. package/skills/dws/dingtalk-drive/references/drive.md +126 -0
  307. package/skills/dws/dingtalk-drive/references/folder-sync.md +40 -0
  308. package/skills/dws/dingtalk-drive/references/intent-guide.md +22 -0
  309. package/skills/dws/dingtalk-drive/references/lite-recipes.md +39 -0
  310. package/skills/dws/dingtalk-event/SKILL.md +106 -0
  311. package/skills/dws/dingtalk-event/references/event-im-keys.md +54 -0
  312. package/skills/dws/dingtalk-event/references/event-im-lifecycle.md +35 -0
  313. package/skills/dws/dingtalk-event/references/event-im-operations.md +52 -0
  314. package/skills/dws/dingtalk-event/references/event-im-output.md +39 -0
  315. package/skills/dws/dingtalk-event/references/event-im.md +58 -0
  316. package/skills/dws/dingtalk-event/references/event-oa.md +155 -0
  317. package/skills/dws/dingtalk-event/references/event-todo.md +50 -0
  318. package/skills/dws/dingtalk-event/references/event-voip.md +69 -0
  319. package/skills/dws/dingtalk-mail/SKILL.md +113 -0
  320. package/skills/dws/dingtalk-mail/references/09-mail.md +45 -0
  321. package/skills/dws/dingtalk-mail/references/intent-guide.md +8 -0
  322. package/skills/dws/dingtalk-mail/references/lite-recipes.md +45 -0
  323. package/skills/dws/dingtalk-mail/references/mail.md +1889 -0
  324. package/skills/dws/dingtalk-mail/references/recipes/conventions.md +44 -0
  325. package/skills/dws/dingtalk-mail/scripts/mail_send_with_cc.py +122 -0
  326. package/skills/dws/dingtalk-mail/scripts/mail_unread_summary.py +114 -0
  327. package/skills/dws/dingtalk-minutes/SKILL.md +116 -0
  328. package/skills/dws/dingtalk-minutes/references/07-minutes.md +149 -0
  329. package/skills/dws/dingtalk-minutes/references/10-minutes-speaker-match.md +32 -0
  330. package/skills/dws/dingtalk-minutes/references/11-minutes-speaker-correct.md +40 -0
  331. package/skills/dws/dingtalk-minutes/references/intent-guide.md +79 -0
  332. package/skills/dws/dingtalk-minutes/references/lite-recipes.md +25 -0
  333. package/skills/dws/dingtalk-minutes/references/minutes.md +210 -0
  334. package/skills/dws/dingtalk-minutes/references/recipes/conventions.md +33 -0
  335. package/skills/dws/dingtalk-minutes/scripts/minutes_recent_summary.py +196 -0
  336. package/skills/dws/dingtalk-minutes/scripts/test_minutes_scripts.py +222 -0
  337. package/skills/dws/dingtalk-misc/SKILL.md +55 -0
  338. package/skills/dws/dingtalk-misc/references/05-reporting.md +29 -0
  339. package/skills/dws/dingtalk-misc/references/agoal.md +302 -0
  340. package/skills/dws/dingtalk-misc/references/attendance-intent-guide.md +7 -0
  341. package/skills/dws/dingtalk-misc/references/attendance-report.md +109 -0
  342. package/skills/dws/dingtalk-misc/references/attendance-schedule.md +115 -0
  343. package/skills/dws/dingtalk-misc/references/attendance-vacation.md +129 -0
  344. package/skills/dws/dingtalk-misc/references/attendance.md +149 -0
  345. package/skills/dws/dingtalk-misc/references/dev/app.md +42 -0
  346. package/skills/dws/dingtalk-misc/references/dev/connect.md +108 -0
  347. package/skills/dws/dingtalk-misc/references/dev/credentials.md +26 -0
  348. package/skills/dws/dingtalk-misc/references/dev/dev-index.md +13 -0
  349. package/skills/dws/dingtalk-misc/references/dev/event.md +47 -0
  350. package/skills/dws/dingtalk-misc/references/dev/mcp.md +106 -0
  351. package/skills/dws/dingtalk-misc/references/dev/member.md +19 -0
  352. package/skills/dws/dingtalk-misc/references/dev/permission.md +56 -0
  353. package/skills/dws/dingtalk-misc/references/dev/recipes.md +58 -0
  354. package/skills/dws/dingtalk-misc/references/dev/robot.md +74 -0
  355. package/skills/dws/dingtalk-misc/references/dev/security.md +21 -0
  356. package/skills/dws/dingtalk-misc/references/dev/version.md +99 -0
  357. package/skills/dws/dingtalk-misc/references/dev/webapp.md +22 -0
  358. package/skills/dws/dingtalk-misc/references/devapp.md +148 -0
  359. package/skills/dws/dingtalk-misc/references/devdoc-intent-guide.md +7 -0
  360. package/skills/dws/dingtalk-misc/references/devdoc.md +42 -0
  361. package/skills/dws/dingtalk-misc/references/ding.md +184 -0
  362. package/skills/dws/dingtalk-misc/references/hrbrain.md +67 -0
  363. package/skills/dws/dingtalk-misc/references/live.md +23 -0
  364. package/skills/dws/dingtalk-misc/references/markdown.md +167 -0
  365. package/skills/dws/dingtalk-misc/references/oa/oa-form-components.md +345 -0
  366. package/skills/dws/dingtalk-misc/references/oa/oa-process-nodes.md +374 -0
  367. package/skills/dws/dingtalk-misc/references/oa.md +1014 -0
  368. package/skills/dws/dingtalk-misc/references/openapi-explorer.md +47 -0
  369. package/skills/dws/dingtalk-misc/references/pat.md +52 -0
  370. package/skills/dws/dingtalk-misc/references/profile.md +41 -0
  371. package/skills/dws/dingtalk-misc/references/recruit.md +89 -0
  372. package/skills/dws/dingtalk-misc/references/report-conventions.md +44 -0
  373. package/skills/dws/dingtalk-misc/references/report-intent-guide.md +11 -0
  374. package/skills/dws/dingtalk-misc/references/report-lite-recipes.md +25 -0
  375. package/skills/dws/dingtalk-misc/references/report.md +109 -0
  376. package/skills/dws/dingtalk-misc/references/sheet/sheet-batch-operations.md +104 -0
  377. package/skills/dws/dingtalk-misc/references/sheet/sheet-chart.md +69 -0
  378. package/skills/dws/dingtalk-misc/references/sheet/sheet-comment.md +166 -0
  379. package/skills/dws/dingtalk-misc/references/sheet/sheet-conditional-format.md +59 -0
  380. package/skills/dws/dingtalk-misc/references/sheet/sheet-dimension-operations.md +60 -0
  381. package/skills/dws/dingtalk-misc/references/sheet/sheet-dropdown.md +103 -0
  382. package/skills/dws/dingtalk-misc/references/sheet/sheet-export.md +135 -0
  383. package/skills/dws/dingtalk-misc/references/sheet/sheet-filter-view.md +51 -0
  384. package/skills/dws/dingtalk-misc/references/sheet/sheet-filter.md +181 -0
  385. package/skills/dws/dingtalk-misc/references/sheet/sheet-formula.md +192 -0
  386. package/skills/dws/dingtalk-misc/references/sheet/sheet-import.md +76 -0
  387. package/skills/dws/dingtalk-misc/references/sheet/sheet-media-image.md +263 -0
  388. package/skills/dws/dingtalk-misc/references/sheet/sheet-pivot-table.md +292 -0
  389. package/skills/dws/dingtalk-misc/references/sheet/sheet-range-operations.md +161 -0
  390. package/skills/dws/dingtalk-misc/references/sheet/sheet-read-data.md +60 -0
  391. package/skills/dws/dingtalk-misc/references/sheet/sheet-revision-changeset.md +46 -0
  392. package/skills/dws/dingtalk-misc/references/sheet/sheet-search-replace.md +120 -0
  393. package/skills/dws/dingtalk-misc/references/sheet/sheet-style-format.md +71 -0
  394. package/skills/dws/dingtalk-misc/references/sheet/sheet-version.md +84 -0
  395. package/skills/dws/dingtalk-misc/references/sheet/sheet-workbook.md +59 -0
  396. package/skills/dws/dingtalk-misc/references/sheet/sheet-write-data.md +93 -0
  397. package/skills/dws/dingtalk-misc/references/sheet-conventions.md +44 -0
  398. package/skills/dws/dingtalk-misc/references/sheet-intent-guide.md +12 -0
  399. package/skills/dws/dingtalk-misc/references/sheet.md +166 -0
  400. package/skills/dws/dingtalk-misc/references/skill.md +98 -0
  401. package/skills/dws/dingtalk-misc/references/unsupported-scripts.md +49 -0
  402. package/skills/dws/dingtalk-misc/references/url-patterns.md +144 -0
  403. package/skills/dws/dingtalk-misc/references/whiteboard/compose.md +200 -0
  404. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/01-overview.md +57 -0
  405. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/02-query.md +288 -0
  406. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/03-update.md +249 -0
  407. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/04-text-style.md +395 -0
  408. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/05-shape-frame-group-connector.md +186 -0
  409. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/06-vector-icon-path.md +313 -0
  410. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/07-examples-errors-write-support.md +270 -0
  411. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/08-catalogs.md +61 -0
  412. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1.md +54 -0
  413. package/skills/dws/dingtalk-misc/references/whiteboard/replace.md +41 -0
  414. package/skills/dws/dingtalk-misc/references/whiteboard/vector.md +51 -0
  415. package/skills/dws/dingtalk-misc/references/whiteboard.md +115 -0
  416. package/skills/dws/dingtalk-misc/scripts/aiapp_create_and_poll.py +138 -0
  417. package/skills/dws/dingtalk-misc/scripts/attendance_my_record.py +91 -0
  418. package/skills/dws/dingtalk-misc/scripts/attendance_report_checkin.py +440 -0
  419. package/skills/dws/dingtalk-misc/scripts/attendance_report_common.py +1698 -0
  420. package/skills/dws/dingtalk-misc/scripts/attendance_report_daily.py +558 -0
  421. package/skills/dws/dingtalk-misc/scripts/attendance_report_detail.py +809 -0
  422. package/skills/dws/dingtalk-misc/scripts/attendance_report_monthly.py +758 -0
  423. package/skills/dws/dingtalk-misc/scripts/attendance_report_record.py +947 -0
  424. package/skills/dws/dingtalk-misc/scripts/attendance_schedule_export.py +344 -0
  425. package/skills/dws/dingtalk-misc/scripts/attendance_schedule_import.py +498 -0
  426. package/skills/dws/dingtalk-misc/scripts/attendance_team_shift.py +89 -0
  427. package/skills/dws/dingtalk-misc/scripts/attendance_vacation_balance.py +617 -0
  428. package/skills/dws/dingtalk-misc/scripts/finance_daily_cashflow.py +70 -0
  429. package/skills/dws/dingtalk-misc/scripts/finance_expense_flow.py +138 -0
  430. package/skills/dws/dingtalk-misc/scripts/oa_batch_approve.py +158 -0
  431. package/skills/dws/dingtalk-misc/scripts/oa_pending_review.py +132 -0
  432. package/skills/dws/dingtalk-misc/scripts/report_received_today.py +315 -0
  433. package/skills/dws/dingtalk-misc/scripts/yida_custom_page_update.py +401 -0
  434. package/skills/dws/dingtalk-misc/scripts/yida_form_builder.py +526 -0
  435. package/skills/dws/dingtalk-misc/scripts/yida_form_fields.py +315 -0
  436. package/skills/dws/dingtalk-misc/scripts/yida_form_inspector.py +311 -0
  437. package/skills/dws/dingtalk-misc/scripts/yida_form_update.py +185 -0
  438. package/skills/dws/dingtalk-misc/scripts/yida_jsx_pipeline.py +1588 -0
  439. package/skills/dws/dingtalk-misc/scripts/yida_page_compiler.py +1441 -0
  440. package/skills/dws/dingtalk-misc/scripts/yida_page_generate.py +514 -0
  441. package/skills/dws/dingtalk-misc/scripts/yida_page_schema.py +519 -0
  442. package/skills/dws/dingtalk-misc/scripts/yida_page_self_check.py +398 -0
  443. package/skills/dws/dingtalk-misc/scripts/yida_process_flow.py +1478 -0
  444. package/skills/dws/dingtalk-misc/scripts/yida_process_update.py +589 -0
  445. package/skills/dws/dingtalk-misc/scripts/yida_report_builder.py +494 -0
  446. package/skills/dws/dingtalk-misc/scripts/yida_report_charts.py +1434 -0
  447. package/skills/dws/dingtalk-misc/scripts/yida_report_update.py +244 -0
  448. package/skills/dws/dingtalk-misc/scripts/yida_schema_common.py +340 -0
  449. package/skills/dws/dingtalk-shared/SKILL.md +83 -0
  450. package/skills/dws/dingtalk-shared/references/capability-limits.md +31 -0
  451. package/skills/dws/dingtalk-shared/references/channel-login.md +26 -0
  452. package/skills/dws/dingtalk-shared/references/confirmation.md +59 -0
  453. package/skills/dws/dingtalk-shared/references/error-codes.md +155 -0
  454. package/skills/dws/dingtalk-shared/references/global-reference.md +122 -0
  455. package/skills/dws/dingtalk-shared/references/intent-guide.md +537 -0
  456. package/skills/dws/dingtalk-shared/references/recipes/conventions.md +45 -0
  457. package/skills/dws/dingtalk-shared/references/recipes/lite-catalog.md +275 -0
  458. package/skills/dws/dingtalk-shared/references/recipes/meta.md +67 -0
  459. package/skills/dws/dingtalk-shared/references/routing.md +52 -0
  460. package/skills/dws/dingtalk-shared/references/runtime-contract.md +11 -0
  461. package/skills/dws/dingtalk-shared/references/schema-usage.md +88 -0
  462. package/skills/dws/dingtalk-shared/references/url-patterns.md +144 -0
  463. package/skills/dws/dingtalk-shared/references/workflow-routing.md +45 -0
  464. package/skills/dws/dingtalk-todo/SKILL.md +119 -0
  465. package/skills/dws/dingtalk-todo/references/02-task.md +70 -0
  466. package/skills/dws/dingtalk-todo/references/intent-guide.md +15 -0
  467. package/skills/dws/dingtalk-todo/references/lite-recipes.md +68 -0
  468. package/skills/dws/dingtalk-todo/references/recipes/conventions.md +44 -0
  469. package/skills/dws/dingtalk-todo/references/todo.md +517 -0
  470. package/skills/dws/dingtalk-todo/scripts/todo_batch_create.py +354 -0
  471. package/skills/dws/dingtalk-todo/scripts/todo_daily_summary.py +153 -0
  472. package/skills/dws/dingtalk-todo/scripts/todo_overdue_check.py +101 -0
  473. package/skills/dws/dingtalk-wiki/SKILL.md +117 -0
  474. package/skills/dws/dingtalk-wiki/references/intent-guide.md +14 -0
  475. package/skills/dws/dingtalk-wiki/references/lite-recipes.md +44 -0
  476. package/skills/dws/dingtalk-wiki/references/wiki-members.md +32 -0
  477. package/skills/dws/dingtalk-wiki/references/wiki-node-ops.md +46 -0
  478. package/skills/dws/dingtalk-wiki/references/wiki.md +65 -0
  479. package/skills/dws/provenance.json +27 -0
@@ -0,0 +1,1588 @@
1
+ #!/usr/bin/env python3
2
+ """yida_jsx_pipeline.py — JSX 全链路处理流水线(transform + field_check + lint)。
3
+
4
+ 三块能力一站式聚合(按 region 分段维护):
5
+ 1) [region: transform] JSX 源码 → React.createElement 调用(纯 Python,零依赖)。
6
+ 2) [region: field_check] 发布前字段 ID 对账(拉表单 schema 比对 JSX 引用)。
7
+ 3) [region: lint] 宜搭专属运行时静态检查(30 条规则)。
8
+
9
+ 公开 API(被 yida_custom_page_update.py 直接 import):
10
+ - transform_jsx(source) -> str
11
+ - field_check(code, app, *, schema_fetcher=None) -> dict
12
+ - lint_yida_source(source, filename=None) -> dict
13
+ - lint_check(source, filename=None) -> dict
14
+ - extract_form_uuids / extract_field_ids / fetch_form_fields (field_check 辅助)
15
+ - EVENT_NAME_ALIASES / THEN_CALLBACK_LINE_LIMIT / CALLBACK_SCAN_LINE_LIMIT (lint 常量)
16
+
17
+ CLI:
18
+ python yida_jsx_pipeline.py transform <jsx_file>
19
+ python yida_jsx_pipeline.py field-check --app APP_X --code-file page.jsx
20
+ python yida_jsx_pipeline.py lint page.jsx [--json] [--stdin]
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import json
26
+ import re
27
+ import subprocess
28
+ import sys
29
+ from pathlib import Path
30
+ from typing import Callable, Iterable
31
+
32
+ # ===========================================================================
33
+ # region: transform —— JSX → React.createElement
34
+ # ===========================================================================
35
+
36
+ _VOID_ELEMENTS = frozenset([
37
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
38
+ 'link', 'meta', 'param', 'source', 'track', 'wbr',
39
+ ])
40
+
41
+ _JSX_TAG_START = re.compile(r'<([A-Za-z_][A-Za-z0-9_.]*|>)')
42
+
43
+
44
+ def transform_jsx(source):
45
+ """将 JSX 源码中的 JSX 表达式转换为 React.createElement 调用。
46
+
47
+ Args:
48
+ source: 包含 JSX 的 JavaScript 源码字符串
49
+
50
+ Returns:
51
+ 转换后不含 JSX 的 JavaScript 源码
52
+ """
53
+ transformer = _JsxTransformer(source)
54
+ return transformer.transform()
55
+
56
+
57
+ class _JsxTransformer:
58
+ """递归下降 JSX 转换器"""
59
+
60
+ def __init__(self, source):
61
+ self.source = source
62
+ self.pos = 0
63
+ self.output = []
64
+
65
+ def transform(self):
66
+ while self.pos < len(self.source):
67
+ if self._at_jsx_start():
68
+ jsx_code = self._parse_jsx_element()
69
+ self.output.append(jsx_code)
70
+ else:
71
+ self.output.append(self.source[self.pos])
72
+ self.pos += 1
73
+ return ''.join(self.output)
74
+
75
+ def _peek(self, offset=0):
76
+ idx = self.pos + offset
77
+ if idx < len(self.source):
78
+ return self.source[idx]
79
+ return ''
80
+
81
+ def _remaining(self):
82
+ return self.source[self.pos:]
83
+
84
+ def _at_jsx_start(self):
85
+ """判断当前 < 是否是 JSX 标签开始(而非比较运算符)"""
86
+ if self._peek() != '<':
87
+ return False
88
+ next_char = self._peek(1)
89
+ if next_char == '>' or next_char == '/':
90
+ return self._is_jsx_context()
91
+ if next_char.isalpha() or next_char == '_':
92
+ return self._is_jsx_context()
93
+ return False
94
+
95
+ def _is_jsx_context(self):
96
+ """通过前文判断当前 < 是否处于 JSX 可出现的上下文"""
97
+ preceding = ''.join(self.output).rstrip()
98
+ if not preceding:
99
+ # 在递归调用中(如表达式容器 {<div>}),output 为空但来源是合法 JSX
100
+ # 检查 source 前面是否刚从表达式容器开始
101
+ if self.pos == 0:
102
+ return True
103
+ return False
104
+ last_char = preceding[-1]
105
+ # JSX 可出现在 return/( 之后、赋值/逗号/冒号/三元之后、逻辑运算符之后
106
+ if last_char in ('(', ',', ':', '?', '=', '&', '|', '!', ';', '{', '[', '\n'):
107
+ return True
108
+ # return <xxx
109
+ if preceding.endswith('return'):
110
+ return True
111
+ # 箭头函数 => <xxx
112
+ if preceding.endswith('=>'):
113
+ return True
114
+ if last_char == '>':
115
+ return False
116
+ return False
117
+
118
+ def _skip_whitespace(self):
119
+ while self.pos < len(self.source) and self.source[self.pos] in ' \t\n\r':
120
+ self.pos += 1
121
+
122
+ def _parse_jsx_element(self):
123
+ """解析一个完整的 JSX 元素,返回 createElement 调用字符串"""
124
+ assert self.source[self.pos] == '<'
125
+ self.pos += 1 # skip <
126
+
127
+ # Fragment: <>...</>
128
+ if self._peek() == '>':
129
+ self.pos += 1 # skip >
130
+ children = self._parse_jsx_children('')
131
+ # expect </>
132
+ if self.source[self.pos:self.pos + 3] == '</>':
133
+ self.pos += 3
134
+ return self._emit_create_element('React.Fragment', 'null', children)
135
+
136
+ # 解析标签名
137
+ tag_name = self._parse_tag_name()
138
+
139
+ # 解析属性
140
+ attrs = self._parse_attributes()
141
+
142
+ # 自闭合 /> 或 >
143
+ self._skip_whitespace()
144
+ if self.source[self.pos:self.pos + 2] == '/>':
145
+ self.pos += 2
146
+ props_str = self._attrs_to_props(attrs)
147
+ return self._emit_create_element(self._tag_ref(tag_name), props_str, [])
148
+
149
+ assert self.source[self.pos] == '>', f"Expected > at pos {self.pos}, got: {self.source[self.pos:self.pos+20]}"
150
+ self.pos += 1 # skip >
151
+
152
+ # 解析子元素
153
+ children = self._parse_jsx_children(tag_name)
154
+
155
+ # 解析闭合标签 </tagName>
156
+ self._parse_closing_tag(tag_name)
157
+
158
+ props_str = self._attrs_to_props(attrs)
159
+ return self._emit_create_element(self._tag_ref(tag_name), props_str, children)
160
+
161
+ def _parse_tag_name(self):
162
+ start = self.pos
163
+ while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] in '_.'):
164
+ self.pos += 1
165
+ return self.source[start:self.pos]
166
+
167
+ def _parse_attributes(self):
168
+ """解析 JSX 属性列表,返回 [(name, value_str), ...]"""
169
+ attrs = []
170
+ while self.pos < len(self.source):
171
+ self._skip_whitespace()
172
+ ch = self._peek()
173
+ if ch == '/' or ch == '>':
174
+ break
175
+ attr_name = self._parse_attribute_name()
176
+ if not attr_name:
177
+ break
178
+
179
+ self._skip_whitespace()
180
+ if self._peek() == '=':
181
+ self.pos += 1 # skip =
182
+ self._skip_whitespace()
183
+ value = self._parse_attribute_value()
184
+ attrs.append((attr_name, value))
185
+ else:
186
+ # boolean attribute
187
+ attrs.append((attr_name, 'true'))
188
+ return attrs
189
+
190
+ def _parse_attribute_name(self):
191
+ start = self.pos
192
+ while self.pos < len(self.source):
193
+ ch = self.source[self.pos]
194
+ if ch.isalnum() or ch in '_-':
195
+ self.pos += 1
196
+ else:
197
+ break
198
+ return self.source[start:self.pos]
199
+
200
+ def _parse_attribute_value(self):
201
+ """解析属性值,返回 JS 表达式字符串"""
202
+ ch = self._peek()
203
+ if ch == '"':
204
+ return self._parse_string_literal('"')
205
+ if ch == "'":
206
+ return self._parse_string_literal("'")
207
+ if ch == '{':
208
+ return self._parse_jsx_expression()
209
+ return 'true'
210
+
211
+ def _parse_string_literal(self, quote):
212
+ self.pos += 1 # skip opening quote
213
+ result = [quote]
214
+ while self.pos < len(self.source):
215
+ ch = self.source[self.pos]
216
+ if ch == '\\':
217
+ result.append(ch)
218
+ self.pos += 1
219
+ if self.pos < len(self.source):
220
+ result.append(self.source[self.pos])
221
+ self.pos += 1
222
+ elif ch == quote:
223
+ result.append(ch)
224
+ self.pos += 1
225
+ break
226
+ else:
227
+ result.append(ch)
228
+ self.pos += 1
229
+ return ''.join(result)
230
+
231
+ def _parse_jsx_expression(self):
232
+ """解析 { ... } 表达式容器,返回内部表达式字符串(可能含嵌套 JSX)"""
233
+ assert self.source[self.pos] == '{'
234
+ self.pos += 1 # skip {
235
+ content = self._read_balanced_braces()
236
+ # 递归转换内部 JSX
237
+ inner_transformed = transform_jsx(content)
238
+ return inner_transformed
239
+
240
+ def _read_balanced_braces(self):
241
+ """从当前位置读取到匹配 } 为止的内容(处理嵌套 {} 和字符串)"""
242
+ depth = 1
243
+ result = []
244
+ while self.pos < len(self.source) and depth > 0:
245
+ ch = self.source[self.pos]
246
+ if ch in ('"', "'", '`'):
247
+ string_content = self._consume_string_in_expr(ch)
248
+ result.append(string_content)
249
+ elif ch == '{':
250
+ depth += 1
251
+ result.append(ch)
252
+ self.pos += 1
253
+ elif ch == '}':
254
+ depth -= 1
255
+ if depth > 0:
256
+ result.append(ch)
257
+ self.pos += 1
258
+ elif ch == '/' and self.pos + 1 < len(self.source):
259
+ next_ch = self.source[self.pos + 1]
260
+ if next_ch == '/':
261
+ comment = self._consume_line_comment()
262
+ result.append(comment)
263
+ elif next_ch == '*':
264
+ comment = self._consume_block_comment()
265
+ result.append(comment)
266
+ else:
267
+ result.append(ch)
268
+ self.pos += 1
269
+ else:
270
+ result.append(ch)
271
+ self.pos += 1
272
+ return ''.join(result)
273
+
274
+ def _consume_string_in_expr(self, quote):
275
+ """消费字符串字面量(含转义),返回完整字符串"""
276
+ result = [quote]
277
+ self.pos += 1
278
+ if quote == '`':
279
+ return self._consume_template_literal(result)
280
+ while self.pos < len(self.source):
281
+ ch = self.source[self.pos]
282
+ result.append(ch)
283
+ self.pos += 1
284
+ if ch == '\\' and self.pos < len(self.source):
285
+ result.append(self.source[self.pos])
286
+ self.pos += 1
287
+ elif ch == quote:
288
+ break
289
+ return ''.join(result)
290
+
291
+ def _consume_template_literal(self, result):
292
+ """消费模板字面量 `...${expr}...`"""
293
+ while self.pos < len(self.source):
294
+ ch = self.source[self.pos]
295
+ result.append(ch)
296
+ self.pos += 1
297
+ if ch == '\\' and self.pos < len(self.source):
298
+ result.append(self.source[self.pos])
299
+ self.pos += 1
300
+ elif ch == '`':
301
+ break
302
+ elif ch == '$' and self.pos < len(self.source) and self.source[self.pos] == '{':
303
+ result.append('{')
304
+ self.pos += 1
305
+ # 读到匹配的 }
306
+ depth = 1
307
+ while self.pos < len(self.source) and depth > 0:
308
+ c = self.source[self.pos]
309
+ result.append(c)
310
+ self.pos += 1
311
+ if c == '{':
312
+ depth += 1
313
+ elif c == '}':
314
+ depth -= 1
315
+ return ''.join(result)
316
+
317
+ def _consume_line_comment(self):
318
+ start = self.pos
319
+ while self.pos < len(self.source) and self.source[self.pos] != '\n':
320
+ self.pos += 1
321
+ return self.source[start:self.pos]
322
+
323
+ def _consume_block_comment(self):
324
+ start = self.pos
325
+ self.pos += 2 # skip /*
326
+ while self.pos < len(self.source) - 1:
327
+ if self.source[self.pos] == '*' and self.source[self.pos + 1] == '/':
328
+ self.pos += 2
329
+ break
330
+ self.pos += 1
331
+ else:
332
+ self.pos = len(self.source)
333
+ return self.source[start:self.pos]
334
+
335
+ def _parse_jsx_children(self, parent_tag):
336
+ """解析子元素直到遇到 </parent_tag> 或 </>"""
337
+ children = []
338
+ text_buf = []
339
+
340
+ while self.pos < len(self.source):
341
+ # 检查闭合标签
342
+ if parent_tag and self.source[self.pos:self.pos + 2] == '</':
343
+ break
344
+ if not parent_tag and self.source[self.pos:self.pos + 3] == '</>':
345
+ break
346
+
347
+ ch = self.source[self.pos]
348
+
349
+ if ch == '{':
350
+ # 先 flush text
351
+ if text_buf:
352
+ text = ''.join(text_buf).strip()
353
+ if text:
354
+ children.append(repr(text))
355
+ text_buf = []
356
+ # 表达式子元素
357
+ self.pos += 1
358
+ expr_content = self._read_balanced_braces()
359
+ expr_transformed = transform_jsx(expr_content)
360
+ stripped = expr_transformed.strip()
361
+ # 跳过纯 JSX 注释表达式 {/* ... */} 或 {// ...}
362
+ # 否则会在 React.createElement 子节点列表里产生空位导致连续逗号 ,,
363
+ comment_stripped = re.sub(r'/\*[\s\S]*?\*/', '', stripped)
364
+ comment_stripped = re.sub(r'//[^\n]*', '', comment_stripped).strip()
365
+ if not comment_stripped:
366
+ continue
367
+ children.append(stripped)
368
+
369
+ elif ch == '<':
370
+ # 先 flush text
371
+ if text_buf:
372
+ text = ''.join(text_buf).strip()
373
+ if text:
374
+ children.append(repr(text))
375
+ text_buf = []
376
+ # 嵌套子元素
377
+ child_element = self._parse_jsx_element()
378
+ children.append(child_element)
379
+ else:
380
+ text_buf.append(ch)
381
+ self.pos += 1
382
+
383
+ # flush remaining text
384
+ if text_buf:
385
+ text = ''.join(text_buf).strip()
386
+ if text:
387
+ children.append(repr(text))
388
+
389
+ return children
390
+
391
+ def _parse_closing_tag(self, expected_tag):
392
+ """解析 </tagName>"""
393
+ if self.source[self.pos:self.pos + 2] != '</':
394
+ return
395
+ self.pos += 2 # skip </
396
+ tag = self._parse_tag_name()
397
+ self._skip_whitespace()
398
+ if self.pos < len(self.source) and self.source[self.pos] == '>':
399
+ self.pos += 1
400
+
401
+ def _tag_ref(self, tag_name):
402
+ """标签名转为 createElement 第一参数"""
403
+ if tag_name[0].isupper():
404
+ return tag_name # 组件引用
405
+ return repr(tag_name) # HTML 元素用字符串
406
+
407
+ def _attrs_to_props(self, attrs):
408
+ """将属性列表转为 props 对象字符串"""
409
+ if not attrs:
410
+ return 'null'
411
+ pairs = []
412
+ for name, value in attrs:
413
+ prop_name = self._normalize_attr_name(name)
414
+ pairs.append(f'{prop_name}: {value}')
415
+ return '{' + ', '.join(pairs) + '}'
416
+
417
+ def _normalize_attr_name(self, name):
418
+ """HTML 属性名 → JS 属性名"""
419
+ mapping = {
420
+ 'class': 'className',
421
+ 'for': 'htmlFor',
422
+ 'tabindex': 'tabIndex',
423
+ 'readonly': 'readOnly',
424
+ 'maxlength': 'maxLength',
425
+ 'colspan': 'colSpan',
426
+ 'rowspan': 'rowSpan',
427
+ 'enctype': 'encType',
428
+ 'contenteditable': 'contentEditable',
429
+ 'crossorigin': 'crossOrigin',
430
+ 'accesskey': 'accessKey',
431
+ 'autocomplete': 'autoComplete',
432
+ 'autofocus': 'autoFocus',
433
+ 'autoplay': 'autoPlay',
434
+ }
435
+ normalized = mapping.get(name.lower(), name)
436
+ # 带 - 的属性名需要引号
437
+ if '-' in normalized and not normalized.startswith('data-') and not normalized.startswith('aria-'):
438
+ return repr(normalized)
439
+ if '-' in normalized:
440
+ return repr(normalized)
441
+ return normalized
442
+
443
+ def _emit_create_element(self, tag, props, children):
444
+ """生成 React.createElement 调用"""
445
+ args = [tag, props]
446
+ args.extend(children)
447
+ return 'React.createElement(' + ', '.join(args) + ')'
448
+
449
+
450
+ # ===========================================================================
451
+ # region: field_check —— 发布前字段 ID 对账
452
+ # ===========================================================================
453
+
454
+ # JSX 里的表单引用:'FORM-XXX' 字面量
455
+ _FORM_UUID_RE = re.compile(r"['\"](FORM-[A-Z0-9]+)['\"]")
456
+
457
+ # 字段 ID 命名规范:以 Field/SelectField/... 结尾再接下划线 + 大小写数字
458
+ # 用宽松前缀(任何字母)+ 强约束的 fieldId 命名格式(见 yida-custom-page-codegen.md)
459
+ _FIELD_ID_LITERAL_RE = re.compile(r"['\"]([a-zA-Z][a-zA-Z0-9]*Field_[A-Za-z0-9]+)['\"]")
460
+
461
+
462
+ def extract_form_uuids(code: str) -> set[str]:
463
+ """提取 JSX 中所有 'FORM-XXX' 字面量。"""
464
+ return set(_FORM_UUID_RE.findall(code))
465
+
466
+
467
+ def extract_field_ids(code: str) -> set[str]:
468
+ """提取 JSX 中所有疑似字段 ID 的字符串字面量(xxxField_yyyy 形式)。"""
469
+ return set(_FIELD_ID_LITERAL_RE.findall(code))
470
+
471
+
472
+ def _run_dws(args: list[str]) -> tuple[object | None, str | None]:
473
+ try:
474
+ result = subprocess.run(["dws"] + args, capture_output=True, text=True, timeout=120)
475
+ except FileNotFoundError:
476
+ return None, "找不到 'dws' 命令(请确认已安装并在 PATH)"
477
+ except subprocess.TimeoutExpired:
478
+ return None, "dws 调用超时"
479
+ if result.returncode != 0:
480
+ err = result.stderr.strip() or result.stdout.strip()
481
+ return None, f"dws 失败 (exit {result.returncode}): {err[:200]}"
482
+ try:
483
+ return json.loads(result.stdout), None
484
+ except json.JSONDecodeError as e:
485
+ return None, f"输出非 JSON: {e}"
486
+
487
+
488
+ _FIELD_ID_VALUE_RE = re.compile(r'\b[A-Za-z]+Field_[A-Za-z0-9]+\b')
489
+ _COMPONENT_LIST_KEYS = ("components", "fields", "data", "items", "result", "children", "list")
490
+
491
+
492
+ def _walk_values(obj):
493
+ if isinstance(obj, dict):
494
+ yield obj
495
+ for value in obj.values():
496
+ yield from _walk_values(value)
497
+ elif isinstance(obj, list):
498
+ for item in obj:
499
+ yield from _walk_values(item)
500
+
501
+
502
+ def _find_nested_value(obj, keys):
503
+ if isinstance(obj, dict):
504
+ for key in keys:
505
+ value = obj.get(key)
506
+ if value not in (None, ""):
507
+ return value
508
+ for value in obj.values():
509
+ found = _find_nested_value(value, keys)
510
+ if found not in (None, ""):
511
+ return found
512
+ elif isinstance(obj, list):
513
+ for item in obj:
514
+ found = _find_nested_value(item, keys)
515
+ if found not in (None, ""):
516
+ return found
517
+ return None
518
+
519
+
520
+ def _normalize_i18n_label(label):
521
+ if isinstance(label, str):
522
+ raw = label.strip()
523
+ if raw.startswith("{") and raw.endswith("}"):
524
+ try:
525
+ parsed = json.loads(raw)
526
+ except json.JSONDecodeError:
527
+ return label
528
+ if isinstance(parsed, dict):
529
+ return (parsed.get("zh_CN") or parsed.get("zh-CN")
530
+ or parsed.get("text") or parsed.get("pureEn_US")
531
+ or parsed.get("en_US") or label)
532
+ return label
533
+ if isinstance(label, dict):
534
+ return (label.get("zh_CN") or label.get("zh-CN") or label.get("text")
535
+ or label.get("pureEn_US") or label.get("en_US") or "")
536
+ return label or ""
537
+
538
+
539
+ def _find_field_id(obj):
540
+ value = _find_nested_value(obj, ("fieldId", "fieldCode", "field_id", "fieldKey"))
541
+ if isinstance(value, str):
542
+ m = _FIELD_ID_VALUE_RE.search(value)
543
+ if m:
544
+ return m.group(0)
545
+ return value
546
+
547
+ key_value = _find_nested_value(obj, ("key", "name", "id"))
548
+ if isinstance(key_value, str):
549
+ m = _FIELD_ID_VALUE_RE.search(key_value)
550
+ if m:
551
+ return m.group(0)
552
+
553
+ text = json.dumps(obj, ensure_ascii=False) if isinstance(obj, (dict, list)) else str(obj)
554
+ m = _FIELD_ID_VALUE_RE.search(text)
555
+ return m.group(0) if m else None
556
+
557
+
558
+ def _extract_component_items(data):
559
+ if isinstance(data, dict):
560
+ for key in _COMPONENT_LIST_KEYS:
561
+ value = data.get(key)
562
+ if isinstance(value, list):
563
+ return value
564
+ if isinstance(value, dict):
565
+ nested = _extract_component_items(value)
566
+ if nested:
567
+ return nested
568
+ if isinstance(data, list):
569
+ return data
570
+
571
+ found = []
572
+ seen = set()
573
+ for item in _walk_values(data):
574
+ if not isinstance(item, dict):
575
+ continue
576
+ fid = _find_field_id(item)
577
+ if fid and fid not in seen:
578
+ seen.add(fid)
579
+ found.append(item)
580
+ return found
581
+
582
+
583
+ def fetch_form_fields(app: str, form_uuid: str) -> tuple[list[dict] | None, str | None]:
584
+ """调用 dws yida form components 拿表单字段。"""
585
+ data, err = _run_dws(["yida", "form", "components", "--app", app,
586
+ "--form", form_uuid, "--format", "json"])
587
+ if err:
588
+ return None, err
589
+ return _extract_component_items(data), None
590
+
591
+
592
+ def _normalize_field(comp: dict) -> dict:
593
+ field_id = _find_field_id(comp)
594
+ label = (_find_nested_value(comp, ("label", "title", "text", "displayName", "nameCn"))
595
+ or "")
596
+ label = _normalize_i18n_label(label)
597
+ return {
598
+ "fieldId": field_id,
599
+ "label": label,
600
+ "componentName": (_find_nested_value(comp, ("componentName", "type", "component"))
601
+ or ""),
602
+ }
603
+
604
+
605
+ def field_check(code: str, app: str, *,
606
+ schema_fetcher: Callable[[str], tuple[list[dict] | None, str | None]] | None = None) -> dict:
607
+ """字段 ID 对账。
608
+
609
+ Args:
610
+ code: JSX 源码
611
+ app: 应用 appType
612
+ schema_fetcher: 可注入的字段拉取函数(测试用),签名 (form_uuid)->(fields, err)
613
+
614
+ Returns:
615
+ {
616
+ ok: bool,
617
+ errors: [{type, fieldId?, message}],
618
+ warnings: [{type, message}],
619
+ info: {checkedForms, referencedFieldCount, knownFieldCount, missing}
620
+ }
621
+ """
622
+ fetcher = schema_fetcher or (lambda fu: fetch_form_fields(app, fu))
623
+
624
+ form_uuids = extract_form_uuids(code)
625
+ referenced = extract_field_ids(code)
626
+
627
+ if not form_uuids:
628
+ return {
629
+ "ok": True, "errors": [], "warnings": [],
630
+ "info": {"skipped": "no_form_uuid_in_jsx",
631
+ "referencedFieldCount": len(referenced)},
632
+ }
633
+
634
+ if not referenced:
635
+ return {
636
+ "ok": True, "errors": [], "warnings": [],
637
+ "info": {"skipped": "no_field_id_referenced",
638
+ "checkedForms": sorted(form_uuids)},
639
+ }
640
+
641
+ all_known: dict[str, dict] = {}
642
+ fetch_errors: list[dict] = []
643
+ for fu in sorted(form_uuids):
644
+ fields, err = fetcher(fu)
645
+ if err or fields is None:
646
+ fetch_errors.append({"form": fu, "error": err or "unknown"})
647
+ continue
648
+ for c in fields:
649
+ f = _normalize_field(c)
650
+ if f["fieldId"]:
651
+ all_known[f["fieldId"]] = {**f, "form": fu}
652
+
653
+ # 所有表单都拉不到 → 无法对账,作为 warning 放行(避免登录态过期把发布卡死)
654
+ if fetch_errors and not all_known:
655
+ return {
656
+ "ok": True,
657
+ "errors": [],
658
+ "warnings": [{
659
+ "type": "schema_fetch_all_failed",
660
+ "message": ("无法拉取任何表单 schema,跳过字段对账。"
661
+ "可能原因:登录态过期 / 网络异常 / form_uuid 不属于当前应用。"
662
+ f"详情:{fetch_errors}"),
663
+ }],
664
+ "info": {"checkedForms": sorted(form_uuids),
665
+ "referencedFieldCount": len(referenced),
666
+ "knownFieldCount": 0},
667
+ }
668
+
669
+ missing = sorted(referenced - all_known.keys())
670
+ errors: list[dict] = []
671
+ if missing:
672
+ forms_hint = ", ".join(sorted(form_uuids))
673
+ for fid in missing:
674
+ errors.append({
675
+ "type": "field_id_not_found",
676
+ "fieldId": fid,
677
+ "message": (
678
+ f"字段 ID `{fid}` 在表单 schema 中不存在。\n"
679
+ f" 已检查的表单:{forms_hint}\n"
680
+ f" 修复建议:\n"
681
+ f" 1) 用 `python yida_form_inspector.py --action fields-snippet "
682
+ f"--app {app} --form <FORM-XXX>` 重新生成 FIELDS 常量\n"
683
+ f" 2) 或核对 var FORM_UUID 是否填错"
684
+ ),
685
+ })
686
+
687
+ warnings: list[dict] = []
688
+ for fe in fetch_errors:
689
+ warnings.append({
690
+ "type": "schema_fetch_partial",
691
+ "message": (f"表单 {fe['form']} schema 拉取失败(部分对账已跳过):{fe['error']}"),
692
+ })
693
+
694
+ return {
695
+ "ok": len(errors) == 0,
696
+ "errors": errors,
697
+ "warnings": warnings,
698
+ "info": {
699
+ "checkedForms": sorted(form_uuids),
700
+ "referencedFieldCount": len(referenced),
701
+ "knownFieldCount": len(all_known),
702
+ "missing": missing,
703
+ },
704
+ }
705
+
706
+
707
+ # ===========================================================================
708
+ # region: lint —— 宜搭专属运行时静态检查(30 条规则)
709
+ # ===========================================================================
710
+
711
+ THEN_CALLBACK_LINE_LIMIT = 50
712
+ CALLBACK_SCAN_LINE_LIMIT = 80
713
+
714
+ EVENT_NAME_ALIASES = {
715
+ 'onclick': 'onClick', 'onchange': 'onChange', 'oninput': 'onChange',
716
+ 'onsubmit': 'onSubmit', 'onkeydown': 'onKeyDown', 'onkeyup': 'onKeyUp',
717
+ 'onkeypress': 'onKeyPress', 'onfocus': 'onFocus', 'onblur': 'onBlur',
718
+ 'onmouseenter': 'onMouseEnter', 'onmouseleave': 'onMouseLeave',
719
+ 'onmousedown': 'onMouseDown', 'onmouseup': 'onMouseUp',
720
+ 'onmousemove': 'onMouseMove', 'oncompositionstart': 'onCompositionStart',
721
+ 'oncompositionend': 'onCompositionEnd',
722
+ }
723
+
724
+
725
+ # ---- helpers ----
726
+
727
+ def _is_in_comment_or_string(line: str, match_index: int) -> bool:
728
+ """判断行内某偏移是否在 // 注释或字符串中(简化版)。"""
729
+ if match_index is None or match_index < 0:
730
+ return False
731
+ before = line[:match_index]
732
+ if '//' in before:
733
+ return True
734
+ quotes = sum(1 for ch in before if ch in ("'", '"'))
735
+ return quotes % 2 != 0
736
+
737
+
738
+ def _strip_comments_and_strings(source: str) -> str:
739
+ """剥离 /**/ // 块/行注释 和 ' " ` 字符串,便于纯结构扫描。"""
740
+ out = re.sub(r'/\*[\s\S]*?\*/', '', source)
741
+ out = re.sub(r'//.*$', '', out, flags=re.M)
742
+ out = re.sub(r"'(?:\\.|[^'\\])*'", "''", out)
743
+ out = re.sub(r'"(?:\\.|[^"\\])*"', '""', out)
744
+ out = re.sub(r'`(?:\\.|[^`\\])*`', '``', out)
745
+ return out
746
+
747
+
748
+ def _extract_function_body(lines: list[str], start_line_index: int, function_index: int) -> str:
749
+ """从 lines[start_line_index] 起,找到第一个 `{` 并按 brace 深度抽出函数体(最多扫 80 行)。"""
750
+ end_line_index = min(len(lines), start_line_index + CALLBACK_SCAN_LINE_LIMIT)
751
+ source = '\n'.join(lines[start_line_index:end_line_index])
752
+ open_brace_index = source.find('{', function_index)
753
+ if open_brace_index < 0:
754
+ return ''
755
+
756
+ brace_depth = 0
757
+ quote: str | None = None
758
+ in_line_comment = False
759
+ in_block_comment = False
760
+
761
+ i = open_brace_index
762
+ n = len(source)
763
+ while i < n:
764
+ ch = source[i]
765
+ nxt = source[i + 1] if i + 1 < n else ''
766
+
767
+ if in_line_comment:
768
+ if ch == '\n':
769
+ in_line_comment = False
770
+ i += 1
771
+ continue
772
+ if in_block_comment:
773
+ if ch == '*' and nxt == '/':
774
+ in_block_comment = False
775
+ i += 2
776
+ continue
777
+ i += 1
778
+ continue
779
+ if quote:
780
+ if ch == '\\':
781
+ i += 2
782
+ continue
783
+ if ch == quote:
784
+ quote = None
785
+ i += 1
786
+ continue
787
+ if ch == '/' and nxt == '/':
788
+ in_line_comment = True
789
+ i += 2
790
+ continue
791
+ if ch == '/' and nxt == '*':
792
+ in_block_comment = True
793
+ i += 2
794
+ continue
795
+ if ch in ("'", '"', '`'):
796
+ quote = ch
797
+ i += 1
798
+ continue
799
+ if ch == '{':
800
+ brace_depth += 1
801
+ elif ch == '}':
802
+ brace_depth -= 1
803
+ if brace_depth == 0:
804
+ return source[open_brace_index + 1:i]
805
+ i += 1
806
+ return source[open_brace_index + 1:]
807
+
808
+
809
+ def _function_callback_uses_this(lines: list[str], line_index: int, match_index: int) -> bool:
810
+ line = lines[line_index]
811
+ fn_idx = line.find('function', match_index)
812
+ if fn_idx < 0:
813
+ return False
814
+ body = _extract_function_body(lines, line_index, fn_idx)
815
+ return bool(re.search(r'\bthis\b', _strip_comments_and_strings(body)))
816
+
817
+
818
+ def _extract_named_export_function_body(source: str, name: str) -> tuple[int, str] | None:
819
+ """Extract body of `export function name(...) { ... }` from full source."""
820
+ m = re.search(r'\bexport\s+function\s+' + re.escape(name) + r'\s*\(', source)
821
+ if not m:
822
+ return None
823
+ open_brace = source.find('{', m.end())
824
+ if open_brace < 0:
825
+ return None
826
+
827
+ brace_depth = 0
828
+ quote: str | None = None
829
+ in_line_comment = False
830
+ in_block_comment = False
831
+ i = open_brace
832
+ n = len(source)
833
+ while i < n:
834
+ ch = source[i]
835
+ nxt = source[i + 1] if i + 1 < n else ''
836
+ if in_line_comment:
837
+ if ch == '\n':
838
+ in_line_comment = False
839
+ i += 1
840
+ continue
841
+ if in_block_comment:
842
+ if ch == '*' and nxt == '/':
843
+ in_block_comment = False
844
+ i += 2
845
+ continue
846
+ i += 1
847
+ continue
848
+ if quote:
849
+ if ch == '\\':
850
+ i += 2
851
+ continue
852
+ if ch == quote:
853
+ quote = None
854
+ i += 1
855
+ continue
856
+ if ch == '/' and nxt == '/':
857
+ in_line_comment = True
858
+ i += 2
859
+ continue
860
+ if ch == '/' and nxt == '*':
861
+ in_block_comment = True
862
+ i += 2
863
+ continue
864
+ if ch in ("'", '"', '`'):
865
+ quote = ch
866
+ i += 1
867
+ continue
868
+ if ch == '{':
869
+ brace_depth += 1
870
+ elif ch == '}':
871
+ brace_depth -= 1
872
+ if brace_depth == 0:
873
+ start_line = source.count('\n', 0, open_brace) + 1
874
+ return start_line, source[open_brace + 1:i]
875
+ i += 1
876
+ start_line = source.count('\n', 0, open_brace) + 1
877
+ return start_line, source[open_brace + 1:]
878
+
879
+
880
+ # ---- disable map ----
881
+
882
+ _DISABLE_LINE_RE = re.compile(
883
+ r'(?:dws|openyida)-lint-disable-line(?:\s+([a-z0-9_,\-\s]+))?', re.I)
884
+ _DISABLE_NEXT_RE = re.compile(
885
+ r'(?:dws|openyida)-lint-disable-next-line(?:\s+([a-z0-9_,\-\s]+))?', re.I)
886
+
887
+
888
+ def _parse_disable_rules(raw: str | None) -> list[str]:
889
+ if not raw or not raw.strip():
890
+ return ['*']
891
+ return [r.strip() for r in re.split(r'[,\s]+', raw) if r.strip()]
892
+
893
+
894
+ def _build_disable_map(lines: list[str]) -> dict[int, set[str]]:
895
+ dmap: dict[int, set[str]] = {}
896
+ for idx, line in enumerate(lines):
897
+ line_no = idx + 1
898
+ m = _DISABLE_LINE_RE.search(line)
899
+ if m:
900
+ dmap.setdefault(line_no, set()).update(_parse_disable_rules(m.group(1)))
901
+ m2 = _DISABLE_NEXT_RE.search(line)
902
+ if m2:
903
+ dmap.setdefault(line_no + 1, set()).update(_parse_disable_rules(m2.group(1)))
904
+ return dmap
905
+
906
+
907
+ def _is_rule_disabled(dmap: dict[int, set[str]], line: int, rule: str) -> bool:
908
+ rules = dmap.get(line)
909
+ if not rules:
910
+ return False
911
+ return '*' in rules or rule in rules
912
+
913
+
914
+ def _push_issue(lst: list[dict], line: int, rule: str, message: str,
915
+ dmap: dict[int, set[str]]) -> None:
916
+ if _is_rule_disabled(dmap, line, rule):
917
+ return
918
+ for it in lst:
919
+ if it['line'] == line and it['rule'] == rule and it['message'] == message:
920
+ return
921
+ lst.append({'line': line, 'rule': rule, 'message': message})
922
+
923
+
924
+ # ---- aux detectors ----
925
+
926
+ _THEN_START_RE = re.compile(
927
+ r'\.then\s*\(\s*(?:function\s*\(|(?:\([^)]*\)|[a-zA-Z_$]\w*)\s*=>)')
928
+
929
+
930
+ def _detect_large_then_callbacks(lines: list[str]) -> list[dict]:
931
+ results: list[dict] = []
932
+ in_then = False
933
+ brace_depth = 0
934
+ then_start_line = 0
935
+ then_body_start_line = 0
936
+
937
+ for i, line in enumerate(lines):
938
+ trimmed = line.strip()
939
+ if trimmed.startswith('//') or trimmed.startswith('*') or trimmed.startswith('/*'):
940
+ continue
941
+ if not in_then:
942
+ m = _THEN_START_RE.search(line)
943
+ if m and not _is_in_comment_or_string(line, m.start()):
944
+ in_then = True
945
+ then_start_line = i + 1
946
+ brace_depth = 0
947
+ after = line[m.start():]
948
+ for ch in after:
949
+ if ch == '{':
950
+ brace_depth += 1
951
+ elif ch == '}':
952
+ brace_depth -= 1
953
+ then_body_start_line = i + 1
954
+ else:
955
+ for ch in line:
956
+ if ch == '{':
957
+ brace_depth += 1
958
+ elif ch == '}':
959
+ brace_depth -= 1
960
+ if brace_depth <= 0:
961
+ callback_line_count = (i + 1) - then_body_start_line
962
+ if callback_line_count > THEN_CALLBACK_LINE_LIMIT:
963
+ results.append({'line': then_start_line, 'lineCount': callback_line_count})
964
+ in_then = False
965
+ return results
966
+
967
+
968
+ _YIDA_CALL_RE = re.compile(r'this\.utils\.yida\.[A-Za-z_$][\w$]*\s*\(')
969
+
970
+
971
+ def _detect_yida_calls_without_catch(source: str, warnings: list[dict],
972
+ dmap: dict[int, set[str]]) -> None:
973
+ for m in _YIDA_CALL_RE.finditer(source):
974
+ line = source.count('\n', 0, m.start()) + 1
975
+ statement = source[m.start():m.start() + 600]
976
+ if '.catch(' not in statement:
977
+ _push_issue(warnings, line, 'yida-api-catch',
978
+ '调用 this.utils.yida.* 应跟 .catch() 防止 Promise 异常吞没', dmap)
979
+
980
+
981
+ _LABEL_FORMATTER_RE = re.compile(
982
+ r'\blabel\s*:\s*\{[\s\S]{0,1200}?\bformatter\s*:\s*function\b')
983
+
984
+
985
+ def _detect_echarts_rich_label_formatter(source: str, warnings: list[dict],
986
+ dmap: dict[int, set[str]]) -> None:
987
+ for m in _LABEL_FORMATTER_RE.finditer(source):
988
+ line = source.count('\n', 0, m.start()) + 1
989
+ block = source[m.start():m.start() + 1600]
990
+ uses_rich = bool(re.search(r'\brich\s*:', block) or
991
+ re.search(r"return\s+['\"`][\s\S]{0,300}?\{[A-Za-z0-9_]+\|", block))
992
+ if uses_rich:
993
+ _push_issue(warnings, line, 'echarts-rich-label-formatter',
994
+ 'echarts label.formatter 使用 rich 模板时建议改用对象写法或 string formatter', dmap)
995
+
996
+
997
+ # ---- lifecycle scan (replaces AST ExportNamedDeclaration / ClassMethod) ----
998
+
999
+ _EXPORT_FN_RE = re.compile(r'\bexport\s+function\s+([A-Za-z][\w]*)\s*\(')
1000
+ _LIFECYCLE_DECL_RE = re.compile(
1001
+ r'(?:^|[\s;{])(componentDidMount|componentWillUnmount)\s*\(')
1002
+
1003
+
1004
+ def _scan_lifecycle(source: str, errors: list[dict], dmap: dict[int, set[str]]) -> None:
1005
+ for m in _EXPORT_FN_RE.finditer(source):
1006
+ name = m.group(1)
1007
+ line = source.count('\n', 0, m.start()) + 1
1008
+ if name.lower() == 'didmount' and name != 'didMount':
1009
+ _push_issue(errors, line, 'lifecycle-case',
1010
+ f'生命周期函数大小写错误:{name} 应为 didMount', dmap)
1011
+ elif name.lower() == 'didunmount' and name != 'didUnmount':
1012
+ _push_issue(errors, line, 'lifecycle-case',
1013
+ f'生命周期函数大小写错误:{name} 应为 didUnmount', dmap)
1014
+ if name in ('componentDidMount', 'componentWillUnmount'):
1015
+ expected = 'didMount' if name == 'componentDidMount' else 'didUnmount'
1016
+ _push_issue(errors, line, 'react-lifecycle-method',
1017
+ f'宜搭页面不使用 React 生命周期:{name} 应为 {expected}', dmap)
1018
+ # 类方法 / 对象方法形式(避免重复行)
1019
+ for m in _LIFECYCLE_DECL_RE.finditer(source):
1020
+ name = m.group(1)
1021
+ line = source.count('\n', 0, m.start()) + 1
1022
+ expected = 'didMount' if name == 'componentDidMount' else 'didUnmount'
1023
+ _push_issue(errors, line, 'react-lifecycle-method',
1024
+ f'宜搭页面不使用 React 生命周期:{name} 应为 {expected}', dmap)
1025
+
1026
+
1027
+ # ---- jsx element scan (replaces AST JSXOpeningElement) ----
1028
+
1029
+ # JSX 起始标签匹配:必须自己处理 brace/quote 平衡,避免 `=>` 中的 `>` 被误当作
1030
+ # 标签结束符(这是 [^<>] 简化正则的固有缺陷)。
1031
+ _JSX_TAG_NAME_RE = re.compile(r'<([a-zA-Z][\w.\-]*)')
1032
+ # 在属性串内找事件属性 onXxx=
1033
+ _EVENT_ATTR_RE = re.compile(r'\b(on[A-Za-z]\w*)\s*=\s*(\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}|"[^"]*"|\'[^\']*\')')
1034
+ _LOWER_EVENT_RE = re.compile(r'\bon[a-z]\w+\b')
1035
+
1036
+
1037
+ def _iter_jsx_open_tags(source: str):
1038
+ """逐个 yield JSX 起始标签 `(start_pos, tag_name, attr_str)`。
1039
+
1040
+ 自己跟踪 `{}` 深度和字符串边界,避免 `() => x` 中的 `>` 被误当作标签结束。
1041
+ """
1042
+ n = len(source)
1043
+ i = 0
1044
+ while i < n:
1045
+ m = _JSX_TAG_NAME_RE.match(source, i)
1046
+ if not m:
1047
+ i += 1
1048
+ continue
1049
+ tag_name = m.group(1)
1050
+ j = m.end()
1051
+ brace = 0
1052
+ quote: str | None = None
1053
+ end_pos = -1
1054
+ while j < n:
1055
+ ch = source[j]
1056
+ nxt = source[j + 1] if j + 1 < n else ''
1057
+ if quote:
1058
+ if ch == '\\':
1059
+ j += 2
1060
+ continue
1061
+ if ch == quote:
1062
+ quote = None
1063
+ j += 1
1064
+ continue
1065
+ if ch in ("'", '"', '`'):
1066
+ quote = ch
1067
+ j += 1
1068
+ continue
1069
+ if ch == '{':
1070
+ brace += 1
1071
+ elif ch == '}':
1072
+ brace -= 1
1073
+ elif ch == '<' and brace == 0:
1074
+ # 嵌套标签,提前结束当前扫描
1075
+ break
1076
+ elif ch == '>' and brace == 0:
1077
+ end_pos = j
1078
+ break
1079
+ j += 1
1080
+ if end_pos < 0:
1081
+ i = m.end()
1082
+ continue
1083
+ attr_str = source[m.end():end_pos]
1084
+ # 兼容自闭合:<tag .../> 的最后 / 落在 attr_str 末尾,去掉
1085
+ if attr_str.endswith('/'):
1086
+ attr_str = attr_str[:-1]
1087
+ yield (m.start(), tag_name, attr_str)
1088
+ i = end_pos + 1
1089
+
1090
+
1091
+ def _attrs_split(attr_str: str) -> dict[str, str]:
1092
+ """粗略拆解属性串,返回 {name: rawValue}。仅用于 button 缺 handler 检测。"""
1093
+ if not attr_str:
1094
+ return {}
1095
+ result: dict[str, str] = {}
1096
+ # 匹配 name="..." name='...' name={...} name (布尔属性)
1097
+ pattern = re.compile(
1098
+ r'([a-zA-Z][\w-]*)(?:\s*=\s*("[^"]*"|\'[^\']*\'|\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}))?')
1099
+ for m in pattern.finditer(attr_str):
1100
+ name = m.group(1)
1101
+ value = m.group(2) or ''
1102
+ result[name] = value
1103
+ return result
1104
+
1105
+
1106
+ def _is_interactive_button(attrs: dict[str, str]) -> bool:
1107
+ if 'disabled' in attrs or 'aria-disabled' in attrs:
1108
+ return True
1109
+ btn_type = attrs.get('type', '').strip('"\'').lower()
1110
+ if btn_type == 'submit':
1111
+ return True
1112
+ for k in ('onClick', 'onMouseDown', 'onKeyDown'):
1113
+ if k in attrs:
1114
+ return True
1115
+ return False
1116
+
1117
+
1118
+ def _canonical_event_name(name: str) -> str:
1119
+ if name in EVENT_NAME_ALIASES:
1120
+ return EVENT_NAME_ALIASES[name]
1121
+ if not name or len(name) <= 2:
1122
+ return name
1123
+ return 'on' + name[2].upper() + name[3:]
1124
+
1125
+
1126
+ _BARE_HANDLER_IDENT_RE = re.compile(r'^(?:handle|on)[A-Z]\w*$')
1127
+ _THIS_OR_SELF_MEMBER_RE = re.compile(r'^(?:this|self)\.[A-Za-z_$][\w$]*$')
1128
+ _BIND_THIS_RE = re.compile(r'\.bind\s*\(\s*this\s*\)\s*$')
1129
+
1130
+
1131
+ def _expr_is_bare_handler_ref(expr: str) -> bool:
1132
+ expr = expr.strip()
1133
+ if _THIS_OR_SELF_MEMBER_RE.match(expr):
1134
+ return True
1135
+ if _BARE_HANDLER_IDENT_RE.match(expr):
1136
+ return True
1137
+ return False
1138
+
1139
+
1140
+ def _arrow_returns_bare_handler(value: str) -> bool:
1141
+ """识别 {() => this.foo} / {(a) => handleX} / {() => { this.foo; }} 这类无效箭头。"""
1142
+ inner = value.strip()
1143
+ if inner.startswith('{') and inner.endswith('}'):
1144
+ inner = inner[1:-1].strip()
1145
+ # 匹配箭头函数:(...) => body 或 ident => body
1146
+ m = re.match(r'(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*(.+)$', inner, re.S)
1147
+ if not m:
1148
+ return False
1149
+ body = m.group(1).strip()
1150
+ if body.startswith('{') and body.endswith('}'):
1151
+ body = body[1:-1].strip().rstrip(';').strip()
1152
+ # 多语句:取最后一个表达式判断
1153
+ parts = [p.strip() for p in re.split(r';|\n', body) if p.strip()]
1154
+ return any(_expr_is_bare_handler_ref(p) for p in parts)
1155
+ return _expr_is_bare_handler_ref(body)
1156
+
1157
+
1158
+ # 复用行扫描的 message 模板,确保和 _DIRECT_METHOD_RE/_BIND_INLINE_RE 触发的告警
1159
+ # 内容完全一致 → _push_issue 的 (line, rule, message) 去重生效。
1160
+ _MSG_DIRECT_METHOD = 'onXxx={this.foo} 会导致 this 上下文丢失,请改用箭头函数'
1161
+ _MSG_BIND_THIS = 'onXxx={fn.bind(this)} 在宜搭运行时不稳定,请改用箭头函数'
1162
+
1163
+
1164
+ def _scan_jsx_elements(source: str, errors: list[dict], warnings: list[dict],
1165
+ dmap: dict[int, set[str]]) -> None:
1166
+ for start_pos, tag, attr_str in _iter_jsx_open_tags(source):
1167
+ line = source.count('\n', 0, start_pos) + 1
1168
+ is_lower_tag = tag[0].islower()
1169
+ attrs = _attrs_split(attr_str)
1170
+
1171
+ if is_lower_tag and tag == 'button' and not _is_interactive_button(attrs):
1172
+ _push_issue(errors, line, 'button-missing-handler',
1173
+ '<button> 必须挂载 onClick / onMouseDown / onKeyDown 之一,'
1174
+ '或显式 disabled / type="submit"', dmap)
1175
+
1176
+ # 事件属性扫描
1177
+ for evt_match in _EVENT_ATTR_RE.finditer(attr_str):
1178
+ attr_name = evt_match.group(1)
1179
+ attr_value = evt_match.group(2) or ''
1180
+ attr_line_offset = attr_str[:evt_match.start()].count('\n')
1181
+ attr_line = line + attr_line_offset
1182
+
1183
+ # event-lowercase: onclick / onchange 等小写形式
1184
+ if _LOWER_EVENT_RE.fullmatch(attr_name) and attr_name[2:3].islower():
1185
+ canon = _canonical_event_name(attr_name)
1186
+ _push_issue(errors, attr_line, 'event-lowercase',
1187
+ f'JSX 事件属性大小写错误:{attr_name} 应为 {canon}', dmap)
1188
+ continue
1189
+
1190
+ # 仅处理 onXxx 形式
1191
+ if not re.match(r'^on[A-Z]', attr_name):
1192
+ continue
1193
+
1194
+ # 字符串值(非大括号包裹):event-call-result
1195
+ if attr_value.startswith('"') or attr_value.startswith("'"):
1196
+ _push_issue(errors, attr_line, 'event-call-result',
1197
+ f'{attr_name} 必须传函数引用,不能是字符串', dmap)
1198
+ continue
1199
+
1200
+ if not (attr_value.startswith('{') and attr_value.endswith('}')):
1201
+ continue
1202
+
1203
+ inner = attr_value[1:-1].strip()
1204
+ # event-direct-method: this.xxx 或 self.xxx 单独成员引用
1205
+ if _THIS_OR_SELF_MEMBER_RE.match(inner):
1206
+ _push_issue(errors, attr_line, 'event-direct-method',
1207
+ _MSG_DIRECT_METHOD, dmap)
1208
+ continue
1209
+ # event-bind-this: xxx.bind(this)
1210
+ if _BIND_THIS_RE.search(inner):
1211
+ _push_issue(errors, attr_line, 'event-bind-this',
1212
+ _MSG_BIND_THIS, dmap)
1213
+ continue
1214
+ # event-call-result: 形如 fn(args) 的调用结果
1215
+ if (re.search(r'\)\s*$', inner) and '=>' not in inner
1216
+ and not inner.startswith('function')
1217
+ and not inner.startswith('(')):
1218
+ if re.match(r'^[A-Za-z_$][\w$.]*\s*\(', inner):
1219
+ _push_issue(errors, attr_line, 'event-call-result',
1220
+ f'{attr_name}={{fn()}} 是把调用结果当 handler,请去掉括号',
1221
+ dmap)
1222
+ continue
1223
+ # event-noop-arrow: () => this.foo 或 () => { this.foo }
1224
+ if '=>' in inner and _arrow_returns_bare_handler(attr_value):
1225
+ _push_issue(errors, attr_line, 'event-noop-arrow',
1226
+ f'{attr_name}={{() => this.foo}} 没有真正调用 handler,'
1227
+ f'请改成 () => this.foo()', dmap)
1228
+
1229
+
1230
+ # ---- main lint ----
1231
+
1232
+ _RENDERJSX_RE = re.compile(r'export\s+function\s+renderJsx\s*\(')
1233
+ _REACT_HOOKS_RE = re.compile(r'\buse(State|Effect|Memo|Callback|Ref|Reducer|Context)\s*\(')
1234
+ _UNSUPPORTED_HOOKS_RE = re.compile(r'\buse(Memo|Callback|Ref|Reducer|Context)\s*\(')
1235
+ _USE_STATE_RE = re.compile(r'\buseState\s*\(')
1236
+ _SUPPORTED_USE_STATE_PREFIX_RE = re.compile(
1237
+ r'\b(?:var|let|const)\s+\[\s*[A-Za-z_$][\w$]*\s*,\s*[A-Za-z_$][\w$]*\s*\]\s*=\s*$')
1238
+ _EXPORT_DEFAULT_RE = re.compile(r'export\s+default\b')
1239
+ _EXPORT_DEFAULT_PAGE_RE = re.compile(r'export\s+default\s+function\s+Page\s*\(')
1240
+ _HAS_JSX_TAG_RE = re.compile(r'<[A-Za-z][\w.-]*(\s|>)')
1241
+
1242
+ _IMPORT_REQUIRE_RE = re.compile(r'^\s*import\s+|\brequire\s*\(')
1243
+ _SUPPORTED_MODERN_IMPORT_RE = re.compile(
1244
+ r'^\s*import\b[\s\S]*?\bfrom\s+[\'"](?:react|react-dom)[\'"]\s*;?\s*$'
1245
+ r'|^\s*import\s+[\'"](?:react|react-dom)[\'"]\s*;?\s*$')
1246
+ _LEGACY_ECHARTS_MAP_RE = re.compile(
1247
+ r'(?:echarts(?:\.min)?\.js/map/js/china|echarts/map/js/china|map/js/china(?:\.js)?)',
1248
+ re.I)
1249
+ _EVENT_FUNCTION_RE = re.compile(r'on[A-Z]\w+=\{function\b')
1250
+ _DIRECT_METHOD_RE = re.compile(r'on[A-Z]\w+=\{this\.[A-Za-z_$][\w$]*\s*\}')
1251
+ _BIND_INLINE_RE = re.compile(r'on[A-Z]\w+=\{[^}]*\.bind\(this\)[^}]*\}')
1252
+ _CONST_LET_RE = re.compile(r'\b(const|let)\s+')
1253
+ _COMPUTED_RE = re.compile(r'\{\s*\[[^\]]+\]\s*:')
1254
+ _PAD_METHOD_RE = re.compile(r'\.(padStart|padEnd)\s*\(')
1255
+ _MAP_FILTER_FN_RE = re.compile(r'\.(map|filter)\s*\(\s*function\b')
1256
+ _FOREACH_FN_RE = re.compile(r'\.forEach\s*\(\s*function\b')
1257
+ _CONTROLLED_INPUT_RE = re.compile(r'<input\b[^>]*\bvalue=')
1258
+ _NATIVE_SELECT_RE = re.compile(r'<select\b')
1259
+ _IFRAME_NAV_RE = re.compile(
1260
+ r'<a\b(?=[^>]*(?:aliwork\.com|yidaapps\.com|/preview/|/workbench))'
1261
+ r'(?![^>]*\btarget=([\'"]?)_top\1)'
1262
+ r'(?![^>]*\btarget=([\'"]?)_blank\2)[^>]*>',
1263
+ re.I)
1264
+ _TOP_LOCATION_RE = re.compile(
1265
+ r'window\.location\.href\s*=\s*[^;\n]*(?:aliwork\.com|yidaapps\.com|/preview/|/workbench)',
1266
+ re.I)
1267
+ _PAGE_SIZE_RE = re.compile(r'\bpageSize\s*:\s*(\d+)')
1268
+ _CUSTOM_STATE_DECL_RE = re.compile(r'\bvar\s+_customState\b')
1269
+ _CUSTOM_STATE_REF_RE = re.compile(r'(?<![.\w$])_customState\b')
1270
+ _SELF_METHOD_ASSIGN_RE = re.compile(r'\b(?:self|this)\.[A-Za-z_$][\w$]*\s*=\s*function\s*\(')
1271
+ _RENDER_YIDA_API_RE = re.compile(r'\b(?:Yida\.api|(?:self|this)\.utils\.yida)\b')
1272
+ _DID_MOUNT_EMULATION_RE = re.compile(r'\bdidMountCalled\b')
1273
+
1274
+
1275
+ def _has_unsupported_use_state_pattern(source: str) -> bool:
1276
+ """Return True if any useState call is not compiler-lowerable.
1277
+
1278
+ The page compiler currently lowers only declaration-form hooks:
1279
+ var [state, setState] = useState(initialValue);
1280
+ It supports multiline initial values, but the declaration prefix itself must
1281
+ be on the same line as useState so that the generated state name is stable.
1282
+ """
1283
+ for match in _USE_STATE_RE.finditer(source):
1284
+ line_start = source.rfind('\n', 0, match.start()) + 1
1285
+ prefix = source[line_start:match.start()]
1286
+ if not _SUPPORTED_USE_STATE_PREFIX_RE.search(prefix):
1287
+ return True
1288
+ return False
1289
+
1290
+
1291
+ def _scan_render_side_effects(source: str, errors: list[dict],
1292
+ dmap: dict[int, set[str]]) -> None:
1293
+ render = _extract_named_export_function_body(source, 'renderJsx')
1294
+ if render is None:
1295
+ return
1296
+ start_line, body = render
1297
+ clean_body = _strip_comments_and_strings(body)
1298
+
1299
+ for m in _SELF_METHOD_ASSIGN_RE.finditer(clean_body):
1300
+ line = start_line + clean_body.count('\n', 0, m.start())
1301
+ _push_issue(errors, line, 'method-defined-in-render',
1302
+ '不要在 renderJsx 内写 self.xxx = function()。'
1303
+ '宜搭运行时方法必须提升为 export function xxx(),'
1304
+ 'renderJsx 只负责渲染', dmap)
1305
+
1306
+ for m in _DID_MOUNT_EMULATION_RE.finditer(clean_body):
1307
+ line = start_line + clean_body.count('\n', 0, m.start())
1308
+ _push_issue(errors, line, 'lifecycle-emulated-in-render',
1309
+ '不要在 renderJsx 内用 didMountCalled 模拟生命周期。'
1310
+ '初始化加载请写 export function didMount()', dmap)
1311
+
1312
+ for m in _RENDER_YIDA_API_RE.finditer(clean_body):
1313
+ line = start_line + clean_body.count('\n', 0, m.start())
1314
+ _push_issue(errors, line, 'api-call-in-render',
1315
+ 'renderJsx 中不要直接调用 Yida API。'
1316
+ '异步加载放到 didMount 或事件触发的 export function 中', dmap)
1317
+
1318
+
1319
+ def lint_yida_source(source: str, filename: str | None = None) -> dict:
1320
+ """对 JSX 源码做静态检查,返回 {'errors': [...], 'warnings': [...]}。
1321
+
1322
+ 每条记录形如:{'line': int, 'rule': str, 'message': str}。
1323
+ rule 命名采用稳定字符串,便于错误码映射与 disable 注释引用。
1324
+ """
1325
+ errors: list[dict] = []
1326
+ warnings: list[dict] = []
1327
+ lines = source.split('\n')
1328
+ dmap = _build_disable_map(lines)
1329
+
1330
+ has_render_jsx = bool(_RENDERJSX_RE.search(source))
1331
+ is_modern_authoring = bool(_EXPORT_DEFAULT_PAGE_RE.search(source))
1332
+ if not has_render_jsx and not is_modern_authoring:
1333
+ _push_issue(errors, 1, 'missing-render-jsx',
1334
+ '页面入口必须为 export function renderJsx() {...},'
1335
+ '或使用可编译的 export default function Page() authoring 模式', dmap)
1336
+ if _UNSUPPORTED_HOOKS_RE.search(source):
1337
+ _push_issue(errors, 1, 'unsupported-hooks',
1338
+ '当前 Python compiler 仅支持 useState 和 useEffect(..., []),'
1339
+ '不支持 useMemo/useCallback/useRef/useReducer/useContext', dmap)
1340
+ elif _REACT_HOOKS_RE.search(source) and not is_modern_authoring:
1341
+ _push_issue(errors, 1, 'react-hooks',
1342
+ 'Hooks 只能出现在 export default function Page() authoring 模式中,'
1343
+ '发布前由 Python compiler 降级为 _customState + didMount', dmap)
1344
+ if is_modern_authoring and _has_unsupported_use_state_pattern(source):
1345
+ _push_issue(errors, 1, 'unsupported-use-state-pattern',
1346
+ '当前 Python compiler 仅支持 var/let/const [state, setState] = useState(init) '
1347
+ '这种解构声明写法', dmap)
1348
+ if _EXPORT_DEFAULT_RE.search(source) and not is_modern_authoring:
1349
+ _push_issue(errors, 1, 'export-default',
1350
+ '仅允许 export default function Page() authoring 模式;'
1351
+ '否则请使用具名 export function renderJsx()', dmap)
1352
+ if filename and filename.lower().endswith('.js') and has_render_jsx \
1353
+ and _HAS_JSX_TAG_RE.search(source):
1354
+ _push_issue(warnings, 1, 'jsx-extension',
1355
+ '文件含 JSX 元素,建议把扩展名从 .js 改为 .jsx', dmap)
1356
+
1357
+ code_without_strings = _strip_comments_and_strings(source)
1358
+ if _CUSTOM_STATE_REF_RE.search(code_without_strings) \
1359
+ and not _CUSTOM_STATE_DECL_RE.search(code_without_strings):
1360
+ _push_issue(errors, 1, 'missing-custom-state',
1361
+ '代码引用了 _customState 但未声明 var _customState = {...}; '
1362
+ '发布后会报 ReferenceError', dmap)
1363
+ _scan_render_side_effects(source, errors, dmap)
1364
+
1365
+ for idx, line in enumerate(lines):
1366
+ line_no = idx + 1
1367
+ trimmed = line.strip()
1368
+ if trimmed.startswith('//') or trimmed.startswith('*') or trimmed.startswith('/*'):
1369
+ continue
1370
+
1371
+ m = _IMPORT_REQUIRE_RE.search(line)
1372
+ if m and not _is_in_comment_or_string(line, m.start()) \
1373
+ and not (is_modern_authoring and _SUPPORTED_MODERN_IMPORT_RE.search(line)):
1374
+ _push_issue(errors, line_no, 'import-require',
1375
+ '宜搭自定义页面不允许 import / require,'
1376
+ '请直接使用 React / this.utils.* 等运行时全局对象', dmap)
1377
+
1378
+ m = _LEGACY_ECHARTS_MAP_RE.search(line)
1379
+ if m:
1380
+ _push_issue(errors, line_no, 'echarts-legacy-map-china',
1381
+ '禁用 echarts/map/js/china,请改用 echarts/map/json/* 数据加载方式', dmap)
1382
+
1383
+ m = _EVENT_FUNCTION_RE.search(line)
1384
+ if m and not _is_in_comment_or_string(line, m.start()) \
1385
+ and _function_callback_uses_this(lines, idx, m.start()):
1386
+ _push_issue(errors, line_no, 'event-function',
1387
+ 'JSX 事件用 function(){...} 且内部含 this,会丢失 this 指向,'
1388
+ '请改用箭头函数', dmap)
1389
+
1390
+ m = _DIRECT_METHOD_RE.search(line)
1391
+ if m and not _is_in_comment_or_string(line, m.start()):
1392
+ _push_issue(errors, line_no, 'event-direct-method',
1393
+ _MSG_DIRECT_METHOD, dmap)
1394
+
1395
+ m = _BIND_INLINE_RE.search(line)
1396
+ if m and not _is_in_comment_or_string(line, m.start()):
1397
+ _push_issue(errors, line_no, 'event-bind-this',
1398
+ _MSG_BIND_THIS, dmap)
1399
+
1400
+ m = _CONST_LET_RE.search(line)
1401
+ if m and not _is_in_comment_or_string(line, m.start()):
1402
+ _push_issue(warnings, line_no, 'const-let',
1403
+ '宜搭页面 Babel 输出 ES5,建议用 var 而非 const/let 以避免编译问题', dmap)
1404
+
1405
+ m = _COMPUTED_RE.search(line)
1406
+ if m and not _is_in_comment_or_string(line, m.start()):
1407
+ _push_issue(errors, line_no, 'computed-property',
1408
+ '对象不允许使用 ES6 computed key({[xxx]: ...}),'
1409
+ '请改用 var obj = {}; obj[xxx] = ...', dmap)
1410
+
1411
+ m = _PAD_METHOD_RE.search(line)
1412
+ if m and not _is_in_comment_or_string(line, m.start()):
1413
+ _push_issue(warnings, line_no, 'pad-method',
1414
+ f'{m.group(1)} 是 ES2017,宜搭运行时不一定支持,'
1415
+ f'请改用手写补零', dmap)
1416
+
1417
+ m = _MAP_FILTER_FN_RE.search(line)
1418
+ if m and not _is_in_comment_or_string(line, m.start()) \
1419
+ and _function_callback_uses_this(lines, idx, m.start()):
1420
+ _push_issue(errors, line_no, 'array-callback-function',
1421
+ f'{m.group(1)}(function(){{...}}) 内部含 this 会丢失上下文,'
1422
+ f'请改用箭头函数', dmap)
1423
+
1424
+ m = _FOREACH_FN_RE.search(line)
1425
+ if m and not _is_in_comment_or_string(line, m.start()) \
1426
+ and _function_callback_uses_this(lines, idx, m.start()):
1427
+ _push_issue(warnings, line_no, 'foreach-callback-function',
1428
+ 'forEach(function(){...}) 内部含 this 建议改用箭头函数', dmap)
1429
+
1430
+ m = _CONTROLLED_INPUT_RE.search(line)
1431
+ if m and not _is_in_comment_or_string(line, m.start()):
1432
+ _push_issue(errors, line_no, 'controlled-input',
1433
+ '<input value={...}> 在宜搭运行时表现异常,'
1434
+ '请改用 defaultValue 或宜搭 Input 组件', dmap)
1435
+
1436
+ m = _NATIVE_SELECT_RE.search(line)
1437
+ if m and not _is_in_comment_or_string(line, m.start()):
1438
+ _push_issue(warnings, line_no, 'native-select-ui',
1439
+ '原生 <select> 样式与宜搭风格不一致,建议用 Select 组件', dmap)
1440
+
1441
+ m = _IFRAME_NAV_RE.search(line)
1442
+ if m and not _is_in_comment_or_string(line, m.start()):
1443
+ _push_issue(warnings, line_no, 'iframe-self-navigation',
1444
+ '<a> 跳转宜搭内部链接时建议加 target="_top" 或 "_blank",'
1445
+ '否则在 iframe 内会卡住', dmap)
1446
+
1447
+ m = _TOP_LOCATION_RE.search(line)
1448
+ if m and not _is_in_comment_or_string(line, m.start()) \
1449
+ and 'window.top.location' not in line:
1450
+ _push_issue(warnings, line_no, 'iframe-self-navigation',
1451
+ 'window.location.href 跳转宜搭内部链接时建议改用 window.top.location', dmap)
1452
+
1453
+ m = _PAGE_SIZE_RE.search(line)
1454
+ if m and int(m.group(1)) > 100 \
1455
+ and not _is_in_comment_or_string(line, m.start()):
1456
+ _push_issue(errors, line_no, 'page-size-limit',
1457
+ f'pageSize={m.group(1)} 超过宜搭限制 100,请分页拉取', dmap)
1458
+
1459
+ for r in _detect_large_then_callbacks(lines):
1460
+ _push_issue(warnings, r['line'], 'large-then-callback',
1461
+ f'.then() 回调超过 {THEN_CALLBACK_LINE_LIMIT} 行(实际 {r["lineCount"]} 行),'
1462
+ f'建议拆分为独立函数', dmap)
1463
+ _detect_yida_calls_without_catch(source, warnings, dmap)
1464
+ _detect_echarts_rich_label_formatter(source, warnings, dmap)
1465
+ _scan_lifecycle(source, errors, dmap)
1466
+ _scan_jsx_elements(source, errors, warnings, dmap)
1467
+
1468
+ errors.sort(key=lambda x: (x['line'], x['rule']))
1469
+ warnings.sort(key=lambda x: (x['line'], x['rule']))
1470
+ return {'errors': errors, 'warnings': warnings}
1471
+
1472
+
1473
+ def lint_check(source: str, filename: str | None = None) -> dict:
1474
+ """与 field_check 同款返回结构。
1475
+
1476
+ Returns: {'ok': bool, 'errors': [...], 'warnings': [...], 'info': {...}}
1477
+ """
1478
+ result = lint_yida_source(source, filename)
1479
+ return {
1480
+ 'ok': len(result['errors']) == 0,
1481
+ 'errors': result['errors'],
1482
+ 'warnings': result['warnings'],
1483
+ 'info': {
1484
+ 'errorCount': len(result['errors']),
1485
+ 'warningCount': len(result['warnings']),
1486
+ },
1487
+ }
1488
+
1489
+
1490
+ # ===========================================================================
1491
+ # region: unified CLI(transform / field-check / lint)
1492
+ # ===========================================================================
1493
+
1494
+ def _print_lint_human(result: dict) -> int:
1495
+ errs = result['errors']
1496
+ warns = result['warnings']
1497
+ if not errs and not warns:
1498
+ print('[OK] lint 通过(30 条规则)')
1499
+ return 0
1500
+ if errs:
1501
+ print(f'[FAIL] {len(errs)} 个错误:', file=sys.stderr)
1502
+ for e in errs:
1503
+ print(f" L{e['line']:>4} [{e['rule']}] {e['message']}", file=sys.stderr)
1504
+ if warns:
1505
+ print(f'[WARN] {len(warns)} 个警告:')
1506
+ for w in warns:
1507
+ print(f" L{w['line']:>4} [{w['rule']}] {w['message']}")
1508
+ return 1 if errs else 0
1509
+
1510
+
1511
+ def _cli_transform(args: argparse.Namespace) -> int:
1512
+ src = Path(args.source).expanduser().read_text(encoding='utf-8')
1513
+ out = transform_jsx(src)
1514
+ if args.output:
1515
+ Path(args.output).expanduser().write_text(out, encoding='utf-8')
1516
+ else:
1517
+ sys.stdout.write(out)
1518
+ return 0
1519
+
1520
+
1521
+ def _cli_field_check(args: argparse.Namespace) -> int:
1522
+ if args.code_file:
1523
+ code = Path(args.code_file).expanduser().read_text(encoding='utf-8')
1524
+ elif args.code:
1525
+ code = args.code
1526
+ else:
1527
+ print('错误: 必须提供 --code-file 或 --code', file=sys.stderr)
1528
+ return 1
1529
+ result = field_check(code, args.app)
1530
+ print(json.dumps(result, ensure_ascii=False, indent=2))
1531
+ return 0 if result['ok'] else 1
1532
+
1533
+
1534
+ def _cli_lint(args: argparse.Namespace) -> int:
1535
+ if args.stdin:
1536
+ src = sys.stdin.read()
1537
+ fname = args.filename
1538
+ elif args.source:
1539
+ path = Path(args.source).expanduser().resolve()
1540
+ if not path.exists():
1541
+ print(f'错误: 文件不存在 {path}', file=sys.stderr)
1542
+ return 1
1543
+ src = path.read_text(encoding='utf-8')
1544
+ fname = str(path)
1545
+ else:
1546
+ print('错误: 必须提供 source 路径或 --stdin', file=sys.stderr)
1547
+ return 1
1548
+ result = lint_yida_source(src, fname)
1549
+ if args.json:
1550
+ print(json.dumps({'ok': len(result['errors']) == 0, **result},
1551
+ ensure_ascii=False, indent=2))
1552
+ return 0 if not result['errors'] else 1
1553
+ return _print_lint_human(result)
1554
+
1555
+
1556
+ def main() -> int:
1557
+ ap = argparse.ArgumentParser(
1558
+ description='宜搭自定义页面 JSX 流水线(transform + field-check + lint)')
1559
+ sub = ap.add_subparsers(dest='cmd', required=True)
1560
+
1561
+ sp_t = sub.add_parser('transform', help='JSX → React.createElement')
1562
+ sp_t.add_argument('source', help='JSX 源文件路径')
1563
+ sp_t.add_argument('--output', help='输出文件(默认 stdout)')
1564
+
1565
+ sp_fc = sub.add_parser('field-check', help='发布前字段 ID 对账')
1566
+ sp_fc.add_argument('--app', required=True, help='应用 appType')
1567
+ sp_fc.add_argument('--code-file', help='JSX 源文件')
1568
+ sp_fc.add_argument('--code', help='JSX 内联字符串')
1569
+
1570
+ sp_l = sub.add_parser('lint', help='宜搭专属 30 条规则静态检查')
1571
+ sp_l.add_argument('source', nargs='?', help='JSX 源文件路径(与 --stdin 二选一)')
1572
+ sp_l.add_argument('--stdin', action='store_true', help='从 stdin 读取源码')
1573
+ sp_l.add_argument('--filename', help='用于 jsx-extension 规则的文件名(仅 --stdin 模式有用)')
1574
+ sp_l.add_argument('--json', action='store_true', help='输出 JSON 而非文本')
1575
+
1576
+ args = ap.parse_args()
1577
+ if args.cmd == 'transform':
1578
+ return _cli_transform(args)
1579
+ if args.cmd == 'field-check':
1580
+ return _cli_field_check(args)
1581
+ if args.cmd == 'lint':
1582
+ return _cli_lint(args)
1583
+ ap.error(f'未知命令: {args.cmd}')
1584
+ return 1
1585
+
1586
+
1587
+ if __name__ == '__main__':
1588
+ sys.exit(main())