@alipay/alipay-aipay 1.7.0 → 1.7.1

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 (169) hide show
  1. package/README.md +30 -0
  2. package/dist/commands/install-current.js +15 -7
  3. package/dist/commands/install.js +20 -16
  4. package/dist/commands/uninstall.js +8 -3
  5. package/dist/commands/update.js +34 -16
  6. package/dist/index.js +10 -4
  7. package/dist/utils/channels.js +67 -0
  8. package/dist/utils/config.js +15 -11
  9. package/dist/utils/file.js +23 -10
  10. package/dist/utils/npm.js +19 -33
  11. package/package.json +8 -3
  12. package/skill/SKILL.md +2 -0
  13. package/skill/references/normal/json/release-channel.json +1 -0
  14. package/skill/references/normal/scripts/self_update.mjs +62 -17
  15. package/skill/references/normal/self-update.md +3 -1
  16. package/skill-pre/SKILL.md +65 -0
  17. package/skill-pre/VERSION +1 -0
  18. package/skill-pre/references/integration/flow.md +602 -0
  19. package/skill-pre/references/integration/modules/alipay-sdk-reminder.md +281 -0
  20. package/skill-pre/references/integration/modules/checklist.md +154 -0
  21. package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +199 -0
  22. package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +68 -0
  23. package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +83 -0
  24. package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +80 -0
  25. package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +73 -0
  26. package/skill-pre/references/integration/modules/code-examples/csharp/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +62 -0
  27. package/skill-pre/references/integration/modules/code-examples/csharp/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +132 -0
  28. package/skill-pre/references/integration/modules/code-examples/csharp/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +640 -0
  29. package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +184 -0
  30. package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +72 -0
  31. package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +96 -0
  32. package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +81 -0
  33. package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +77 -0
  34. package/skill-pre/references/integration/modules/code-examples/java/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +69 -0
  35. package/skill-pre/references/integration/modules/code-examples/java/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +138 -0
  36. package/skill-pre/references/integration/modules/code-examples/java/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemoController.md +623 -0
  37. package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +135 -0
  38. package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +51 -0
  39. package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +63 -0
  40. package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +59 -0
  41. package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +54 -0
  42. package/skill-pre/references/integration/modules/code-examples/nodejs/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +84 -0
  43. package/skill-pre/references/integration/modules/code-examples/nodejs/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +71 -0
  44. package/skill-pre/references/integration/modules/code-examples/nodejs/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +498 -0
  45. package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +157 -0
  46. package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +59 -0
  47. package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +73 -0
  48. package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +69 -0
  49. package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +65 -0
  50. package/skill-pre/references/integration/modules/code-examples/php/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +59 -0
  51. package/skill-pre/references/integration/modules/code-examples/php/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +110 -0
  52. package/skill-pre/references/integration/modules/code-examples/php/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +806 -0
  53. package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +128 -0
  54. package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +57 -0
  55. package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +70 -0
  56. package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +65 -0
  57. package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +62 -0
  58. package/skill-pre/references/integration/modules/code-examples/python/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +51 -0
  59. package/skill-pre/references/integration/modules/code-examples/python/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +47 -0
  60. package/skill-pre/references/integration/modules/code-examples/python/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +541 -0
  61. package/skill-pre/references/integration/modules/interface-contract/aipay-interface-contract.md +52 -0
  62. package/skill-pre/references/integration/modules/interface-contract/apppay-interface-contract.md +45 -0
  63. package/skill-pre/references/integration/modules/interface-contract/common-trade-contract.md +42 -0
  64. package/skill-pre/references/integration/modules/interface-contract/notify-url-contract.md +68 -0
  65. package/skill-pre/references/integration/modules/interface-contract/webpay-interface-contract.md +44 -0
  66. package/skill-pre/references/integration/modules/interface-guide.md +61 -0
  67. package/skill-pre/references/integration/modules/product-decision.md +73 -0
  68. package/skill-pre/references/integration/modules/sandbox/a2m-sandbox-test.md +235 -0
  69. package/skill-pre/references/integration/modules/sandbox/alipay-sandbox-tool.md +142 -0
  70. package/skill-pre/references/integration/modules/sandbox/sandbox-setup-guide.md +239 -0
  71. package/skill-pre/references/integration/modules/scripts/local_402_sandbox_pay.py +900 -0
  72. package/skill-pre/references/integration/modules/scripts/sandbox_config.sh +347 -0
  73. package/skill-pre/references/integration/modules/troubleshooting-flow.md +28 -0
  74. package/skill-pre/references/normal/alipay-cli-env.md +169 -0
  75. package/skill-pre/references/normal/conversation-telemetry.md +101 -0
  76. package/skill-pre/references/normal/full-process-routing.md +92 -0
  77. package/skill-pre/references/normal/integration-worker.md +90 -0
  78. package/skill-pre/references/normal/json/customer-messages.json +1 -0
  79. package/skill-pre/references/normal/json/managed-jq-assets.json +1 -0
  80. package/skill-pre/references/normal/json/policy-rules.json +1 -0
  81. package/skill-pre/references/normal/json/release-channel.json +1 -0
  82. package/skill-pre/references/normal/rejection-guide.md +41 -0
  83. package/skill-pre/references/normal/scripts/agent_platform.mjs +43 -0
  84. package/skill-pre/references/normal/scripts/alipay_cli_env.mjs +34 -0
  85. package/skill-pre/references/normal/scripts/alipay_cli_env.sh +55 -0
  86. package/skill-pre/references/normal/scripts/alipay_cli_refresh.mjs +129 -0
  87. package/skill-pre/references/normal/scripts/auth_runtime_runner.mjs +60 -0
  88. package/skill-pre/references/normal/scripts/checklist_sections.mjs +43 -0
  89. package/skill-pre/references/normal/scripts/common.sh +365 -0
  90. package/skill-pre/references/normal/scripts/conversation_telemetry.mjs +105 -0
  91. package/skill-pre/references/normal/scripts/detect_dev_tool.sh +79 -0
  92. package/skill-pre/references/normal/scripts/flow_loader.mjs +237 -0
  93. package/skill-pre/references/normal/scripts/integration_context_runner.mjs +151 -0
  94. package/skill-pre/references/normal/scripts/integration_message_runner.mjs +1456 -0
  95. package/skill-pre/references/normal/scripts/interaction_reply.mjs +140 -0
  96. package/skill-pre/references/normal/scripts/managed_jq.mjs +215 -0
  97. package/skill-pre/references/normal/scripts/onboarding_discovery_runner.mjs +387 -0
  98. package/skill-pre/references/normal/scripts/onboarding_message_runner.mjs +1425 -0
  99. package/skill-pre/references/normal/scripts/onboarding_recovery_runner.mjs +100 -0
  100. package/skill-pre/references/normal/scripts/open_official_url.sh +205 -0
  101. package/skill-pre/references/normal/scripts/platform_compat.mjs +42 -0
  102. package/skill-pre/references/normal/scripts/preview_runtime.mjs +151 -0
  103. package/skill-pre/references/normal/scripts/project_route_inspector.sh +132 -0
  104. package/skill-pre/references/normal/scripts/python_script_runner.mjs +58 -0
  105. package/skill-pre/references/normal/scripts/render_customer_message.mjs +1792 -0
  106. package/skill-pre/references/normal/scripts/runtime.mjs +271 -0
  107. package/skill-pre/references/normal/scripts/self_update.mjs +376 -0
  108. package/skill-pre/references/normal/scripts/telemetry.mjs +2102 -0
  109. package/skill-pre/references/normal/self-update.md +7 -0
  110. package/skill-pre/references/normal/telemetry.md +70 -0
  111. package/skill-pre/references/onboarding/assets/keygen.html +926 -0
  112. package/skill-pre/references/onboarding/flow.md +595 -0
  113. package/skill-pre/references/onboarding/modules/app-release.md +90 -0
  114. package/skill-pre/references/onboarding/modules/authorization.md +43 -0
  115. package/skill-pre/references/onboarding/modules/error-handling.md +62 -0
  116. package/skill-pre/references/onboarding/modules/product-sign.md +49 -0
  117. package/skill-pre/references/onboarding/modules/scripts/app.sh +1485 -0
  118. package/skill-pre/references/onboarding/modules/scripts/ar_sign_apply.sh +331 -0
  119. package/skill-pre/references/onboarding/modules/scripts/auth.sh +1040 -0
  120. package/skill-pre/references/onboarding/modules/scripts/download_key_tool.sh +4 -0
  121. package/skill-pre/references/onboarding/modules/scripts/error_handler.sh +841 -0
  122. package/skill-pre/references/onboarding/modules/scripts/mcc.sh +1085 -0
  123. package/skill-pre/references/onboarding/modules/scripts/network_retry.sh +135 -0
  124. package/skill-pre/references/onboarding/modules/scripts/query_sign_status.sh +105 -0
  125. package/skill-pre/references/onboarding/modules/scripts/service.sh +528 -0
  126. package/skill-pre/references/onboarding/modules/scripts/sign_status_parser.sh +58 -0
  127. package/skill-pre/references/onboarding/modules/scripts/upload_screenshots.sh +208 -0
  128. package/skill-pre/references/onboarding/modules/scripts/upload_special_license.sh +195 -0
  129. package/skill-pre/references/onboarding/modules/service-registration.md +40 -0
  130. package/skill-pre/references/runtime-entry.md +97 -0
  131. package/dist/commands/install-current.d.ts +0 -4
  132. package/dist/commands/install-current.d.ts.map +0 -1
  133. package/dist/commands/install-current.js.map +0 -1
  134. package/dist/commands/install.d.ts +0 -10
  135. package/dist/commands/install.d.ts.map +0 -1
  136. package/dist/commands/install.js.map +0 -1
  137. package/dist/commands/prepare-tools.d.ts +0 -5
  138. package/dist/commands/prepare-tools.d.ts.map +0 -1
  139. package/dist/commands/prepare-tools.js.map +0 -1
  140. package/dist/commands/uninstall.d.ts +0 -10
  141. package/dist/commands/uninstall.d.ts.map +0 -1
  142. package/dist/commands/uninstall.js.map +0 -1
  143. package/dist/commands/update.d.ts +0 -10
  144. package/dist/commands/update.d.ts.map +0 -1
  145. package/dist/commands/update.js.map +0 -1
  146. package/dist/index.d.ts +0 -2
  147. package/dist/index.d.ts.map +0 -1
  148. package/dist/index.js.map +0 -1
  149. package/dist/types.d.ts +0 -42
  150. package/dist/types.d.ts.map +0 -1
  151. package/dist/types.js.map +0 -1
  152. package/dist/utils/alipay-cli.d.ts +0 -20
  153. package/dist/utils/alipay-cli.d.ts.map +0 -1
  154. package/dist/utils/alipay-cli.js.map +0 -1
  155. package/dist/utils/config.d.ts +0 -52
  156. package/dist/utils/config.d.ts.map +0 -1
  157. package/dist/utils/config.js.map +0 -1
  158. package/dist/utils/file.d.ts +0 -77
  159. package/dist/utils/file.d.ts.map +0 -1
  160. package/dist/utils/file.js.map +0 -1
  161. package/dist/utils/jq.d.ts +0 -17
  162. package/dist/utils/jq.d.ts.map +0 -1
  163. package/dist/utils/jq.js.map +0 -1
  164. package/dist/utils/npm.d.ts +0 -17
  165. package/dist/utils/npm.d.ts.map +0 -1
  166. package/dist/utils/npm.js.map +0 -1
  167. package/dist/utils/shell.d.ts +0 -24
  168. package/dist/utils/shell.d.ts.map +0 -1
  169. package/dist/utils/shell.js.map +0 -1
@@ -0,0 +1,2102 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import https from 'node:https';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
7
+ import { execFileSync, spawn, spawnSync } from 'node:child_process';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { normalizeAgentPlatform as normalizePlatform } from './agent_platform.mjs';
10
+ import { conversationCode, normalizeConversationEvent, normalizeConversationMessageInput, sanitizeConversationText, validConversationId } from './conversation_telemetry.mjs';
11
+
12
+ const internalPrefix = 'ALIPAY_AIPAY_INTERNAL:';
13
+ const disableValues = new Set(['0', 'false', 'off', 'no']);
14
+ const canonicalProductTypes = new Set(['AIPAY', 'WEBPAY', 'APPPAY']);
15
+ const allowedRootIntents = new Set(['integration_only', 'onboarding_only', 'full_process']);
16
+ const syncLogBizType = 'yuyanmonitorl';
17
+ const syncLogServerURL = 'https://collect.alipay.com/yuyan/';
18
+ const syncLogMonitorVersion = 'alipay-aipay-skill:sync-log';
19
+ const syncLogCode = '1005';
20
+ const syncLogMsg = 'alipay_aipay_skill_event';
21
+ const syncLogYuyanID = '180020010001290755';
22
+ const uploadTimeoutMs = 3000;
23
+ const permissionedForegroundFlushLimit = 5;
24
+ const localWriteMaxAttempts = 2;
25
+ const runStateLockWaitMs = 5000;
26
+ const runStateLockStaleMs = 30000;
27
+ const outboxFlushLimit = 50;
28
+ const outboxClaimTtlMs = 5 * 60 * 1000;
29
+ const outboxMaxRecords = 1000;
30
+ const outboxMaxAgeMs = 7 * 24 * 60 * 60 * 1000;
31
+ const genericSessionStateEnvKeys = Object.freeze([
32
+ 'HERMES_SESSION_ID',
33
+ 'OPENAI_CONVERSATION_ID',
34
+ 'SESSION_ID',
35
+ 'CI_JOB_ID'
36
+ ]);
37
+ const platformSessionStateEnvKeys = new Map([
38
+ ['codex', Object.freeze(['CODEX_THREAD_ID', 'CODEX_SESSION_ID'])],
39
+ ['claudecode', Object.freeze(['CLAUDE_CODE_SESSION_ID', 'CLAUDE_SESSION_ID'])],
40
+ ['openclaw', Object.freeze(['OPENCLAW_SESSION_ID'])],
41
+ ['cursor', Object.freeze(['CURSOR_SESSION_ID', 'CURSOR_TRACE_ID'])],
42
+ ['coze', Object.freeze(['COZE_SESSION_ID', 'COZE_CONVERSATION_ID'])]
43
+ ]);
44
+ const permissionedFlushNetworkErrors = new Set([
45
+ 'ENOTFOUND',
46
+ 'EAI_AGAIN',
47
+ 'ECONNREFUSED',
48
+ 'ECONNRESET',
49
+ 'ETIMEDOUT',
50
+ 'EHOSTUNREACH',
51
+ 'ENETUNREACH',
52
+ 'NETWORK_UNREACHABLE'
53
+ ]);
54
+ const productAliases = {
55
+ aipay: 'AIPAY',
56
+ webpay: 'WEBPAY',
57
+ apppay: 'APPPAY',
58
+ AIPAY: 'AIPAY',
59
+ WEBPAY: 'WEBPAY',
60
+ APPPAY: 'APPPAY'
61
+ };
62
+
63
+ export const approvedStages = Object.freeze([
64
+ 'clarify_intent',
65
+ 'integration.create_sandbox',
66
+ 'integration.complete_coding',
67
+ 'onboarding.confirm_onboarding_plan',
68
+ 'onboarding.login',
69
+ 'onboarding.confirm_signing',
70
+ 'onboarding.confirm_service',
71
+ 'onboarding.confirm_alipayapp',
72
+ 'integration_summary',
73
+ 'onboarding_summary',
74
+ 'all_summary'
75
+ ]);
76
+
77
+ const countedStageRoutes = Object.freeze({
78
+ integration_only: Object.freeze({
79
+ AIPAY: ['integration.create_sandbox', 'integration.complete_coding'],
80
+ WEBPAY: ['integration.create_sandbox', 'integration.complete_coding'],
81
+ APPPAY: ['integration.create_sandbox', 'integration.complete_coding']
82
+ }),
83
+ onboarding_only: Object.freeze({
84
+ AIPAY: [
85
+ 'onboarding.confirm_onboarding_plan',
86
+ 'onboarding.login',
87
+ 'onboarding.confirm_signing',
88
+ 'onboarding.confirm_service',
89
+ 'onboarding.confirm_alipayapp'
90
+ ],
91
+ WEBPAY: [
92
+ 'onboarding.confirm_onboarding_plan',
93
+ 'onboarding.login',
94
+ 'onboarding.confirm_signing',
95
+ 'onboarding.confirm_alipayapp'
96
+ ],
97
+ APPPAY: [
98
+ 'onboarding.confirm_onboarding_plan',
99
+ 'onboarding.login',
100
+ 'onboarding.confirm_signing',
101
+ 'onboarding.confirm_alipayapp'
102
+ ]
103
+ }),
104
+ full_process: Object.freeze({
105
+ AIPAY: [
106
+ 'integration.create_sandbox',
107
+ 'integration.complete_coding',
108
+ 'onboarding.confirm_onboarding_plan',
109
+ 'onboarding.login',
110
+ 'onboarding.confirm_signing',
111
+ 'onboarding.confirm_service',
112
+ 'onboarding.confirm_alipayapp'
113
+ ],
114
+ WEBPAY: [
115
+ 'integration.create_sandbox',
116
+ 'integration.complete_coding',
117
+ 'onboarding.confirm_onboarding_plan',
118
+ 'onboarding.login',
119
+ 'onboarding.confirm_signing',
120
+ 'onboarding.confirm_alipayapp'
121
+ ],
122
+ APPPAY: [
123
+ 'integration.create_sandbox',
124
+ 'integration.complete_coding',
125
+ 'onboarding.confirm_onboarding_plan',
126
+ 'onboarding.login',
127
+ 'onboarding.confirm_signing',
128
+ 'onboarding.confirm_alipayapp'
129
+ ]
130
+ })
131
+ });
132
+
133
+ const summaryStages = new Set(['integration_summary', 'onboarding_summary', 'all_summary']);
134
+ function flowForStage(stage) {
135
+ if (stage.startsWith('integration.')) return 'integration';
136
+ if (stage.startsWith('onboarding.')) return 'onboarding';
137
+ if (summaryStages.has(stage)) return 'common';
138
+ return 'common';
139
+ }
140
+
141
+ const stageRules = new Map(approvedStages.map((stage) => [stage, {
142
+ stage,
143
+ flow: flowForStage(stage)
144
+ }]));
145
+
146
+ function routeStages(rootIntent, productType) {
147
+ const route = countedStageRoutes[rootIntent]?.[productType];
148
+ return Array.isArray(route) ? route : [];
149
+ }
150
+
151
+ function completedStageList(state) {
152
+ const events = state?.stage_events;
153
+ if (!events || typeof events !== 'object' || Array.isArray(events)) return [];
154
+ return Object.keys(events).filter((stage) => approvedStages.includes(stage));
155
+ }
156
+
157
+ function stageAlreadyRecorded(state, stage) {
158
+ return completedStageList(state).includes(stage);
159
+ }
160
+
161
+ function stageProgressCount(route, completedStages, stage, { includeCurrent = false } = {}) {
162
+ const current = includeCurrent && route.includes(stage) ? stage : '';
163
+ const seen = new Set([...completedStages, current].filter(Boolean));
164
+ return route.reduce((count, routeStage) => count + (seen.has(routeStage) ? 1 : 0), 0);
165
+ }
166
+
167
+ function progressPctForStage(rootIntent, productType, stage, completedStages) {
168
+ const route = routeStages(rootIntent, productType);
169
+ if (route.length === 0) return 0;
170
+ if (summaryStages.has(stage)) {
171
+ const progress = stageProgressCount(route, completedStages, stage, { includeCurrent: false });
172
+ return Math.min(100, Math.ceil((progress / route.length) * 100));
173
+ }
174
+ if (!route.includes(stage)) return -1;
175
+ const progress = stageProgressCount(route, completedStages, stage, { includeCurrent: true });
176
+ return Math.min(100, Math.ceil((progress / route.length) * 100));
177
+ }
178
+
179
+ function messageStageFor(messageId, context = {}, env = process.env) {
180
+ switch (messageId) {
181
+ case 'onboarding.plan.confirm':
182
+ return 'onboarding.confirm_onboarding_plan';
183
+ case 'integration.checklist.result':
184
+ return 'integration_summary';
185
+ case 'process.partial_result':
186
+ return context.rootIntent === 'full_process'
187
+ || context.root_intent === 'full_process'
188
+ || currentRunContext(env)?.root_intent === 'full_process'
189
+ ? 'all_summary'
190
+ : 'onboarding_summary';
191
+ default:
192
+ return '';
193
+ }
194
+ }
195
+
196
+ function shouldCloseRunAfterStage(event, state = {}) {
197
+ if (!event || !stageRules.has(event.stage)) return false;
198
+ const rootIntent = state.root_intent || event.root_intent || '';
199
+ if (event.stage === 'integration_summary') return rootIntent === 'integration_only';
200
+ if (event.stage === 'onboarding_summary') return rootIntent === 'onboarding_only';
201
+ if (event.stage === 'all_summary') return rootIntent === 'full_process';
202
+ return false;
203
+ }
204
+
205
+ const outboxV1RecordFields = new Set(['schema_ver', 'created_at', 'event']);
206
+ const outboxV2RecordFields = new Set(['schema_ver', 'created_at', 'platform', 'event']);
207
+ const metricEventFields = new Set([
208
+ 'schema_ver',
209
+ 'event_id',
210
+ 'skill',
211
+ 'skill_version',
212
+ 'run_id',
213
+ 'stage',
214
+ 'timestamp',
215
+ 'flow',
216
+ 'product_type',
217
+ 'root_intent',
218
+ 'ext_info'
219
+ ]);
220
+ const stageExtInfoFields = new Set([
221
+ 'user_input_after_clarify_intent',
222
+ 'history_count',
223
+ 'root_intent',
224
+ 'progress_pct'
225
+ ]);
226
+
227
+ function safeText(value) {
228
+ return typeof value === 'string' && /^[A-Za-z0-9_.:-]+$/.test(value) ? value : '';
229
+ }
230
+
231
+ function safeDimension(value, fallback = 'unknown', maxLength = 80) {
232
+ if (typeof value !== 'string') return fallback;
233
+ const trimmed = value.trim().replace(/[,\^=\r\n]/g, '_');
234
+ if (!trimmed) return fallback;
235
+ return trimmed.slice(0, maxLength);
236
+ }
237
+
238
+ function failedResult(error, code = 'REQUEST_ERROR', message = 'request error') {
239
+ return {
240
+ ok: false,
241
+ error_code: safeDimension(error?.code || error?.name || code, code, 80),
242
+ error_message: safeDimension(error?.message || message, message, 240)
243
+ };
244
+ }
245
+
246
+ function normalizeSwitch(value) {
247
+ return String(value || '').trim().toLowerCase();
248
+ }
249
+
250
+ export function telemetryEnabled(env = process.env) {
251
+ return !disableValues.has(normalizeSwitch(env.ALIPAY_AIPAY_TELEMETRY));
252
+ }
253
+
254
+ function telemetryCaptureOnly(env = process.env) {
255
+ return normalizeSwitch(env.ALIPAY_AIPAY_TELEMETRY_CAPTURE_ONLY) === '1';
256
+ }
257
+
258
+ export function telemetryUploadEnabled(env = process.env) {
259
+ return telemetryEnabled(env)
260
+ && !telemetryCaptureOnly(env)
261
+ && !disableValues.has(normalizeSwitch(env.ALIPAY_AIPAY_TELEMETRY_UPLOAD))
262
+ && !isMaintenanceProcess(env);
263
+ }
264
+
265
+ function scriptPath() {
266
+ return fileURLToPath(import.meta.url);
267
+ }
268
+
269
+ function scriptDir() {
270
+ return path.dirname(scriptPath());
271
+ }
272
+
273
+ function skillRoot() {
274
+ return path.resolve(scriptDir(), '../../..');
275
+ }
276
+
277
+ function telemetryRoot() {
278
+ return path.join(os.tmpdir(), 'alipay-aipay-telemetry', 'pre');
279
+ }
280
+
281
+ function isMaintenanceProcess(env = process.env, argv = process.argv) {
282
+ if (normalizeSwitch(env.ALIPAY_AIPAY_TELEMETRY_ALLOW_MAINTENANCE) === '1') return false;
283
+ if (env.NODE_TEST_CONTEXT) return true;
284
+ if (env.npm_lifecycle_event === 'test') return true;
285
+ return argv.some((arg) => /(^|[/\\])test[/\\].+\.test\.mjs$/u.test(String(arg || '')));
286
+ }
287
+
288
+ function stableSessionSource(env = process.env) {
289
+ const explicitSession = safeRunId(env.ALIPAY_AIPAY_SESSION_ID);
290
+ if (explicitSession) return explicitSession;
291
+ const agentPlatform = platformFromEnv(env);
292
+ const platformKeys = platformSessionStateEnvKeys.get(agentPlatform) || [];
293
+ const sessionKeys = [...platformKeys, ...genericSessionStateEnvKeys];
294
+ return sessionKeys.map((key) => safeRunId(env[key])).find(Boolean) || '';
295
+ }
296
+
297
+ function stableStateSource(env = process.env) {
298
+ return stableSessionSource(env) || platformProjectId(env) || '';
299
+ }
300
+
301
+ // Resolve once per conversation; callers carry the result across process boundaries.
302
+ export function resolveTelemetrySession(env = process.env) {
303
+ return stableSessionSource(env) || `session-${crypto.randomUUID()}`;
304
+ }
305
+
306
+ export function applyTelemetrySession(args, env = process.env) {
307
+ const remaining = [];
308
+ let session;
309
+ for (let index = 0; index < args.length; index += 1) {
310
+ if (args[index] !== '--session-id') {
311
+ remaining.push(args[index]);
312
+ continue;
313
+ }
314
+ if (session !== undefined) throw new Error('duplicate --session-id');
315
+ const value = args[++index];
316
+ if (!value || value.startsWith('--') || !safeRunId(value)) throw new Error('invalid --session-id');
317
+ session = value;
318
+ }
319
+ if (session !== undefined) env.ALIPAY_AIPAY_SESSION_ID = session;
320
+ return remaining;
321
+ }
322
+
323
+ function stateKeyFromSource(source) {
324
+ if (!source) return 'default';
325
+ if (safeText(source) && source.length <= 128 && source !== '.' && source !== '..') return source;
326
+ return `key-${crypto.createHash('sha256').update(source).digest('hex')}`;
327
+ }
328
+
329
+ function stableStateKey(env = process.env) {
330
+ return stateKeyFromSource(stableStateSource(env));
331
+ }
332
+
333
+ function stateDir(env = process.env) {
334
+ if (env.ALIPAY_AIPAY_TELEMETRY_DIR) return env.ALIPAY_AIPAY_TELEMETRY_DIR;
335
+ return telemetryRoot();
336
+ }
337
+
338
+ function runStatePath(env = process.env) {
339
+ if (env.ALIPAY_AIPAY_RUN_STATE_FILE) return env.ALIPAY_AIPAY_RUN_STATE_FILE;
340
+ return path.join(stateDir(env), `${stableStateKey(env)}.json`);
341
+ }
342
+
343
+ function runStateLockPath(env = process.env) {
344
+ return `${runStatePath(env)}.lock`;
345
+ }
346
+
347
+ function outboxDir(env = process.env) {
348
+ if (env.ALIPAY_AIPAY_TELEMETRY_OUTBOX_DIR) return env.ALIPAY_AIPAY_TELEMETRY_OUTBOX_DIR;
349
+ return path.join(stateDir(env), 'outbox');
350
+ }
351
+
352
+ function uploadStatusPath(env = process.env) {
353
+ if (env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_STATUS_FILE) return env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_STATUS_FILE;
354
+ return path.join(stateDir(env), 'upload-status.json');
355
+ }
356
+
357
+ export function telemetryStoragePaths(env = process.env) {
358
+ return {
359
+ state_dir: stateDir(env),
360
+ run_state_file: runStatePath(env),
361
+ outbox_dir: outboxDir(env),
362
+ upload_status_file: uploadStatusPath(env)
363
+ };
364
+ }
365
+
366
+ function ensurePrivateDir(dir) {
367
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
368
+ try {
369
+ fs.chmodSync(dir, 0o700);
370
+ } catch {}
371
+ }
372
+
373
+ function writePrivateJsonOnce(file, value) {
374
+ let tempPath = '';
375
+ try {
376
+ ensurePrivateDir(path.dirname(file));
377
+ tempPath = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.tmp`);
378
+ fs.writeFileSync(tempPath, `${JSON.stringify(value)}\n`, { mode: 0o600 });
379
+ try {
380
+ fs.chmodSync(tempPath, 0o600);
381
+ } catch {}
382
+ fs.renameSync(tempPath, file);
383
+ return true;
384
+ } catch {
385
+ try {
386
+ if (tempPath && fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
387
+ } catch {}
388
+ return false;
389
+ }
390
+ }
391
+
392
+ function writePrivateJson(file, value) {
393
+ for (let attempt = 0; attempt < localWriteMaxAttempts; attempt += 1) {
394
+ if (writePrivateJsonOnce(file, value)) return true;
395
+ }
396
+ return false;
397
+ }
398
+
399
+ function acquireRunStateLock(env = process.env, lockPath = runStateLockPath(env)) {
400
+ const startedAt = Date.now();
401
+ try {
402
+ ensurePrivateDir(path.dirname(lockPath));
403
+ while (Date.now() - startedAt < runStateLockWaitMs) {
404
+ try {
405
+ const descriptor = fs.openSync(lockPath, 'wx', 0o600);
406
+ fs.closeSync(descriptor);
407
+ return lockPath;
408
+ } catch (error) {
409
+ if (error?.code !== 'EEXIST') return '';
410
+ try {
411
+ const lockStat = fs.statSync(lockPath);
412
+ if (Date.now() - lockStat.mtimeMs >= runStateLockStaleMs) fs.unlinkSync(lockPath);
413
+ } catch {}
414
+ sleepSync(10);
415
+ }
416
+ }
417
+ } catch {}
418
+ return '';
419
+ }
420
+
421
+ function withRunStateLock(env = process.env, operation, requestedLockPath = runStateLockPath(env)) {
422
+ const lockPath = acquireRunStateLock(env, requestedLockPath);
423
+ if (!lockPath) return { acquired: false, value: null };
424
+ try {
425
+ return { acquired: true, value: operation() };
426
+ } finally {
427
+ try {
428
+ fs.unlinkSync(lockPath);
429
+ } catch {}
430
+ }
431
+ }
432
+
433
+ function isValidRunState(parsed) {
434
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
435
+ if (parsed.schema_ver !== 2
436
+ || !safeRunId(parsed.run_id)
437
+ || !allowedRootIntents.has(parsed.root_intent)
438
+ || !canonicalProductTypes.has(parsed.product_type)
439
+ || !['ACTIVE', 'CLOSED'].includes(parsed.lifecycle)) return false;
440
+ const createdAt = Number(parsed.created_at);
441
+ const updatedAt = Number(parsed.updated_at);
442
+ if (!Number.isSafeInteger(createdAt) || createdAt <= 0
443
+ || !Number.isSafeInteger(updatedAt) || updatedAt <= 0) return false;
444
+
445
+ // Telemetry-off runs intentionally retain only the context needed by business CLI attribution.
446
+ if (parsed.telemetry_disabled === true) {
447
+ return !Object.prototype.hasOwnProperty.call(parsed, 'counter_count')
448
+ && !Object.prototype.hasOwnProperty.call(parsed, 'input_hashes')
449
+ && !Object.prototype.hasOwnProperty.call(parsed, 'stage_events')
450
+ && parsed.terminal_at === null;
451
+ }
452
+
453
+ if (parsed.clarify_intent_recorded !== true
454
+ || !Number.isSafeInteger(parsed.counter_count)
455
+ || parsed.counter_count < 0
456
+ || !Array.isArray(parsed.input_hashes)
457
+ || new Set(parsed.input_hashes).size !== parsed.input_hashes.length
458
+ || parsed.input_hashes.some((hash) => typeof hash !== 'string' || !/^[a-f0-9]{32}$/u.test(hash))
459
+ || !parsed.stage_events || typeof parsed.stage_events !== 'object' || Array.isArray(parsed.stage_events)) return false;
460
+
461
+ const stageNames = Object.keys(parsed.stage_events);
462
+ if (!stageNames.includes('clarify_intent') || stageNames.some((stage) => !approvedStages.includes(stage))) return false;
463
+ for (const record of Object.values(parsed.stage_events)) {
464
+ if (!record || typeof record !== 'object' || Array.isArray(record)
465
+ || !safeText(record.event_id) || record.event_id.length > 128
466
+ || !Number.isSafeInteger(record.generated_at) || record.generated_at <= 0) return false;
467
+ }
468
+ if (parsed.lifecycle === 'ACTIVE' && parsed.terminal_at !== null) return false;
469
+ if (parsed.lifecycle === 'CLOSED'
470
+ && (!Number.isSafeInteger(parsed.terminal_at) || parsed.terminal_at <= 0)) return false;
471
+ return true;
472
+ }
473
+
474
+ function readRunState(env = process.env) {
475
+ try {
476
+ const statePath = runStatePath(env);
477
+ const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
478
+ return isValidRunState(parsed) ? parsed : null;
479
+ } catch {
480
+ return null;
481
+ }
482
+ }
483
+
484
+ export function currentRunContext(env = process.env) {
485
+ const state = readRunState(env);
486
+ if (!state) return null;
487
+ return Object.freeze({
488
+ run_id: state.run_id,
489
+ root_intent: state.root_intent,
490
+ product_type: state.product_type,
491
+ terminal: state.lifecycle === 'CLOSED'
492
+ });
493
+ }
494
+
495
+ function canReadImplicitRunState(env = process.env) {
496
+ return !isMaintenanceProcess(env);
497
+ }
498
+
499
+ function recordLocalTelemetryFailure(errorCode, message, env = process.env) {
500
+ if (!telemetryEnabled(env)) return;
501
+ const safeCode = safeDimension(errorCode, 'LOCAL_TELEMETRY_FAILED', 80);
502
+ const safeMessage = safeDimension(message || safeCode, safeCode, 240);
503
+ writeUploadStatus({
504
+ status: 'local_failed',
505
+ error_code: safeCode,
506
+ error_message: safeMessage
507
+ }, env);
508
+ emitTelemetryError(new Error(safeCode));
509
+ }
510
+
511
+ function writeRunState(state, env = process.env) {
512
+ return writePrivateJson(runStatePath(env), state);
513
+ }
514
+
515
+ function persistRunState(state, env = process.env) {
516
+ const persisted = writeRunState(state, env);
517
+ if (!persisted && telemetryEnabled(env)) {
518
+ recordLocalTelemetryFailure('LOCAL_RUN_STATE_WRITE_FAILED', 'failed to write local telemetry run state', env);
519
+ }
520
+ return persisted;
521
+ }
522
+
523
+ function outboxFilename(event) {
524
+ const timestamp = Number.isFinite(Number(event?.timestamp)) ? String(Number(event.timestamp)) : String(Date.now());
525
+ const eventID = isConversationEvent(event)
526
+ ? `${event.conversation_id}-${event.sequence}`
527
+ : safeText(event?.event_id) || crypto.randomUUID();
528
+ const suffix = isConversationEvent(event) ? '.conversation' : '.json';
529
+ return `${timestamp}-${eventID}${suffix}`;
530
+ }
531
+
532
+ function writeOutboxRecord(event, env = process.env) {
533
+ if (!telemetryEnabled(env)) return '';
534
+ try {
535
+ const normalizedEvent = normalizeTelemetryEvent(event);
536
+ if (!normalizedEvent) return '';
537
+ const dir = outboxDir(env);
538
+ ensurePrivateDir(dir);
539
+ const filename = outboxFilename(normalizedEvent);
540
+ const finalPath = path.join(dir, filename);
541
+ const record = { schema_ver: 2, created_at: Date.now(), platform: platformFromEnv(env), event: normalizedEvent };
542
+ if (!writePrivateJson(finalPath, record)) return '';
543
+ pruneOutbox(env);
544
+ return finalPath;
545
+ } catch {
546
+ return '';
547
+ }
548
+ }
549
+
550
+ export function enqueueMetricEvent(event, env = process.env) {
551
+ if (!telemetryEnabled(env)) return false;
552
+ return Boolean(writeOutboxRecord(event, env));
553
+ }
554
+
555
+ function safeRunId(value) {
556
+ if (typeof value !== 'string' || !value.trim()) return '';
557
+ return /[\u0000-\u001f\u007f,^=]/u.test(value) ? '' : value;
558
+ }
559
+
560
+ function platformProjectId(env = process.env, agentPlatform = platformFromEnv(env)) {
561
+ if (agentPlatform === 'coze') return safeRunId(env.COZE_PROJECT_ID) || '';
562
+ if (agentPlatform === 'meoo') return safeRunId(env.MEOO_PROJECT_ID) || '';
563
+ if (agentPlatform === 'miaoda' && env.MIAODA) return safeRunId(env.VITE_APP_ID) || '';
564
+ return '';
565
+ }
566
+
567
+ function canonicalProductType(value) {
568
+ const canonical = typeof value === 'string' ? productAliases[value] : '';
569
+ return canonicalProductTypes.has(canonical) ? canonical : '';
570
+ }
571
+
572
+ function parseNonNegativeInteger(value) {
573
+ if (typeof value === 'number') return Number.isSafeInteger(value) && value >= 0 ? value : null;
574
+ if (typeof value !== 'string' || !/^(?:0|[1-9]\d*)$/.test(value)) return null;
575
+ const parsed = Number(value);
576
+ return Number.isSafeInteger(parsed) ? parsed : null;
577
+ }
578
+
579
+ function sleepSync(ms) {
580
+ if (!Number.isFinite(ms) || ms <= 0) return;
581
+ try {
582
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(ms, 30000));
583
+ } catch {}
584
+ }
585
+
586
+ function normalizeUserInputId(value) {
587
+ if (typeof value !== 'string') return '';
588
+ const text = value.trim();
589
+ if (!text || text.length > 256 || /[\u0000-\u001f\u007f]/u.test(text)) return '';
590
+ return text;
591
+ }
592
+
593
+ function userInputHash(inputId, runId) {
594
+ const normalized = normalizeUserInputId(inputId);
595
+ if (!normalized || !safeRunId(runId)) return '';
596
+ return crypto.createHash('sha256').update(`${runId}\u0000${normalized}`).digest('hex').slice(0, 32);
597
+ }
598
+
599
+ function normalizeUserInputHashes(value) {
600
+ if (!Array.isArray(value)) return [];
601
+ return [...new Set(value.filter((entry) => typeof entry === 'string' && /^[a-f0-9]{32}$/u.test(entry)))];
602
+ }
603
+
604
+ function parseHistoryCount(value, { strict = false } = {}) {
605
+ if (value === null || value === 'null' || value === undefined) return null;
606
+ const parsed = parseNonNegativeInteger(value);
607
+ if (parsed !== null) return parsed;
608
+ if (strict) throw new Error('--history-count 只支持非负整数或 null');
609
+ return undefined;
610
+ }
611
+
612
+ function stageExtInfoFromPayload(payload = {}) {
613
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return '';
614
+ const userInputAfterClarifyIntent = parseNonNegativeInteger(
615
+ payload.userInputAfterClarifyIntent ?? payload.user_input_after_clarify_intent
616
+ );
617
+ if (userInputAfterClarifyIntent === null) return '';
618
+ const historyCount = parseHistoryCount(payload.historyCount ?? payload.history_count);
619
+ if (historyCount === undefined) return '';
620
+ const rootIntent = payload.rootIntent ?? payload.root_intent;
621
+ if (!allowedRootIntents.has(rootIntent)) return '';
622
+ const progressPct = Number(payload.progressPct ?? payload.progress_pct);
623
+ if (!Number.isSafeInteger(progressPct) || progressPct < 0 || progressPct > 100) return '';
624
+ return JSON.stringify({
625
+ user_input_after_clarify_intent: userInputAfterClarifyIntent,
626
+ history_count: historyCount,
627
+ root_intent: rootIntent,
628
+ progress_pct: progressPct
629
+ });
630
+ }
631
+
632
+ function legalStageForState(state, stage) {
633
+ if (!state || !stageRules.has(stage)) return false;
634
+ if (stage === 'clarify_intent') return true;
635
+ if (stage === 'integration_summary') return ['integration_only', 'full_process'].includes(state.root_intent);
636
+ if (stage === 'onboarding_summary') return state.root_intent === 'onboarding_only';
637
+ if (stage === 'all_summary') return state.root_intent === 'full_process';
638
+ return routeStages(state.root_intent, state.product_type).includes(stage);
639
+ }
640
+
641
+ function eventFromState(stage, state, historyCount, nowMs = Date.now(), eventId = crypto.randomUUID()) {
642
+ if (!legalStageForState(state, stage)) return null;
643
+ const completedStages = completedStageList(state);
644
+ const progressPct = stage === 'clarify_intent'
645
+ ? 0
646
+ : progressPctForStage(state.root_intent, state.product_type, stage, completedStages);
647
+ if (progressPct < 0) return null;
648
+ const extInfo = stageExtInfoFromPayload({
649
+ userInputAfterClarifyIntent: state.counter_count,
650
+ historyCount,
651
+ rootIntent: state.root_intent,
652
+ progressPct
653
+ });
654
+ if (!extInfo) return null;
655
+ return {
656
+ schema_ver: 2,
657
+ event_id: eventId,
658
+ skill: 'alipay-aipay',
659
+ skill_version: skillVersionFromDisk(),
660
+ run_id: state.run_id,
661
+ stage,
662
+ timestamp: nowMs,
663
+ flow: flowForStage(stage),
664
+ product_type: state.product_type,
665
+ root_intent: state.root_intent,
666
+ ext_info: extInfo
667
+ };
668
+ }
669
+
670
+ function writeMetricEventMarker(event, stream) {
671
+ if (stream && typeof stream.write === 'function') {
672
+ stream.write(`${internalPrefix}METRIC_EVENT=${JSON.stringify(event)}\n`);
673
+ }
674
+ }
675
+
676
+ export function emitStageMetric(stage, context = {}, env = process.env) {
677
+ try {
678
+ if (!telemetryEnabled(env) || !stageRules.has(stage)) return false;
679
+ const activeRun = currentRunContext(env);
680
+ const runId = safeRunId(context.runId || context.run_id || env.ALIPAY_AIPAY_TELEMETRY_RUN_ID || activeRun?.run_id);
681
+ const historyCount = context.historyCount ?? context.history_count ?? env.ALIPAY_AIPAY_TELEMETRY_HISTORY_COUNT ?? 'null';
682
+ const child = spawnSync(process.execPath, [
683
+ scriptPath(), 'stage', stage,
684
+ '--run-id', runId,
685
+ '--history-count', String(historyCount)
686
+ ], {
687
+ stdio: normalizeSwitch(env.ALIPAY_AIPAY_TEST_MODE) === '1' ? 'inherit' : 'ignore',
688
+ env,
689
+ timeout: uploadTimeoutMs + 2000
690
+ });
691
+ return !child.error && child.status === 0;
692
+ } catch {
693
+ return false;
694
+ }
695
+ }
696
+
697
+ export function emitMessageMetric(messageId, variantName, variables = {}, context = {}, env = process.env) {
698
+ try {
699
+ const stage = messageStageFor(messageId, context, env);
700
+ return stage ? emitStageMetric(stage, context, env) : false;
701
+ } catch {
702
+ return false;
703
+ }
704
+ }
705
+
706
+ function parseCliArgs(argv) {
707
+ const command = argv[2];
708
+ const args = { _: [] };
709
+ for (let index = 3; index < argv.length; index += 1) {
710
+ const token = argv[index];
711
+ if (!token.startsWith('--')) {
712
+ args._.push(token);
713
+ continue;
714
+ }
715
+ const name = token.slice(2);
716
+ const value = argv[index + 1];
717
+ if (value === undefined || value.startsWith('--')) throw new Error(`参数 --${name} 缺少取值`);
718
+ args[name] = value;
719
+ index += 1;
720
+ }
721
+ return { command, args };
722
+ }
723
+
724
+ function contextFromCliArgs(args) {
725
+ const context = {
726
+ runId: args['run-id'],
727
+ historyCount: args['history-count']
728
+ };
729
+ return context;
730
+ }
731
+
732
+ function assertCliArgs(args, allowedOptions, positionalCount) {
733
+ for (const name of Object.keys(args)) {
734
+ if (name !== '_' && !allowedOptions.has(name)) throw new Error(`不支持参数 --${name}`);
735
+ }
736
+ if (args._.length !== positionalCount) throw new Error('invalid telemetry arguments');
737
+ }
738
+
739
+ function createRunState(args, env = process.env, nowMs = Date.now()) {
740
+ const rootIntent = args['root-intent'];
741
+ if (!allowedRootIntents.has(rootIntent)) throw new Error('--root-intent 仅支持 integration_only、onboarding_only、full_process');
742
+ const productType = canonicalProductType(args['product-type']);
743
+ if (!productType) return null;
744
+ const runId = crypto.randomUUID();
745
+ return {
746
+ schema_ver: 2,
747
+ run_id: runId,
748
+ root_intent: rootIntent,
749
+ product_type: productType,
750
+ lifecycle: 'ACTIVE',
751
+ clarify_intent_recorded: false,
752
+ counter_count: 0,
753
+ input_hashes: [],
754
+ stage_events: {},
755
+ created_at: nowMs,
756
+ updated_at: nowMs,
757
+ terminal_at: null
758
+ };
759
+ }
760
+
761
+ function reusableRunState(existing, requestedRootIntent, requestedProductType) {
762
+ if (!existing || existing.lifecycle !== 'ACTIVE') return false;
763
+ if (existing.root_intent !== requestedRootIntent) return false;
764
+ const existingProductType = canonicalProductType(existing.product_type);
765
+ const nextProductType = canonicalProductType(requestedProductType);
766
+ if (!existingProductType || !nextProductType) return false;
767
+ return existingProductType === nextProductType;
768
+ }
769
+
770
+ function createOrReuseRunState(args, env = process.env, nowMs = Date.now()) {
771
+ const rootIntent = args['root-intent'];
772
+ if (!allowedRootIntents.has(rootIntent)) throw new Error('--root-intent 仅支持 integration_only、onboarding_only、full_process');
773
+ const requestedProductType = canonicalProductType(args['product-type']);
774
+ if (!requestedProductType) return null;
775
+ const existing = readRunState(env);
776
+ if (reusableRunState(existing, rootIntent, requestedProductType)
777
+ && (telemetryEnabled(env) ? existing.clarify_intent_recorded === true : true)) {
778
+ const state = {
779
+ ...existing,
780
+ updated_at: nowMs
781
+ };
782
+ return {
783
+ reused: true,
784
+ state
785
+ };
786
+ }
787
+ const state = createRunState(args, env, nowMs);
788
+ return state ? { reused: false, state } : null;
789
+ }
790
+
791
+ export function recordTelemetryUserInput(inputId, env = process.env, nowMs = Date.now(), runId = '') {
792
+ if (!telemetryEnabled(env)) return null;
793
+ const transaction = withRunStateLock(env, () => {
794
+ const existing = readRunState(env);
795
+ const explicitRunId = safeRunId(runId);
796
+ if (!existing?.run_id || existing.lifecycle !== 'ACTIVE' || !explicitRunId || explicitRunId !== existing.run_id) return null;
797
+ const normalizedInputId = normalizeUserInputId(inputId);
798
+ if (!normalizedInputId) throw new Error('用户输入计数必须提供不含用户原文或敏感信息的本地幂等 --input-id');
799
+ const count = parseNonNegativeInteger(existing.counter_count);
800
+ if (count === null) return null;
801
+ const hashes = normalizeUserInputHashes(existing.input_hashes);
802
+ const inputHash = userInputHash(normalizedInputId, existing.run_id);
803
+ if (!inputHash) throw new Error('用户输入计数 input-id 无法生成本地幂等键');
804
+ if (hashes.includes(inputHash)) return { user_input_after_clarify_intent: count };
805
+ const next = {
806
+ ...existing,
807
+ counter_count: count + 1,
808
+ input_hashes: [...hashes, inputHash],
809
+ updated_at: nowMs
810
+ };
811
+ return persistRunState(next, env)
812
+ ? { user_input_after_clarify_intent: next.counter_count }
813
+ : null;
814
+ });
815
+ if (!transaction.acquired) {
816
+ recordLocalTelemetryFailure('LOCAL_RUN_STATE_WRITE_FAILED', 'failed to lock local telemetry run state', env);
817
+ return null;
818
+ }
819
+ return transaction.value;
820
+ }
821
+
822
+ function conversationSessionSource(env, explicitSessionId = '') {
823
+ // Never use a project ID or the shared default key for conversation identity.
824
+ return stableSessionSource(env) || safeRunId(explicitSessionId) || '';
825
+ }
826
+
827
+ function conversationStatePath(conversationId, env) {
828
+ return validConversationId(conversationId)
829
+ ? path.join(stateDir(env), 'conversations', `${conversationId}.json`) : '';
830
+ }
831
+
832
+ function sessionHash(source) {
833
+ return crypto.createHash('sha256').update(source).digest('hex');
834
+ }
835
+
836
+ function conversationSessionFile(ownerHash, suffix, env) {
837
+ return path.join(stateDir(env), 'conversations', `${ownerHash}.${suffix}`);
838
+ }
839
+
840
+ function readConversationConsent(ownerHash, env) {
841
+ try {
842
+ const consent = JSON.parse(fs.readFileSync(conversationSessionFile(ownerHash, 'consent', env), 'utf8'));
843
+ if (consent.schema_ver !== 1 || consent.session_hash !== ownerHash
844
+ || !['GRANTED', 'DENIED'].includes(consent.decision)
845
+ || (consent.decision === 'GRANTED' && !validConversationId(consent.grant_id))
846
+ || !Array.isArray(consent.input_hashes)
847
+ || consent.input_hashes.some(hash => !/^[a-f0-9]{32}$/.test(hash))) return null;
848
+ return consent;
849
+ } catch { return null; }
850
+ }
851
+
852
+ // Consent is independent of business runs and contains no reply text. Replaying
853
+ // an earlier confirmation must never undo a later opt-out.
854
+ export function setConversationConsent({ sessionId = '', decision, inputId, conversationId } = {}, env = process.env) {
855
+ if (!['GRANTED', 'DENIED'].includes(decision)) return null;
856
+ // An explicit refusal is a privacy preference, even while collection is off.
857
+ // Otherwise toggling the switch later could resurrect an older grant.
858
+ if (decision !== 'DENIED' && (!telemetryEnabled(env) || !conversationCode(env))) return { decision: 'DISABLED' };
859
+ const input = normalizeUserInputId(inputId);
860
+ if (!input) return null;
861
+ try {
862
+ const source = conversationSessionSource(env, sessionId);
863
+ const existing = conversationId ? readConversationState(conversationId, env) : null;
864
+ const ownerHash = source ? sessionHash(source) : existing?.session_hash;
865
+ if (!ownerHash || (conversationId && (!existing || existing.session_hash !== ownerHash))) return null;
866
+ const transaction = withRunStateLock(env, () => {
867
+ const previous = readConversationConsent(ownerHash, env);
868
+ const hash = userInputHash(input, ownerHash);
869
+ if (previous?.input_hashes.includes(hash)) return previous;
870
+ const next = { schema_ver: 1, session_hash: ownerHash, decision,
871
+ grant_id: decision === 'GRANTED'
872
+ ? (previous?.decision === 'GRANTED' ? previous.grant_id : crypto.randomUUID()) : null,
873
+ input_hashes: [...(previous?.input_hashes || []), hash] };
874
+ if (!writePrivateJson(conversationSessionFile(ownerHash, 'consent', env), next)) return null;
875
+ if (decision === 'DENIED') {
876
+ // The consent gate already prevents writes even if closing state fails.
877
+ let indexedId;
878
+ try { indexedId = JSON.parse(fs.readFileSync(conversationSessionFile(ownerHash, 'index', env), 'utf8')).conversation_id; } catch {}
879
+ for (const id of new Set([conversationId, indexedId])) {
880
+ const state = readConversationState(id, env);
881
+ if (state?.session_hash === ownerHash && state.lifecycle === 'ACTIVE') {
882
+ writePrivateJson(conversationStatePath(id, env), { ...state, lifecycle: 'CLOSED' });
883
+ }
884
+ }
885
+ }
886
+ return next;
887
+ }, conversationSessionFile(ownerHash, 'index.lock', env));
888
+ return transaction.acquired ? transaction.value : null;
889
+ } catch { return null; }
890
+ }
891
+
892
+ function hasConversationConsent(state, env) {
893
+ const consent = state && readConversationConsent(state.session_hash, env);
894
+ return consent?.decision === 'GRANTED' && state.consent_id === consent.grant_id;
895
+ }
896
+
897
+ function readConversationState(conversationId, env, { checkOwner = true } = {}) {
898
+ try {
899
+ const state = JSON.parse(fs.readFileSync(conversationStatePath(conversationId, env), 'utf8'));
900
+ const source = checkOwner ? conversationSessionSource(env) : '';
901
+ if (state.schema_ver !== 3 || state.conversation_id !== conversationId
902
+ || !['ACTIVE', 'CLOSED'].includes(state.lifecycle)
903
+ || !/^[a-f0-9]{64}$/.test(state.session_hash)
904
+ || (source && state.session_hash !== sessionHash(source))
905
+ || !Number.isSafeInteger(state.sequence) || state.sequence < 0
906
+ || !Array.isArray(state.message_hashes) || state.message_hashes.length !== state.sequence
907
+ || state.message_hashes.some((hash) => !/^[a-f0-9]{32}$/.test(hash))
908
+ || new Set(state.message_hashes).size !== state.sequence
909
+ || !Number.isSafeInteger(state.created_at) || state.created_at <= 0) return null;
910
+ return state;
911
+ } catch { return null; }
912
+ }
913
+
914
+ export function startConversation({ sessionId = '' } = {}, env = process.env, nowMs = Date.now()) {
915
+ if (!telemetryEnabled(env) || !conversationCode(env)) return null;
916
+ try {
917
+ const source = conversationSessionSource(env, sessionId);
918
+ if (!source) return null;
919
+ const ownerHash = sessionHash(source);
920
+ const indexPath = path.join(stateDir(env), 'conversations', `${ownerHash}.index`);
921
+ const transaction = withRunStateLock(env, () => {
922
+ const consent = readConversationConsent(ownerHash, env);
923
+ if (consent?.decision !== 'GRANTED') return null;
924
+ let existingId;
925
+ try { existingId = JSON.parse(fs.readFileSync(indexPath, 'utf8')).conversation_id; } catch {}
926
+ const existing = readConversationState(existingId, env);
927
+ if (existing?.lifecycle === 'ACTIVE' && existing.session_hash === ownerHash
928
+ && existing.consent_id === consent.grant_id) {
929
+ return { ...existing, created: false };
930
+ }
931
+ const conversationId = crypto.randomUUID();
932
+ const state = { schema_ver: 3, lifecycle: 'ACTIVE', conversation_id: conversationId, session_hash: ownerHash,
933
+ consent_id: consent.grant_id,
934
+ created_at: nowMs, sequence: 0, message_hashes: [] };
935
+ if (!writePrivateJson(conversationStatePath(conversationId, env), state)
936
+ || !writePrivateJson(indexPath, { conversation_id: conversationId })) return null;
937
+ return { ...state, created: true };
938
+ }, `${indexPath}.lock`);
939
+ return transaction.acquired ? transaction.value : null;
940
+ } catch { return null; }
941
+ }
942
+
943
+ function conversationEventBase(state, productType, env, nowMs) {
944
+ return { schema_ver: 5, kind: 'conversation_message',
945
+ skill: 'alipay-aipay', skill_version: skillVersionFromDisk(), conversation_id: state.conversation_id,
946
+ product_type: productType, timestamp: nowMs, code: conversationCode(env) };
947
+ }
948
+
949
+ export function endConversation(conversationId, env = process.env) {
950
+ if (!telemetryEnabled(env) || !conversationCode(env) || !validConversationId(conversationId)) return null;
951
+ try {
952
+ const file = conversationStatePath(conversationId, env);
953
+ const existing = readConversationState(conversationId, env);
954
+ if (!existing) return null;
955
+ const transaction = withRunStateLock(env, () => {
956
+ const state = readConversationState(conversationId, env);
957
+ if (!state || state.lifecycle === 'CLOSED') return state;
958
+ const closed = { ...state, lifecycle: 'CLOSED' };
959
+ return writePrivateJson(file, closed) ? closed : null;
960
+ }, conversationSessionFile(existing.session_hash, 'index.lock', env));
961
+ return transaction.acquired ? transaction.value : null;
962
+ } catch { return null; }
963
+ }
964
+
965
+ export async function recordConversationMessage(message, context = {}, env = process.env, nowMs = Date.now()) {
966
+ if (!telemetryEnabled(env) || !conversationCode(env)) return null;
967
+ try {
968
+ message = normalizeConversationMessageInput(message);
969
+ if (!message) return null;
970
+ const allowed = new Set(['role', 'content', 'has_attachments']);
971
+ if (!hasOnlyFields(message, allowed) || !['user', 'assistant'].includes(message.role)
972
+ || (message.has_attachments !== undefined && typeof message.has_attachments !== 'boolean')) return null;
973
+ const sanitized = sanitizeConversationText(message.content);
974
+ const messageId = normalizeUserInputId(context.messageId);
975
+ // Assistant content is captured only when the caller explicitly marks the final reply.
976
+ if (message.role === 'assistant' && context.final !== true) return null;
977
+ const conversationId = context.conversationId;
978
+ const productType = context.productType == null ? null : canonicalProductType(context.productType);
979
+ if (!hasOnlyFields(context, new Set(['conversationId', 'messageId', 'productType', 'final']))
980
+ || !sanitized || !messageId || !validConversationId(conversationId)
981
+ || (context.productType != null && !productType)) return null;
982
+ const file = conversationStatePath(conversationId, env);
983
+ const existing = readConversationState(conversationId, env);
984
+ if (!existing) return null;
985
+ const transaction = withRunStateLock(env, () => {
986
+ const state = readConversationState(conversationId, env);
987
+ if (!state || state.lifecycle !== 'ACTIVE' || !hasConversationConsent(state, env)) return null;
988
+ const hash = userInputHash(`${message.role}:${messageId}`, conversationId);
989
+ const existingIndex = state.message_hashes.indexOf(hash);
990
+ // A retry of an already captured message keeps its original sequence and is
991
+ // reported as a duplicate success, never as a capture failure.
992
+ if (existingIndex !== -1) return { duplicate: true, sequence: existingIndex + 1 };
993
+ const sequence = state.sequence + 1;
994
+ const event = normalizeConversationEvent({ ...conversationEventBase(state, productType, env, nowMs),
995
+ sequence, role: message.role, content: sanitized.content, truncated: sanitized.truncated,
996
+ has_attachments: message.has_attachments ?? false, redaction_version: 1 });
997
+ const next = { ...state, sequence, message_hashes: [...state.message_hashes, hash] };
998
+ return event && writePrivateJson(file, next) ? event : null;
999
+ }, conversationSessionFile(existing.session_hash, 'index.lock', env));
1000
+ if (!transaction.acquired || !transaction.value) return null;
1001
+ if (transaction.value.duplicate) return transaction.value;
1002
+ // Never echo dialogue content in stderr markers, including in capture-only mode.
1003
+ await deliverPersistedStageEvent(transaction.value, env, null);
1004
+ return transaction.value;
1005
+ } catch { return null; }
1006
+ }
1007
+
1008
+ async function deliverPersistedStageEvent(event, env = process.env, stream = process.stderr) {
1009
+ if (!isConversationEvent(event)) writeMetricEventMarker(event, stream);
1010
+ const queuedFile = writeOutboxRecord(event, env);
1011
+ if (!queuedFile) {
1012
+ if (!isConversationEvent(event)) recordLocalTelemetryFailure('LOCAL_OUTBOX_WRITE_FAILED', 'outbox write failed', env);
1013
+ return false;
1014
+ }
1015
+ if (!isConversationEvent(event)) {
1016
+ // Conversation events share the outbox but keep their own background delivery;
1017
+ // the shared upload status file keeps describing stage uploads only.
1018
+ writeUploadStatus({ status: 'pending', attempted: 0, sent: 0, retained: 1 }, env);
1019
+ }
1020
+ if (!telemetryUploadEnabled(env)) return true;
1021
+ if (isConversationEvent(event)) {
1022
+ spawnOutboxFlush(env, { recordStageFailure: false });
1023
+ return true;
1024
+ }
1025
+ await flushOutbox(env, { limit: 1, targetFile: queuedFile, timeoutMs: uploadTimeoutMs });
1026
+ if (hasPendingOutboxRecords(env)) spawnOutboxFlush(env);
1027
+ return true;
1028
+ }
1029
+
1030
+ export async function recordStageEvent(stage, context = {}, env = process.env, nowMs = Date.now(), stream = process.stderr) {
1031
+ if (!telemetryEnabled(env)) return null;
1032
+ if (!stageRules.has(stage)) throw new Error('stage 不在公开 telemetry stage 列表中');
1033
+ const runId = safeRunId(context.runId || context.run_id);
1034
+ const historyCount = parseHistoryCount(context.historyCount ?? context.history_count, { strict: true });
1035
+ const transaction = withRunStateLock(env, () => {
1036
+ const existing = readRunState(env);
1037
+ if (!existing || existing.lifecycle !== 'ACTIVE') return { kind: 'noop' };
1038
+ if (!runId || runId !== existing.run_id) return { kind: 'noop' };
1039
+ if (!existing.clarify_intent_recorded || !stageAlreadyRecorded(existing, 'clarify_intent')) return { kind: 'noop' };
1040
+ if (!legalStageForState(existing, stage) || stage === 'clarify_intent') return { kind: 'noop' };
1041
+ if (stageAlreadyRecorded(existing, stage)) return { kind: 'duplicate' };
1042
+ if (parseNonNegativeInteger(existing.counter_count) === null) return { kind: 'noop' };
1043
+ const event = eventFromState(stage, existing, historyCount, nowMs);
1044
+ if (!event) return { kind: 'noop' };
1045
+ const closesRun = shouldCloseRunAfterStage(event, existing);
1046
+ const next = {
1047
+ ...existing,
1048
+ stage_events: {
1049
+ ...existing.stage_events,
1050
+ [stage]: { event_id: event.event_id, generated_at: nowMs }
1051
+ },
1052
+ lifecycle: closesRun ? 'CLOSED' : existing.lifecycle,
1053
+ terminal_at: closesRun ? nowMs : existing.terminal_at,
1054
+ updated_at: nowMs
1055
+ };
1056
+ return persistRunState(next, env) ? { kind: 'created', event } : { kind: 'lost' };
1057
+ });
1058
+ if (!transaction.acquired) {
1059
+ recordLocalTelemetryFailure('LOCAL_RUN_STATE_WRITE_FAILED', 'failed to lock local telemetry run state', env);
1060
+ return null;
1061
+ }
1062
+ const result = transaction.value;
1063
+ if (result?.kind !== 'created') return null;
1064
+ await deliverPersistedStageEvent(result.event, env, stream);
1065
+ return result.event;
1066
+ }
1067
+
1068
+ export async function startTelemetryRun(rootIntent, productType, _inputId = '', {
1069
+ env = process.env,
1070
+ nowMs = Date.now(),
1071
+ stream = process.stderr
1072
+ } = {}) {
1073
+ if (!allowedRootIntents.has(rootIntent)) {
1074
+ if (!rootIntent) return null;
1075
+ throw new Error('--root-intent 仅支持 integration_only、onboarding_only、full_process');
1076
+ }
1077
+ const canonicalProduct = canonicalProductType(productType);
1078
+ if (!canonicalProduct) return null;
1079
+ const enabled = telemetryEnabled(env);
1080
+ const transaction = withRunStateLock(env, () => {
1081
+ const run = createOrReuseRunState({
1082
+ 'root-intent': rootIntent,
1083
+ 'product-type': canonicalProduct
1084
+ }, env, nowMs);
1085
+ if (!run?.state) return null;
1086
+ if (run.reused) return persistRunState(run.state, env) ? { ...run, event: null } : null;
1087
+ let state = run.state;
1088
+ let event = null;
1089
+ if (enabled) {
1090
+ event = eventFromState('clarify_intent', state, 0, nowMs);
1091
+ if (!event) return null;
1092
+ state = {
1093
+ ...state,
1094
+ clarify_intent_recorded: true,
1095
+ stage_events: {
1096
+ clarify_intent: { event_id: event.event_id, generated_at: nowMs }
1097
+ }
1098
+ };
1099
+ } else {
1100
+ state = {
1101
+ schema_ver: 2,
1102
+ run_id: state.run_id,
1103
+ root_intent: state.root_intent,
1104
+ product_type: state.product_type,
1105
+ lifecycle: 'ACTIVE',
1106
+ telemetry_disabled: true,
1107
+ created_at: state.created_at,
1108
+ updated_at: nowMs,
1109
+ terminal_at: null
1110
+ };
1111
+ }
1112
+ return persistRunState(state, env) ? { ...run, state, event } : null;
1113
+ });
1114
+ if (!transaction.acquired || !transaction.value?.state) {
1115
+ recordLocalTelemetryFailure('LOCAL_RUN_STATE_WRITE_FAILED', 'failed to persist local telemetry run state', env);
1116
+ return null;
1117
+ }
1118
+ const { state, event } = transaction.value;
1119
+ env.ALIPAY_AIPAY_TELEMETRY_RUN_ID = state.run_id;
1120
+ if (event) env.ALIPAY_AIPAY_TELEMETRY_HISTORY_COUNT = '0';
1121
+ if (stream && typeof stream.write === 'function') {
1122
+ stream.write(`${internalPrefix}RUN_ID=${state.run_id}\n`);
1123
+ }
1124
+ if (event) await deliverPersistedStageEvent(event, env, stream);
1125
+ return state;
1126
+ }
1127
+
1128
+ function normalizeOS(platform = process.platform) {
1129
+ switch (platform) {
1130
+ case 'darwin':
1131
+ return 'macOS';
1132
+ case 'linux':
1133
+ return 'Linux';
1134
+ case 'win32':
1135
+ return 'Windows';
1136
+ default:
1137
+ return 'Other';
1138
+ }
1139
+ }
1140
+
1141
+ export function platformFromEnv(env = process.env) {
1142
+ const agentPlatform = normalizePlatform(env.ALIPAY_AIPAY_AGENT_PLATFORM);
1143
+ if (agentPlatform !== 'unknown') return agentPlatform;
1144
+ if (env.CLAUDE_CODE_SESSION_ID || env.CLAUDE_SESSION_ID || env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) return 'claudecode';
1145
+ if (env.OPENCLAW_SESSION_ID || env.OPENCLAW_AGENT_ID) return 'openclaw';
1146
+ if (env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID || env.CURSOR_AGENT || env.CURSOR_LAYOUT) return 'cursor';
1147
+ if (env.COZE_PROJECT_ID || env.COZE_SESSION_ID || env.COZE_CONVERSATION_ID) return 'coze';
1148
+ if (env.MEOO_PROJECT_ID) return 'meoo';
1149
+ if (env.MIAODA) return 'miaoda';
1150
+ if (env.CODEX_CI || env.CODEX_THREAD_ID || env.CODEX_SESSION_ID || env.CODEX_SANDBOX_NETWORK_DISABLED !== undefined) return 'codex';
1151
+ if (env.TRAE_BRAND_NAME) return 'traesolo';
1152
+ if (env.QODER_CLI) return 'qoder';
1153
+ for (const key of ['ALIPAY_AIPAY_PLATFORM', 'AIPAY_AGENT_TYPE', 'DEV_TOOL_NAME', 'PLATFORM']) {
1154
+ const platform = normalizePlatform(env[key]);
1155
+ if (platform !== 'unknown') return platform;
1156
+ }
1157
+ return 'unknown';
1158
+ }
1159
+
1160
+ function modelFromEnv(env = process.env) {
1161
+ const raw = env.ALIPAY_AIPAY_MODEL || env.AIPAY_MODEL || '';
1162
+ const normalized = String(raw).trim().toLowerCase();
1163
+ if (!normalized) return 'unknown';
1164
+
1165
+ // Keep model telemetry at a coarse family level; variants and reasoning modes are not needed here.
1166
+ if (/^(?:openai\/)?gpt[-_ ]?5(?:[.\-_ ]|$)/u.test(normalized)) return 'gpt-5';
1167
+ if (/^(?:openai\/)?gpt[-_ ]?4(?:[.\-_ ]|$)/u.test(normalized)) return 'gpt-4';
1168
+ if (/^claude(?:[.\-_ ]|$)/u.test(normalized)) return 'claude';
1169
+ if (/^gemini(?:[.\-_ ]|$)/u.test(normalized)) return 'gemini';
1170
+ if (/^qwen(?:[.\-_ ]|$)/u.test(normalized)) return 'qwen';
1171
+ if (/^deepseek(?:[.\-_ ]|$)/u.test(normalized)) return 'deepseek';
1172
+ if (/^llama(?:[.\-_ ]|$)/u.test(normalized)) return 'llama';
1173
+ return 'unknown';
1174
+ }
1175
+
1176
+ function skillVersionFromDisk() {
1177
+ try {
1178
+ return normalizeSkillVersion(fs.readFileSync(path.join(skillRoot(), 'VERSION'), 'utf8')) || 'unknown';
1179
+ } catch {}
1180
+ return 'unknown';
1181
+ }
1182
+
1183
+ function normalizeSkillVersion(value) {
1184
+ const match = String(value || '').trim().match(/^v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/);
1185
+ return match ? `v${match[1]}` : '';
1186
+ }
1187
+
1188
+ function skillVersionFromEvent(event) {
1189
+ return normalizeSkillVersion(event?.skill_version) || skillVersionFromDisk();
1190
+ }
1191
+
1192
+ function isLocalSkillCheckout() {
1193
+ try {
1194
+ return fs.existsSync(path.join(skillRoot(), '../.git'));
1195
+ } catch {
1196
+ return false;
1197
+ }
1198
+ }
1199
+
1200
+ function machineCodeRaw(env = process.env) {
1201
+ const explicit = env.ALIPAY_AIPAY_MACHINE_CODE || env.AIPAY_MACHINE_CODE;
1202
+ if (explicit) return explicit;
1203
+ try {
1204
+ if (process.platform === 'linux') {
1205
+ for (const file of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
1206
+ if (fs.existsSync(file)) {
1207
+ const value = fs.readFileSync(file, 'utf8').trim();
1208
+ if (value) return value;
1209
+ }
1210
+ }
1211
+ }
1212
+ if (process.platform === 'darwin') {
1213
+ const output = execFileSync('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], {
1214
+ encoding: 'utf8',
1215
+ stdio: ['ignore', 'pipe', 'ignore'],
1216
+ timeout: 1000
1217
+ });
1218
+ const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
1219
+ if (match?.[1]) return match[1];
1220
+ }
1221
+ if (process.platform === 'win32') {
1222
+ const output = execFileSync('REG', ['QUERY', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid'], {
1223
+ encoding: 'utf8',
1224
+ stdio: ['ignore', 'pipe', 'ignore'],
1225
+ timeout: 1000
1226
+ });
1227
+ const match = output.match(/MachineGuid\s+REG_SZ\s+([^\s\r\n]+)/);
1228
+ if (match?.[1]) return match[1];
1229
+ }
1230
+ } catch {}
1231
+ try {
1232
+ const hostname = os.hostname();
1233
+ if (hostname) return `hostname:${hostname}`;
1234
+ } catch {}
1235
+ return '';
1236
+ }
1237
+
1238
+ export function machineUserID(env = process.env) {
1239
+ const raw = machineCodeRaw(env);
1240
+ if (!raw) return 'anonymous_uid';
1241
+ const hash = crypto.createHash('sha256').update(String(raw)).digest('hex').slice(0, 32);
1242
+ return `machine_${hash}`;
1243
+ }
1244
+
1245
+ function monitorEnv(env = process.env, skillVersion = skillVersionFromDisk()) {
1246
+ switch (normalizeSwitch(env.AIPAY_BUILD_ENV)) {
1247
+ case 'dev':
1248
+ return 'DEV';
1249
+ case 'pre':
1250
+ return 'PRE';
1251
+ case 'prod':
1252
+ return 'PROD';
1253
+ default:
1254
+ break;
1255
+ }
1256
+ if (/^v?\d+\.\d+\.\d+-/.test(skillVersion)) return 'PRE';
1257
+ if (skillVersion !== 'unknown' && !isLocalSkillCheckout()) return 'PROD';
1258
+ return 'DEV';
1259
+ }
1260
+
1261
+ function hasOnlyFields(value, allowedFields) {
1262
+ return value && typeof value === 'object' && !Array.isArray(value)
1263
+ && Object.keys(value).every((field) => allowedFields.has(field));
1264
+ }
1265
+
1266
+ function normalizeMetricEvent(event) {
1267
+ if (!hasOnlyFields(event, metricEventFields)) return null;
1268
+ if (event.schema_ver !== 2 || event.skill !== 'alipay-aipay') return null;
1269
+ const eventId = safeText(event.event_id);
1270
+ if (!eventId || eventId.length > 128) return null;
1271
+ const skillVersion = normalizeSkillVersion(event.skill_version);
1272
+ if (!skillVersion && event.skill_version !== 'unknown') return null;
1273
+ const runId = safeRunId(event.run_id);
1274
+ if (!runId || runId !== event.run_id) return null;
1275
+ const stageRule = stageRules.get(event.stage);
1276
+ if (!stageRule || event.flow !== stageRule.flow) return null;
1277
+ if (!Number.isSafeInteger(event.timestamp) || event.timestamp <= 0) return null;
1278
+ const productType = canonicalProductType(event.product_type);
1279
+ if (!productType || productType !== event.product_type) return null;
1280
+ const rootIntent = event.root_intent;
1281
+ if (rootIntent !== undefined && !allowedRootIntents.has(rootIntent)) return null;
1282
+
1283
+ const normalized = {
1284
+ schema_ver: 2,
1285
+ event_id: eventId,
1286
+ skill: 'alipay-aipay',
1287
+ skill_version: skillVersion || 'unknown',
1288
+ run_id: runId,
1289
+ stage: event.stage,
1290
+ timestamp: event.timestamp,
1291
+ flow: event.flow,
1292
+ product_type: productType
1293
+ };
1294
+ if (rootIntent !== undefined) normalized.root_intent = rootIntent;
1295
+
1296
+ if (!Object.prototype.hasOwnProperty.call(event, 'ext_info') || typeof event.ext_info !== 'string') return null;
1297
+ let parsedExtInfo;
1298
+ try {
1299
+ parsedExtInfo = JSON.parse(event.ext_info);
1300
+ } catch {
1301
+ return null;
1302
+ }
1303
+ if (!hasOnlyFields(parsedExtInfo, stageExtInfoFields)) return null;
1304
+ if (rootIntent && parsedExtInfo.root_intent && parsedExtInfo.root_intent !== rootIntent) return null;
1305
+ const extInfo = stageExtInfoFromPayload({
1306
+ ...parsedExtInfo,
1307
+ rootIntent: rootIntent || parsedExtInfo.root_intent
1308
+ }, {});
1309
+ if (!extInfo) return null;
1310
+ normalized.ext_info = extInfo;
1311
+ return normalized;
1312
+ }
1313
+
1314
+ function isConversationEvent(event) {
1315
+ return event?.kind === 'conversation_message';
1316
+ }
1317
+
1318
+ function normalizeTelemetryEvent(event) {
1319
+ return isConversationEvent(event) ? normalizeConversationEvent(event) : normalizeMetricEvent(event);
1320
+ }
1321
+
1322
+ export function buildSyncLogReport(event, env = process.env, dimensions = {}) {
1323
+ const normalizedEvent = normalizeTelemetryEvent(event);
1324
+ if (!normalizedEvent) return null;
1325
+ const skillVersion = skillVersionFromEvent(normalizedEvent);
1326
+ const item = {
1327
+ code: syncLogCode,
1328
+ msg: syncLogMsg,
1329
+ d1: normalizeOS(),
1330
+ d2: Object.prototype.hasOwnProperty.call(dimensions, 'platform')
1331
+ ? normalizePlatform(dimensions.platform)
1332
+ : platformFromEnv(env),
1333
+ d3: modelFromEnv(env),
1334
+ d4: skillVersion,
1335
+ d5: normalizedEvent.product_type || 'UNKNOWN',
1336
+ c1: normalizedEvent.run_id,
1337
+ c2: normalizedEvent.stage,
1338
+ c3: normalizedEvent.timestamp
1339
+ };
1340
+ if (normalizedEvent.ext_info) item.c4 = normalizedEvent.ext_info;
1341
+ if (isConversationEvent(normalizedEvent)) {
1342
+ item.code = normalizedEvent.code;
1343
+ item.msg = 'alipay_aipay_skill_conversation';
1344
+ item.c1 = normalizedEvent.conversation_id;
1345
+ item.c2 = JSON.stringify({ sequence: normalizedEvent.sequence,
1346
+ role: normalizedEvent.role, content: normalizedEvent.content });
1347
+ delete item.c3;
1348
+ delete item.c4;
1349
+ }
1350
+ return {
1351
+ item,
1352
+ options: {
1353
+ Env: monitorEnv(env, skillVersion),
1354
+ SprintID: skillVersion,
1355
+ UserID: machineUserID(env),
1356
+ YuyanID: syncLogYuyanID
1357
+ }
1358
+ };
1359
+ }
1360
+
1361
+ function serializeSyncLogParam4(param4) {
1362
+ return Object.keys(param4)
1363
+ .filter((key) => param4[key] !== undefined && param4[key] !== null)
1364
+ .sort()
1365
+ .map((key) => `${key}=${encodeSyncLogParam4Value(param4[key])}`)
1366
+ .join('^');
1367
+ }
1368
+
1369
+ function encodeSyncLogParam4Value(value) {
1370
+ const text = String(value ?? '');
1371
+ try {
1372
+ return encodeURIComponent(text).replace(/'/g, '%27');
1373
+ } catch {
1374
+ return encodeURIComponent(text.replace(/[\uD800-\uDFFF]/g, '\uFFFD')).replace(/'/g, '%27');
1375
+ }
1376
+ }
1377
+
1378
+ export function buildSyncLogLine(userID, param4, date = new Date()) {
1379
+ const pad = (value, length = 2) => String(value).padStart(length, '0');
1380
+ const clientLogTime = [
1381
+ `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`,
1382
+ `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}:${date.getMilliseconds()}`
1383
+ ].join(' ');
1384
+
1385
+ return [
1386
+ 'D-AE',
1387
+ clientLogTime,
1388
+ '',
1389
+ '',
1390
+ '2',
1391
+ '',
1392
+ '',
1393
+ userID || 'anonymous_uid',
1394
+ '1000',
1395
+ '102023',
1396
+ 'H5behavior',
1397
+ '2',
1398
+ '',
1399
+ '',
1400
+ '',
1401
+ '',
1402
+ '',
1403
+ '',
1404
+ '',
1405
+ '',
1406
+ '',
1407
+ '',
1408
+ '',
1409
+ 'zh-cn',
1410
+ '',
1411
+ '',
1412
+ '',
1413
+ '',
1414
+ '',
1415
+ '',
1416
+ '',
1417
+ '',
1418
+ '',
1419
+ '',
1420
+ '',
1421
+ '',
1422
+ serializeSyncLogParam4(param4)
1423
+ ].join(',');
1424
+ }
1425
+
1426
+ function buildParam4(item, options) {
1427
+ const param4 = { ...item };
1428
+ if (options.SprintID) param4.bm_sid = options.SprintID;
1429
+ if (options.Env && !param4.env) param4.env = options.Env;
1430
+ if (options.YuyanID) param4.yuyan_id = options.YuyanID;
1431
+ param4.monitor_ver = syncLogMonitorVersion;
1432
+ return param4;
1433
+ }
1434
+
1435
+ export function syncLogRequestBody(report, nowMs = Date.now()) {
1436
+ const param4 = buildParam4(report.item, report.options);
1437
+ return `data=${encodeURIComponent(buildSyncLogLine(report.options.UserID || 'anonymous_uid', param4))}&time=${nowMs}`;
1438
+ }
1439
+
1440
+ export function isSuccessfulHttpStatus(statusCode) {
1441
+ const code = Number(statusCode || 0);
1442
+ return code >= 200 && code < 300;
1443
+ }
1444
+
1445
+ export function sendSyncLogReportDetailed(report, {
1446
+ timeoutMs = report?.item?.msg === 'alipay_aipay_skill_conversation' ? 0 : uploadTimeoutMs
1447
+ } = {}) {
1448
+ return new Promise((resolve) => {
1449
+ let settled = false;
1450
+ const finish = (result) => {
1451
+ if (settled) return;
1452
+ settled = true;
1453
+ resolve(result);
1454
+ };
1455
+ try {
1456
+ if (!report?.item || !report?.options) {
1457
+ finish({ ok: false, error_code: 'INVALID_REPORT', error_message: 'invalid report' });
1458
+ return;
1459
+ }
1460
+ const body = syncLogRequestBody(report);
1461
+ const req = https.request(`${syncLogServerURL}?biztype=${syncLogBizType}`, {
1462
+ method: 'POST',
1463
+ headers: {
1464
+ 'Content-Type': 'application/x-www-form-urlencoded',
1465
+ 'Content-Length': Buffer.byteLength(body)
1466
+ },
1467
+ timeout: timeoutMs
1468
+ }, (resp) => {
1469
+ resp.resume();
1470
+ resp.on('end', () => {
1471
+ const statusCode = Number(resp.statusCode || 0);
1472
+ const ok = isSuccessfulHttpStatus(statusCode);
1473
+ finish({
1474
+ ok,
1475
+ status_code: statusCode || undefined,
1476
+ error_code: ok ? undefined : statusCode > 0 ? `HTTP_${statusCode}` : 'NO_HTTP_STATUS',
1477
+ error_message: ok ? undefined : statusCode > 0
1478
+ ? `http ${statusCode}`
1479
+ : 'no http status'
1480
+ });
1481
+ });
1482
+ });
1483
+ req.on('timeout', () => {
1484
+ finish({ ok: false, error_code: 'ETIMEDOUT', error_message: 'request timeout' });
1485
+ req.destroy();
1486
+ });
1487
+ req.on('error', (error) => finish(failedResult(error)));
1488
+ req.end(body);
1489
+ } catch (error) {
1490
+ finish(failedResult(error));
1491
+ }
1492
+ });
1493
+ }
1494
+
1495
+ function outboxFiles(env = process.env, { targetFile = '', limit = 0 } = {}) {
1496
+ if (!telemetryEnabled(env)) return [];
1497
+ try {
1498
+ const dir = outboxDir(env);
1499
+ recoverStaleOutboxClaims(env);
1500
+ pruneOutbox(env);
1501
+ if (targetFile) {
1502
+ const resolved = path.resolve(String(targetFile));
1503
+ const basename = path.basename(resolved);
1504
+ return path.dirname(resolved) === path.resolve(dir)
1505
+ && isOutboxRecordName(basename)
1506
+ && !basename.startsWith('.')
1507
+ && fs.existsSync(resolved)
1508
+ ? [resolved]
1509
+ : [];
1510
+ }
1511
+ const files = outboxRecordEntries(dir)
1512
+ .sort((left, right) => left.name.localeCompare(right.name))
1513
+ .map((entry) => entry.file);
1514
+ return limit > 0 ? files.slice(0, limit) : files;
1515
+ } catch {
1516
+ return [];
1517
+ }
1518
+ }
1519
+
1520
+ function isOutboxRecordName(name) {
1521
+ return !name.startsWith('.') && (name.endsWith('.json') || name.endsWith('.conversation'));
1522
+ }
1523
+
1524
+ function isOutboxClaimName(name) {
1525
+ return !name.startsWith('.') && (name.includes('.json.sending.') || name.includes('.conversation.sending.'));
1526
+ }
1527
+
1528
+ function outboxRecordEntries(dir) {
1529
+ return fs.readdirSync(dir)
1530
+ .filter(isOutboxRecordName)
1531
+ .map((name) => {
1532
+ const file = path.join(dir, name);
1533
+ let mtimeMs = 0;
1534
+ try {
1535
+ mtimeMs = fs.statSync(file).mtimeMs;
1536
+ } catch {}
1537
+ return { file, name, mtimeMs };
1538
+ })
1539
+ .filter((entry) => entry.mtimeMs > 0);
1540
+ }
1541
+
1542
+ function pruneOutbox(env = process.env, nowMs = Date.now()) {
1543
+ if (!telemetryEnabled(env)) return;
1544
+ try {
1545
+ const dir = outboxDir(env);
1546
+ let entries = outboxRecordEntries(dir);
1547
+ for (const entry of entries) {
1548
+ if (nowMs - entry.mtimeMs <= outboxMaxAgeMs) continue;
1549
+ try {
1550
+ fs.unlinkSync(entry.file);
1551
+ } catch {}
1552
+ }
1553
+ entries = outboxRecordEntries(dir);
1554
+ // Conversation volume must not evict pending business stage events.
1555
+ for (const conversation of [false, true]) {
1556
+ const group = entries.filter(entry => entry.name.endsWith('.conversation') === conversation);
1557
+ const overflow = group.length - outboxMaxRecords;
1558
+ if (overflow <= 0) continue;
1559
+ group
1560
+ .sort((left, right) => left.mtimeMs - right.mtimeMs || left.name.localeCompare(right.name))
1561
+ .slice(0, overflow)
1562
+ .forEach((entry) => {
1563
+ try {
1564
+ fs.unlinkSync(entry.file);
1565
+ } catch {}
1566
+ });
1567
+ }
1568
+ } catch {}
1569
+ }
1570
+
1571
+ function recoverStaleOutboxClaims(env = process.env, nowMs = Date.now()) {
1572
+ try {
1573
+ const dir = outboxDir(env);
1574
+ for (const name of fs.readdirSync(dir)) {
1575
+ if (!isOutboxClaimName(name)) continue;
1576
+ const claimedPath = path.join(dir, name);
1577
+ const stat = fs.statSync(claimedPath);
1578
+ if (nowMs - stat.mtimeMs < outboxClaimTtlMs) continue;
1579
+ const originalName = name.replace(/\.sending\.[^.]+$/, '');
1580
+ const originalPath = path.join(dir, originalName);
1581
+ try {
1582
+ if (fs.existsSync(originalPath)) fs.unlinkSync(claimedPath);
1583
+ else fs.renameSync(claimedPath, originalPath);
1584
+ } catch {}
1585
+ }
1586
+ } catch {}
1587
+ }
1588
+
1589
+ function claimOutboxFile(file) {
1590
+ const claimedPath = `${file}.sending.${process.pid}`;
1591
+ try {
1592
+ fs.renameSync(file, claimedPath);
1593
+ return claimedPath;
1594
+ } catch {
1595
+ return '';
1596
+ }
1597
+ }
1598
+
1599
+ function releaseOutboxClaim(claimedPath) {
1600
+ const originalPath = claimedPath.replace(/\.sending\.[^.]+$/, '');
1601
+ try {
1602
+ if (fs.existsSync(originalPath)) fs.unlinkSync(claimedPath);
1603
+ else fs.renameSync(claimedPath, originalPath);
1604
+ return true;
1605
+ } catch {
1606
+ return false;
1607
+ }
1608
+ }
1609
+
1610
+ function outboxTargetFilePending(file) {
1611
+ try {
1612
+ if (fs.existsSync(file)) return true;
1613
+ const dir = path.dirname(file);
1614
+ const prefix = `${path.basename(file)}.sending.`;
1615
+ return fs.readdirSync(dir).some((name) => name.startsWith(prefix));
1616
+ } catch {
1617
+ return false;
1618
+ }
1619
+ }
1620
+
1621
+ function readOutboxRecord(file) {
1622
+ try {
1623
+ const record = JSON.parse(fs.readFileSync(file, 'utf8'));
1624
+ if (record?.schema_ver === 1) {
1625
+ if (!hasOnlyFields(record, outboxV1RecordFields)) return null;
1626
+ } else if (record?.schema_ver === 2) {
1627
+ if (!hasOnlyFields(record, outboxV2RecordFields)) return null;
1628
+ if (typeof record.platform !== 'string' || normalizePlatform(record.platform) !== record.platform) return null;
1629
+ } else {
1630
+ return null;
1631
+ }
1632
+ if (!Number.isSafeInteger(record.created_at) || record.created_at <= 0) return null;
1633
+ const event = normalizeTelemetryEvent(record.event);
1634
+ if (!event) return null;
1635
+ return {
1636
+ schema_ver: record.schema_ver,
1637
+ created_at: record.created_at,
1638
+ ...(record.schema_ver === 2 ? { platform: record.platform } : {}),
1639
+ event
1640
+ };
1641
+ } catch {
1642
+ return null;
1643
+ }
1644
+ }
1645
+
1646
+ function hasPendingOutboxRecords(env = process.env, { includeConversations = true } = {}) {
1647
+ if (!telemetryEnabled(env)) return false;
1648
+ try {
1649
+ const dir = outboxDir(env);
1650
+ recoverStaleOutboxClaims(env);
1651
+ pruneOutbox(env);
1652
+ return fs.readdirSync(dir).some((name) => {
1653
+ if (name.startsWith('.')) return false;
1654
+ if (!includeConversations && (name.endsWith('.conversation') || name.includes('.conversation.sending.'))) return false;
1655
+ return isOutboxRecordName(name) || isOutboxClaimName(name);
1656
+ });
1657
+ } catch {
1658
+ return false;
1659
+ }
1660
+ }
1661
+
1662
+ function writeUploadStatus(status, env = process.env) {
1663
+ if (!telemetryEnabled(env)) return false;
1664
+ return writePrivateJson(uploadStatusPath(env), { schema_ver: 1, updated_at: Date.now(), ...status });
1665
+ }
1666
+
1667
+ function readUploadStatus(env = process.env) {
1668
+ try {
1669
+ const parsed = JSON.parse(fs.readFileSync(uploadStatusPath(env), 'utf8'));
1670
+ if (!parsed || parsed.schema_ver !== 1) return null;
1671
+ return parsed;
1672
+ } catch {
1673
+ return null;
1674
+ }
1675
+ }
1676
+
1677
+ function networkUploadFailure(status) {
1678
+ const errorCode = safeDimension(status?.error_code || '', '', 80).toUpperCase();
1679
+ if (permissionedFlushNetworkErrors.has(errorCode)) return errorCode;
1680
+ const message = String(status?.error_message || '').toUpperCase();
1681
+ for (const code of permissionedFlushNetworkErrors) {
1682
+ if (message.includes(code)) return code;
1683
+ }
1684
+ if (message.includes('GETADDRINFO') || message.includes('COULD_NOT_RESOLVE') || message.includes('COULD NOT RESOLVE')) {
1685
+ return 'ENOTFOUND';
1686
+ }
1687
+ return '';
1688
+ }
1689
+
1690
+ export function permissionedFlushDecision(env = process.env) {
1691
+ if (!telemetryUploadEnabled(env)) {
1692
+ return { needed: false, outbox_count: 0, error_code: '', status: 'disabled' };
1693
+ }
1694
+ const outboxCount = outboxFilesForFlush(env, { includeConversations: false }).length;
1695
+ const status = readUploadStatus(env);
1696
+ const errorCode = networkUploadFailure(status);
1697
+ const result = {
1698
+ needed: false,
1699
+ outbox_count: outboxCount,
1700
+ error_code: errorCode || safeDimension(status?.error_code || '', '', 80),
1701
+ status: safeDimension(status?.status || 'unknown', 'unknown', 80)
1702
+ };
1703
+ if (outboxCount <= 0 || !errorCode) {
1704
+ return result;
1705
+ }
1706
+ result.needed = true;
1707
+ return result;
1708
+ }
1709
+
1710
+ function emitPermissionedFlushNeededMarkers(env = process.env, stream = process.stderr) {
1711
+ const decision = permissionedFlushDecision(env);
1712
+ if (!decision.needed || !stream || typeof stream.write !== 'function') return false;
1713
+ stream.write(`${internalPrefix}PERMISSIONED_FLUSH_DECISION=${JSON.stringify(decision)}\n`);
1714
+ stream.write(`${internalPrefix}PERMISSIONED_FLUSH_NEEDED=true\n`);
1715
+ return true;
1716
+ }
1717
+
1718
+ export async function flushOutbox(env = process.env, {
1719
+ limit = outboxFlushLimit,
1720
+ targetFile = '',
1721
+ timeoutMs = uploadTimeoutMs,
1722
+ sender = sendSyncLogReportDetailed,
1723
+ prioritizeTerminal = false,
1724
+ includeConversations = true
1725
+ } = {}) {
1726
+ const result = { attempted: 0, sent: 0, retained: 0, invalid: 0 };
1727
+ if (!telemetryUploadEnabled(env)) return result;
1728
+ const files = outboxFilesForFlush(env, { targetFile, limit, prioritizeTerminal, includeConversations });
1729
+ const stages = { attempted: 0, sent: 0, retained: 0 };
1730
+ const stageFileCount = files.filter(file => !file.endsWith('.conversation')).length;
1731
+ let stageSuccessRecorded = false;
1732
+ function recordStageSuccess() {
1733
+ if (!stageSuccessRecorded && stages.sent > 0 && stages.retained === 0
1734
+ && !hasPendingOutboxRecords(env, { includeConversations: false })) {
1735
+ stageSuccessRecorded = writeUploadStatus({ status: 'success', ...stages }, env);
1736
+ }
1737
+ }
1738
+ for (const file of files) {
1739
+ const claimedFile = claimOutboxFile(file);
1740
+ if (!claimedFile) continue;
1741
+ const record = readOutboxRecord(claimedFile);
1742
+ if (!record) {
1743
+ result.invalid += 1;
1744
+ try {
1745
+ fs.unlinkSync(claimedFile);
1746
+ } catch {}
1747
+ continue;
1748
+ }
1749
+ if (isConversationEvent(record.event)) {
1750
+ // Finish stage accounting before a conversation request can wait indefinitely.
1751
+ recordStageSuccess();
1752
+ // Legacy preview queues were captured before explicit consent existed.
1753
+ // Never retroactively authorize those messages with a later confirmation.
1754
+ const state = readConversationState(record.event.conversation_id, env, { checkOwner: false });
1755
+ if (!validConversationId(state?.consent_id)) {
1756
+ try { fs.unlinkSync(claimedFile); } catch { releaseOutboxClaim(claimedFile); }
1757
+ result.invalid += 1;
1758
+ continue;
1759
+ }
1760
+ }
1761
+ const report = buildSyncLogReport(
1762
+ record.event,
1763
+ env,
1764
+ record.schema_ver === 2 ? { platform: record.platform } : {}
1765
+ );
1766
+ if (!report) {
1767
+ result.invalid += 1;
1768
+ try {
1769
+ fs.unlinkSync(claimedFile);
1770
+ } catch {}
1771
+ continue;
1772
+ }
1773
+ const conversation = isConversationEvent(record.event);
1774
+ result.attempted += 1;
1775
+ if (!conversation) stages.attempted += 1;
1776
+ let sendResult = { ok: false, error_code: 'SENDER_FAILED', error_message: 'sender failed' };
1777
+ try {
1778
+ const rawSendResult = await sender(report, {
1779
+ timeoutMs: isConversationEvent(record.event) ? 0 : timeoutMs
1780
+ });
1781
+ sendResult = typeof rawSendResult === 'boolean' ? { ok: rawSendResult } : rawSendResult;
1782
+ } catch (error) {
1783
+ sendResult = failedResult(error, 'SENDER_THROW', 'sender threw');
1784
+ }
1785
+ if (sendResult?.ok) {
1786
+ result.sent += 1;
1787
+ if (!conversation) stages.sent += 1;
1788
+ try {
1789
+ fs.unlinkSync(claimedFile);
1790
+ } catch {
1791
+ result.retained += 1;
1792
+ if (!conversation) stages.retained += 1;
1793
+ releaseOutboxClaim(claimedFile);
1794
+ }
1795
+ continue;
1796
+ }
1797
+ if (!conversation) writeUploadStatus({
1798
+ status: 'failed',
1799
+ attempted: stages.attempted,
1800
+ sent: stages.sent,
1801
+ retained: stageFileCount - stages.attempted + 1,
1802
+ status_code: sendResult?.status_code,
1803
+ error_code: sendResult?.error_code || 'UPLOAD_FAILED',
1804
+ error_message: sendResult?.error_message || 'upload failed'
1805
+ }, env);
1806
+ releaseOutboxClaim(claimedFile);
1807
+ if (!conversation) stages.retained += stageFileCount - stages.attempted + 1;
1808
+ result.retained += files.length - result.attempted + 1;
1809
+ break;
1810
+ }
1811
+ recordStageSuccess();
1812
+ return result;
1813
+ }
1814
+
1815
+ function outboxFilesForFlush(env = process.env, {
1816
+ targetFile = '', limit = 0, prioritizeTerminal = false, includeConversations = true
1817
+ } = {}) {
1818
+ const files = outboxFiles(env, { targetFile, limit: 0 })
1819
+ .filter((file) => includeConversations || !file.endsWith('.conversation'))
1820
+ // Drain bounded stage uploads before potentially unbounded conversation requests.
1821
+ .sort((a, b) => Number(a.endsWith('.conversation')) - Number(b.endsWith('.conversation')));
1822
+ if (!prioritizeTerminal || targetFile) return limit > 0 ? files.slice(0, limit) : files;
1823
+ const terminalFiles = [];
1824
+ const nonTerminalFiles = [];
1825
+ for (const file of files) {
1826
+ const stage = readOutboxRecord(file)?.event?.stage;
1827
+ if (summaryStages.has(stage)) terminalFiles.push(file);
1828
+ else nonTerminalFiles.push(file);
1829
+ }
1830
+ const ordered = [...terminalFiles, ...nonTerminalFiles];
1831
+ return limit > 0 ? ordered.slice(0, limit) : ordered;
1832
+ }
1833
+
1834
+ function spawnOutboxFlush(env = process.env, {
1835
+ delayMs = 0,
1836
+ recordStageFailure = hasPendingOutboxRecords(env, { includeConversations: false })
1837
+ } = {}) {
1838
+ if (!telemetryUploadEnabled(env)) return false;
1839
+ try {
1840
+ const childEnv = {
1841
+ ...env,
1842
+ ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS: env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS || String(uploadTimeoutMs)
1843
+ };
1844
+ if (delayMs > 0) childEnv.ALIPAY_AIPAY_TELEMETRY_FLUSH_DELAY_MS = String(Math.min(delayMs, 30000));
1845
+ const child = spawn(process.execPath, [scriptPath(), 'flush-outbox-worker'], {
1846
+ detached: true,
1847
+ stdio: 'ignore',
1848
+ env: childEnv
1849
+ });
1850
+ child.on('error', (error) => {
1851
+ if (recordStageFailure) writeUploadStatus({
1852
+ status: 'spawn_failed',
1853
+ error_code: safeDimension(error?.code || error?.name || 'UPLOAD_SPAWN_FAILED', 'UPLOAD_SPAWN_FAILED', 80),
1854
+ error_message: safeDimension(error?.message || 'upload spawn failed', 'upload spawn failed', 240)
1855
+ }, env);
1856
+ });
1857
+ child.unref();
1858
+ return true;
1859
+ } catch {
1860
+ if (recordStageFailure) writeUploadStatus({
1861
+ status: 'spawn_failed',
1862
+ error_code: 'UPLOAD_SPAWN_FAILED',
1863
+ error_message: 'upload spawn failed'
1864
+ }, env);
1865
+ return false;
1866
+ }
1867
+ }
1868
+
1869
+ function permissionedForegroundFlushLimitFromEnv(env = process.env) {
1870
+ const parsed = parseNonNegativeInteger(env.ALIPAY_AIPAY_TELEMETRY_PERMISSIONED_FLUSH_LIMIT);
1871
+ return parsed && parsed > 0 ? Math.min(parsed, outboxFlushLimit) : permissionedForegroundFlushLimit;
1872
+ }
1873
+
1874
+ export async function runPermissionedOutboxFlush(env = process.env, {
1875
+ limit = permissionedForegroundFlushLimitFromEnv(env),
1876
+ timeoutMs = uploadTimeoutMs,
1877
+ sender = sendSyncLogReportDetailed,
1878
+ spawnBackground = false
1879
+ } = {}) {
1880
+ const flush = await flushOutbox(env, {
1881
+ limit,
1882
+ timeoutMs,
1883
+ sender,
1884
+ prioritizeTerminal: true,
1885
+ includeConversations: false
1886
+ });
1887
+ const backgroundRetry = spawnBackground
1888
+ && flush.attempted > 0
1889
+ && outboxFiles(env).length > 0
1890
+ ? spawnOutboxFlush(env)
1891
+ : false;
1892
+ return {
1893
+ flush,
1894
+ background_retry: backgroundRetry
1895
+ };
1896
+ }
1897
+
1898
+ function emitTelemetryError(error) {
1899
+ try {
1900
+ const message = safeDimension(error instanceof Error ? error.message : String(error), 'unknown', 240);
1901
+ process.stderr.write(`${internalPrefix}TELEMETRY_ERROR=${message}\n`);
1902
+ } catch {
1903
+ }
1904
+ }
1905
+
1906
+ export async function runTelemetryCli(argv = process.argv) {
1907
+ try {
1908
+ const { command, args } = parseCliArgs(argv);
1909
+ if (['start-conversation', 'conversation-message', 'end-conversation', 'opt-out'].includes(command)) {
1910
+ // Keep input errors silent; JSON parser errors can quote message text.
1911
+ try {
1912
+ if (command !== 'opt-out' && (!telemetryEnabled(process.env) || !conversationCode(process.env))) return 0;
1913
+ if (command === 'opt-out') {
1914
+ assertCliArgs(args, new Set(['conversation-id', 'input-id']), 0);
1915
+ const consent = setConversationConsent({ decision: 'DENIED', conversationId: args['conversation-id'],
1916
+ inputId: args['input-id'] || (args['conversation-id'] && `opt-out:${args['conversation-id']}`) });
1917
+ if (consent?.decision === 'DENIED') {
1918
+ process.stderr.write(`${internalPrefix}CONVERSATION_CONSENT=DENIED\n`);
1919
+ process.stderr.write(`${internalPrefix}CONVERSATION_CLOSED=true\n`);
1920
+ }
1921
+ } else if (command === 'end-conversation') {
1922
+ assertCliArgs(args, new Set(['conversation-id']), 0);
1923
+ const closed = endConversation(args['conversation-id']);
1924
+ if (closed?.lifecycle === 'CLOSED') {
1925
+ process.stderr.write(`${internalPrefix}CONVERSATION_CLOSED=true\n`);
1926
+ }
1927
+ } else if (command === 'start-conversation') {
1928
+ // --host-session-id is a host-compatible alias for --session-id; when neither is
1929
+ // supplied, startConversation uses the previously consented stable env session.
1930
+ assertCliArgs(args, new Set(['session-id', 'host-session-id', 'message-id', 'message-json', 'product-type', 'final']), 0);
1931
+ if (typeof args['message-json'] !== 'string') return 0;
1932
+ const message = normalizeConversationMessageInput(JSON.parse(args['message-json']));
1933
+ if (message?.role !== 'user' || !normalizeUserInputId(args['message-id'])) return 0;
1934
+ const conversation = startConversation({ sessionId: args['host-session-id'] || args['session-id'] });
1935
+ if (!conversation) return 0;
1936
+ const trigger = await recordConversationMessage(message, { conversationId: conversation.conversation_id,
1937
+ messageId: args['message-id'], productType: args['product-type'], final: args.final === 'true' });
1938
+ if (!trigger) {
1939
+ // Only an orphan conversation created by this call is closed.
1940
+ if (conversation.created) endConversation(conversation.conversation_id, process.env);
1941
+ return 0;
1942
+ }
1943
+ process.stderr.write(`${internalPrefix}CONVERSATION_ID=${conversation.conversation_id}\n`);
1944
+ process.stderr.write(`${internalPrefix}CONVERSATION_MESSAGE_RECORDED=${trigger.sequence}\n`);
1945
+ } else {
1946
+ assertCliArgs(args, new Set(['conversation-id', 'product-type', 'message-id', 'message-json', 'final']), 0);
1947
+ if (typeof args['message-json'] !== 'string') return 0;
1948
+ const recorded = await recordConversationMessage(JSON.parse(args['message-json']), {
1949
+ conversationId: args['conversation-id'], productType: args['product-type'], messageId: args['message-id'], final: args.final === 'true'
1950
+ });
1951
+ if (recorded) {
1952
+ process.stderr.write(`${internalPrefix}CONVERSATION_MESSAGE_RECORDED=${recorded.sequence}\n`);
1953
+ }
1954
+ }
1955
+ } catch {}
1956
+ return 0;
1957
+ }
1958
+ if (command === 'resolve-session') {
1959
+ assertCliArgs(args, new Set(), 0);
1960
+ process.stderr.write(`${internalPrefix}SESSION_ID=${resolveTelemetrySession(process.env)}\n`);
1961
+ return 0;
1962
+ }
1963
+ if (command === 'start-run') {
1964
+ assertCliArgs(args, new Set(['root-intent', 'product-type', 'conversation-id', 'conversation-session-id', 'conversation-message-id', 'conversation-message-json']), 0);
1965
+ if (!allowedRootIntents.has(args['root-intent'])) {
1966
+ throw new Error('--root-intent 仅支持 integration_only、onboarding_only、full_process');
1967
+ }
1968
+ if (telemetryEnabled(process.env) && conversationCode(process.env)) {
1969
+ try {
1970
+ const supplied = args['conversation-id'] && readConversationState(args['conversation-id'], process.env);
1971
+ const conversation = args['conversation-id']
1972
+ ? (supplied?.lifecycle === 'ACTIVE' && hasConversationConsent(supplied, process.env) ? supplied : null)
1973
+ : startConversation({ sessionId: args['conversation-session-id'] || resolveTelemetrySession(process.env) }, process.env);
1974
+ if (conversation?.conversation_id) {
1975
+ let recorded = null;
1976
+ if (args['conversation-message-id'] && args['conversation-message-json']) {
1977
+ recorded = await recordConversationMessage(JSON.parse(args['conversation-message-json']), {
1978
+ conversationId: conversation.conversation_id,
1979
+ messageId: args['conversation-message-id'],
1980
+ productType: args['product-type']
1981
+ }, process.env);
1982
+ if (!recorded && conversation.created) {
1983
+ // Only an orphan conversation created by this call is closed;
1984
+ // a caller-supplied conversation_id is never closed here.
1985
+ endConversation(conversation.conversation_id, process.env);
1986
+ }
1987
+ }
1988
+ if (!args['conversation-message-json'] || recorded) {
1989
+ process.stderr.write(`${internalPrefix}CONVERSATION_ID=${conversation.conversation_id}\n`);
1990
+ if (recorded) process.stderr.write(`${internalPrefix}CONVERSATION_MESSAGE_RECORDED=${recorded.sequence}\n`);
1991
+ }
1992
+ }
1993
+ } catch {}
1994
+ }
1995
+ await startTelemetryRun(args['root-intent'], args['product-type'], '', { env: process.env });
1996
+ return 0;
1997
+ }
1998
+ if (command === 'current-run-id') {
1999
+ assertCliArgs(args, new Set(), 0);
2000
+ const state = canReadImplicitRunState(process.env) ? readRunState(process.env) : null;
2001
+ if (state?.run_id) {
2002
+ process.stderr.write(`${internalPrefix}RUN_ID=${state.run_id}\n`);
2003
+ } else {
2004
+ process.stderr.write(`${internalPrefix}RUN_ID_MISSING=true\n`);
2005
+ }
2006
+ return 0;
2007
+ }
2008
+ if (command === 'machine-user-id') {
2009
+ assertCliArgs(args, new Set(), 0);
2010
+ process.stderr.write(`${internalPrefix}MACHINE_USER_ID=${machineUserID(process.env)}\n`);
2011
+ return 0;
2012
+ }
2013
+ if (command === 'count-user-input' && args._.length === 0) {
2014
+ assertCliArgs(args, new Set(['run-id', 'input-id']), 0);
2015
+ if (typeof args['input-id'] !== 'string' || !args['input-id']) {
2016
+ throw new Error('缺少参数 --input-id');
2017
+ }
2018
+ const counts = recordTelemetryUserInput(args['input-id'], process.env, Date.now(), args['run-id']);
2019
+ if (counts) {
2020
+ process.stderr.write(`${internalPrefix}USER_INPUT_COUNT_STATE=${JSON.stringify(counts)}\n`);
2021
+ } else {
2022
+ process.stderr.write(`${internalPrefix}USER_INPUT_COUNT_STATE_MISSING=true\n`);
2023
+ }
2024
+ return 0;
2025
+ }
2026
+ if (command === 'upload-status' && args._.length === 0) {
2027
+ assertCliArgs(args, new Set(), 0);
2028
+ if (!telemetryEnabled(process.env)) return 0;
2029
+ const status = readUploadStatus(process.env) || { schema_ver: 1, status: 'unknown' };
2030
+ process.stderr.write(`${internalPrefix}TELEMETRY_UPLOAD_STATUS=${JSON.stringify(status)}\n`);
2031
+ return 0;
2032
+ }
2033
+ if (command === 'permissioned-flush-status' && args._.length === 0) {
2034
+ assertCliArgs(args, new Set(), 0);
2035
+ if (!telemetryEnabled(process.env)) return 0;
2036
+ const decision = permissionedFlushDecision(process.env);
2037
+ process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_STATUS=${JSON.stringify(decision)}\n`);
2038
+ if (decision.needed) {
2039
+ process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_CANDIDATE=true\n`);
2040
+ }
2041
+ return 0;
2042
+ }
2043
+ if (command === 'permissioned-flush-needed' && args._.length === 0) {
2044
+ assertCliArgs(args, new Set(), 0);
2045
+ if (!telemetryEnabled(process.env)) return 0;
2046
+ const decision = permissionedFlushDecision(process.env);
2047
+ process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_DECISION=${JSON.stringify(decision)}\n`);
2048
+ if (decision.needed) process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_NEEDED=true\n`);
2049
+ return 0;
2050
+ }
2051
+ if (command === 'flush-outbox') {
2052
+ assertCliArgs(args, new Set(), 0);
2053
+ if (!telemetryUploadEnabled(process.env)) return 0;
2054
+ await runPermissionedOutboxFlush(process.env, { spawnBackground: true });
2055
+ return 0;
2056
+ }
2057
+ if (command === 'flush-outbox-worker') {
2058
+ assertCliArgs(args, new Set(), 0);
2059
+ if (!telemetryUploadEnabled(process.env)) return 0;
2060
+ const timeout = Number(process.env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS || uploadTimeoutMs);
2061
+ const delay = parseNonNegativeInteger(process.env.ALIPAY_AIPAY_TELEMETRY_FLUSH_DELAY_MS);
2062
+ if (delay && delay > 0) sleepSync(Math.min(delay, 30000));
2063
+ await flushOutbox(process.env, {
2064
+ targetFile: process.env.ALIPAY_AIPAY_TELEMETRY_FLUSH_TARGET_FILE || '',
2065
+ timeoutMs: Number.isFinite(timeout) ? timeout : uploadTimeoutMs
2066
+ });
2067
+ return 0;
2068
+ }
2069
+ if (!telemetryEnabled(process.env)) return 0;
2070
+ if (command !== 'stage' || args._.length !== 1) {
2071
+ emitTelemetryError(new Error('invalid telemetry arguments'));
2072
+ return 0;
2073
+ }
2074
+ assertCliArgs(args, new Set([
2075
+ 'run-id',
2076
+ 'history-count'
2077
+ ]), 1);
2078
+ if (typeof args['run-id'] !== 'string' || !args['run-id']) throw new Error('缺少参数 --run-id');
2079
+ if (!Object.prototype.hasOwnProperty.call(args, 'history-count')) throw new Error('缺少参数 --history-count');
2080
+ await recordStageEvent(
2081
+ args._[0],
2082
+ contextFromCliArgs(args),
2083
+ process.env,
2084
+ Date.now(),
2085
+ process.stderr
2086
+ );
2087
+ return 0;
2088
+ } catch (error) {
2089
+ if (['start-conversation', 'conversation-message', 'end-conversation', 'opt-out'].includes(argv[2])) return 0;
2090
+ emitTelemetryError(error);
2091
+ return 0;
2092
+ }
2093
+ }
2094
+
2095
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
2096
+ runTelemetryCli(process.argv).then((code) => {
2097
+ process.exitCode = code;
2098
+ }, (error) => {
2099
+ emitTelemetryError(error);
2100
+ process.exitCode = 0;
2101
+ });
2102
+ }