sophhub 0.4.19 → 0.4.21

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 (633) hide show
  1. package/README.md +199 -187
  2. package/agents/ai-cs-admin/.config.json +51 -51
  3. package/agents/ai-cs-admin/AGENTS.md +293 -293
  4. package/agents/ai-cs-admin/HEARTBEAT.md +18 -18
  5. package/agents/ai-cs-qa/.config.json +47 -47
  6. package/agents/ai-cs-qa/BOOTSTRAP.md +22 -22
  7. package/agents/ai-cs-qa/scripts/setup_links.sh +39 -39
  8. package/agents/beauty/.config.json +17 -17
  9. package/agents/beauty/AGENTS.md +234 -234
  10. package/agents/beauty/BOOTSTRAP.md +55 -55
  11. package/agents/beauty/HEARTBEAT.md +5 -5
  12. package/agents/beauty/IDENTITY.md +5 -5
  13. package/agents/beauty/MEMORY.md +44 -44
  14. package/agents/beauty/SOUL.md +64 -64
  15. package/agents/beauty/TOOLS.md +160 -160
  16. package/agents/beauty/USER.md +114 -114
  17. package/agents/intern-admin/.config.json +60 -60
  18. package/agents/intern-admin/AGENTS.md +267 -267
  19. package/agents/intern-admin/BOOTSTRAP.md +21 -21
  20. package/agents/intern-admin/HEARTBEAT.md +3 -3
  21. package/agents/intern-admin/IDENTITY.md +6 -6
  22. package/agents/intern-admin/MEMORY.md +21 -21
  23. package/agents/intern-admin/SOUL.md +23 -23
  24. package/agents/intern-admin/TOOLS.md +93 -93
  25. package/agents/intern-admin/USER.md +16 -16
  26. package/agents/intern-admin/scripts/init_workspace.sh +27 -27
  27. package/agents/intern-qa/.config.json +46 -46
  28. package/agents/intern-qa/AGENTS.md +303 -303
  29. package/agents/intern-qa/BOOTSTRAP.md +16 -16
  30. package/agents/intern-qa/HEARTBEAT.md +3 -3
  31. package/agents/intern-qa/IDENTITY.md +6 -6
  32. package/agents/intern-qa/MEMORY.md +22 -22
  33. package/agents/intern-qa/SOUL.md +24 -24
  34. package/agents/intern-qa/TOOLS.md +24 -24
  35. package/agents/intern-qa/USER.md +27 -27
  36. package/agents/intern-qa/scripts/setup_links.sh +54 -54
  37. package/agents/parent-toddler/.config.json +37 -37
  38. package/agents/parent-toddler/AGENTS.md +51 -51
  39. package/agents/parent-toddler/BOOTSTRAP.md +55 -55
  40. package/agents/parent-toddler/HEARTBEAT.md +5 -5
  41. package/agents/parent-toddler/IDENTITY.md +5 -5
  42. package/agents/parent-toddler/MEMORY.md +22 -22
  43. package/agents/parent-toddler/SOUL.md +35 -35
  44. package/agents/parent-toddler/TOOLS.md +31 -31
  45. package/agents/parent-toddler/USER.md +44 -44
  46. package/agents/vip-admin/.config.json +51 -51
  47. package/agents/vip-admin/AGENTS.md +314 -314
  48. package/agents/vip-admin/BOOTSTRAP.md +21 -21
  49. package/agents/vip-admin/HEARTBEAT.md +19 -19
  50. package/agents/vip-admin/IDENTITY.md +6 -6
  51. package/agents/vip-admin/MEMORY.md +30 -30
  52. package/agents/vip-admin/SOUL.md +25 -25
  53. package/agents/vip-admin/TOOLS.md +108 -108
  54. package/agents/vip-admin/USER.md +31 -31
  55. package/agents/vip-qa/.config.json +58 -58
  56. package/agents/vip-qa/AGENTS.md +319 -319
  57. package/agents/vip-qa/BOOTSTRAP.md +73 -73
  58. package/agents/vip-qa/HEARTBEAT.md +23 -23
  59. package/agents/vip-qa/IDENTITY.md +7 -7
  60. package/agents/vip-qa/MEMORY.md +23 -23
  61. package/agents/vip-qa/SOUL.md +34 -34
  62. package/agents/vip-qa/TOOLS.md +41 -41
  63. package/agents/vip-qa/USER.md +16 -16
  64. package/agents/vip-qa/scripts/setup_links.sh +39 -39
  65. package/bin/sophhub.js +25 -25
  66. package/package.json +35 -33
  67. package/skills/agent-install/skill.json +34 -34
  68. package/skills/agent-install/src/SKILL.md +240 -240
  69. package/skills/agent-install/src/pyproject.toml +6 -6
  70. package/skills/agent-install/src/scripts/backup_agent.py +120 -120
  71. package/skills/agent-install/src/scripts/check_installed.py +479 -479
  72. package/skills/agent-install/src/scripts/common.py +568 -568
  73. package/skills/agent-install/src/scripts/copy_agent_files.py +59 -59
  74. package/skills/agent-install/src/scripts/list_agents.py +285 -285
  75. package/skills/agent-install/src/scripts/resolve_install_params.py +90 -90
  76. package/skills/agent-install/src/scripts/update_agent_md.py +76 -76
  77. package/skills/agent-install/src/scripts/update_openclaw.py +193 -193
  78. package/skills/agent-install/src/scripts/verify_download.py +148 -148
  79. package/skills/aippt/skill.json +20 -20
  80. package/skills/aippt/src/SKILL.md +235 -235
  81. package/skills/aippt/src/pyproject.toml +8 -8
  82. package/skills/aippt/src/scripts/auth.py +122 -122
  83. package/skills/aippt/src/scripts/ppt.py +361 -361
  84. package/skills/aippt/src/scripts/provider_docmee.py +299 -299
  85. package/skills/beauty-salon-inventory/skill.json +16 -16
  86. package/skills/beauty-salon-inventory/src/SKILL.md +69 -69
  87. package/skills/beauty-salon-inventory/src/scripts/init_salon_inventory.py +39 -39
  88. package/skills/beauty-salon-inventory/src/scripts/init_salon_inventory.sh +4 -4
  89. package/skills/beauty-salon-inventory/src/scripts/salon_inventory_cli.py +244 -244
  90. package/skills/beauty-salon-marketing/skill.json +10 -10
  91. package/skills/beauty-salon-marketing/src/SKILL.md +36 -36
  92. package/skills/beauty-salon-marketing/src/playbooks/beauty-salon-festival.md +19 -19
  93. package/skills/beauty-salon-marketing/src/playbooks/beauty-salon-segment.md +18 -18
  94. package/skills/beauty-salon-marketing/src/scripts/beauty_marketing_cli.py +99 -99
  95. package/skills/beauty-salon-marketing/src/scripts/member_segment.py +114 -114
  96. package/skills/beauty-salon-member-appointment/skill.json +10 -10
  97. package/skills/beauty-salon-member-appointment/src/SKILL.md +36 -36
  98. package/skills/beauty-salon-member-appointment/src/pyproject.toml +9 -9
  99. package/skills/beauty-salon-member-appointment/src/scripts/run_e2e_smoke.py +160 -160
  100. package/skills/beauty-salon-member-appointment/src/src/member_appt_cli/__init__.py +1 -1
  101. package/skills/beauty-salon-member-appointment/src/src/member_appt_cli/__main__.py +4 -4
  102. package/skills/beauty-salon-member-appointment/src/src/member_appt_cli/cli.py +921 -921
  103. package/skills/beauty-salon-member-appointment/src/src/member_appt_cli/db.py +30 -30
  104. package/skills/beauty-salon-membership/skill.json +20 -20
  105. package/skills/beauty-salon-membership/src/SKILL.md +67 -67
  106. package/skills/beauty-salon-product-service/skill.json +12 -12
  107. package/skills/beauty-salon-product-service/src/SKILL.md +42 -42
  108. package/skills/beauty-salon-product-service/src/pyproject.toml +9 -9
  109. package/skills/beauty-salon-product-service/src/src/product_service_cli/__init__.py +1 -1
  110. package/skills/beauty-salon-product-service/src/src/product_service_cli/__main__.py +4 -4
  111. package/skills/beauty-salon-product-service/src/src/product_service_cli/cli.py +329 -329
  112. package/skills/beauty-salon-product-service/src/src/product_service_cli/db.py +29 -29
  113. package/skills/beauty-salon-staff/skill.json +10 -10
  114. package/skills/beauty-salon-staff/src/SKILL.md +37 -37
  115. package/skills/beauty-salon-staff/src/pyproject.toml +9 -9
  116. package/skills/beauty-salon-staff/src/src/staff_cli/__init__.py +1 -1
  117. package/skills/beauty-salon-staff/src/src/staff_cli/__main__.py +4 -4
  118. package/skills/beauty-salon-staff/src/src/staff_cli/cli.py +479 -479
  119. package/skills/beauty-salon-staff/src/src/staff_cli/db.py +28 -28
  120. package/skills/beauty-salon-suite/skill.json +13 -13
  121. package/skills/beauty-salon-suite/src/SKILL.md +18 -18
  122. package/skills/beauty-salon-suite/src/beauty_db/__init__.py +2 -2
  123. package/skills/beauty-salon-suite/src/beauty_db/db.py +249 -249
  124. package/skills/beauty-salon-traffic/skill.json +20 -20
  125. package/skills/beauty-salon-traffic/src/SKILL.md +84 -84
  126. package/skills/bing-image-search/skill.json +20 -20
  127. package/skills/bing-image-search/src/SKILL.md +105 -105
  128. package/skills/bot-api-status/skill.json +44 -44
  129. package/skills/bot-api-status/src/SKILL.md +99 -99
  130. package/skills/bot-api-status/src/pyproject.toml +5 -5
  131. package/skills/bot-api-status/src/scripts/secret.py +496 -496
  132. package/skills/bot-secret/skill.json +35 -35
  133. package/skills/bot-secret/src/SKILL.md +51 -51
  134. package/skills/bot-secret/src/pyproject.toml +5 -5
  135. package/skills/bot-secret/src/scripts/secret.py +120 -120
  136. package/skills/cake-flower-holiday-campaign/skill.json +20 -20
  137. package/skills/cake-flower-holiday-campaign/src/SKILL.md +68 -68
  138. package/skills/cake-flower-order-sop/skill.json +20 -20
  139. package/skills/cake-flower-order-sop/src/SKILL.md +65 -65
  140. package/skills/claw-agent-get-send/skill.json +32 -32
  141. package/skills/claw-agent-get-send/src/SKILL.md +43 -43
  142. package/skills/claw-agent-get-send/src/pyproject.toml +5 -5
  143. package/skills/claw-agent-get-send/src/scripts/appia_claw.py +379 -379
  144. package/skills/compact-context/skill.json +20 -20
  145. package/skills/compact-context/src/SKILL.md +133 -133
  146. package/skills/compact-context/src/scripts/check.sh +381 -381
  147. package/skills/compact-context/src/scripts/set-keep-recent.mjs +1337 -1337
  148. package/skills/compact-context/src/scripts/setup.sh +96 -96
  149. package/skills/consensus/skill.json +20 -20
  150. package/skills/consensus/src/SKILL.md +93 -93
  151. package/skills/deepwiki/skill.json +20 -20
  152. package/skills/deepwiki/src/SKILL.md +45 -45
  153. package/skills/deepwiki/src/_meta.json +5 -5
  154. package/skills/deepwiki/src/scripts/deepwiki.js +135 -135
  155. package/skills/didi-ride/skill.json +20 -20
  156. package/skills/didi-ride/src/SKILL.md +309 -309
  157. package/skills/didi-ride/src/_meta.json +5 -5
  158. package/skills/didi-ride/src/assets/PREFERENCE.md +58 -58
  159. package/skills/didi-ride/src/package.json +15 -15
  160. package/skills/didi-ride/src/references/api_references.md +171 -171
  161. package/skills/didi-ride/src/references/error_handling.md +68 -68
  162. package/skills/didi-ride/src/references/setup.md +73 -73
  163. package/skills/didi-ride/src/references/workflow.md +150 -150
  164. package/skills/feishu-bitable/skill.json +20 -20
  165. package/skills/feishu-bitable/src/CHECKLIST.md +149 -149
  166. package/skills/feishu-bitable/src/README.md +177 -177
  167. package/skills/feishu-bitable/src/SKILL.md +113 -113
  168. package/skills/feishu-bitable/src/_meta.json +5 -5
  169. package/skills/feishu-bitable/src/api.js +380 -380
  170. package/skills/feishu-bitable/src/bin/cli.js +283 -283
  171. package/skills/feishu-bitable/src/description.md +142 -142
  172. package/skills/feishu-bitable/src/examples/create-records.json +51 -51
  173. package/skills/feishu-bitable/src/examples/create-table.json +63 -63
  174. package/skills/feishu-bitable/src/package-lock.json +324 -324
  175. package/skills/feishu-bitable/src/package.json +32 -32
  176. package/skills/feishu-bitable/src/publish-config.json +13 -13
  177. package/skills/feishu-bitable/src/test-simple.js +60 -60
  178. package/skills/feishu-bitable/src/utils.js +260 -260
  179. package/skills/feishu-notes-assistant-universal/skill.json +20 -20
  180. package/skills/feishu-notes-assistant-universal/src/README.md +55 -55
  181. package/skills/feishu-notes-assistant-universal/src/SKILL.md +159 -159
  182. package/skills/feishu-notes-assistant-universal/src/scripts/_resolve_lark_cli.py +58 -58
  183. package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_meeting_minutes.py +462 -462
  184. package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_notes_crud.py +547 -547
  185. package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_notes_crud_test.py +181 -181
  186. package/skills/feishu-notes-assistant-universal/src/scripts/run_meeting_minutes.py +80 -80
  187. package/skills/feishu-notes-assistant-universal/src/scripts/run_meeting_minutes.sh +5 -5
  188. package/skills/feishu-notes-assistant-universal/src/scripts/run_note_crud.py +32 -32
  189. package/skills/feishu-notes-assistant-universal/src/scripts/run_note_crud.sh +5 -5
  190. package/skills/flight-booking/skill.json +36 -36
  191. package/skills/flight-booking/src/SKILL.md +288 -288
  192. package/skills/flight-booking/src/scripts/flight_booking.py +1237 -1237
  193. package/skills/flyai/skill.json +20 -20
  194. package/skills/flyai/src/SKILL.md +119 -119
  195. package/skills/flyai/src/references/fliggy-fast-search.md +53 -53
  196. package/skills/flyai/src/references/search-flight.md +89 -89
  197. package/skills/flyai/src/references/search-hotels.md +57 -57
  198. package/skills/flyai/src/references/search-poi.md +48 -48
  199. package/skills/google-maps/skill.json +20 -20
  200. package/skills/google-maps/src/SKILL.md +237 -237
  201. package/skills/google-maps/src/_meta.json +5 -5
  202. package/skills/google-maps/src/lib/map_helper.py +912 -912
  203. package/skills/image-classify/skill.json +42 -42
  204. package/skills/image-classify/src/SKILL.md +368 -368
  205. package/skills/image-classify/src/references/config.json +4 -4
  206. package/skills/image-classify/src/scripts/face_search.py +1276 -1276
  207. package/skills/image-description/skill.json +34 -34
  208. package/skills/image-description/src/SKILL.md +33 -33
  209. package/skills/image-description/src/pyproject.toml +8 -8
  210. package/skills/image-description/src/scripts/ana_image.py +112 -112
  211. package/skills/image-identify-world/skill.json +20 -20
  212. package/skills/image-identify-world/src/SKILL.md +40 -40
  213. package/skills/image-identify-world/src/pyproject.toml +8 -8
  214. package/skills/image-identify-world/src/scripts/identify_world.py +115 -115
  215. package/skills/insurance-policy-review/skill.json +27 -27
  216. package/skills/insurance-policy-review/src/SKILL.md +75 -75
  217. package/skills/insurance-sales-playbook/skill.json +20 -20
  218. package/skills/insurance-sales-playbook/src/SKILL.md +58 -58
  219. package/skills/inventory-management/skill.json +20 -20
  220. package/skills/inventory-management/src/SKILL.md +241 -241
  221. package/skills/inventory-management/src/scripts/inventory.py +1844 -1844
  222. package/skills/large-task-router/skill.json +20 -20
  223. package/skills/large-task-router/src/SKILL.md +79 -79
  224. package/skills/large-task-router/src/templates/plan.md +74 -74
  225. package/skills/lawding-contract-review/skill.json +20 -20
  226. package/skills/lawding-contract-review/src/SKILL.md +284 -284
  227. package/skills/lawding-contract-review/src/references/legal-language-library.md +1385 -1385
  228. package/skills/lawding-contract-review/src/scripts/build_reminders.py +471 -471
  229. package/skills/lawding-contract-review/src/scripts/register_contract_cron.py +457 -457
  230. package/skills/md2pdf-converter/skill.json +20 -20
  231. package/skills/md2pdf-converter/src/SKILL.md +244 -244
  232. package/skills/md2pdf-converter/src/_meta.json +5 -5
  233. package/skills/md2pdf-converter/src/scripts/generate_emoji_mapping.py +74 -74
  234. package/skills/md2pdf-converter/src/scripts/md2pdf-local.sh +291 -291
  235. package/skills/notes-hub-assistant/skill.json +20 -20
  236. package/skills/notes-hub-assistant/src/SKILL.md +233 -233
  237. package/skills/notes-hub-assistant/src/scripts/_resolve_lark_cli.py +48 -48
  238. package/skills/notes-hub-assistant/src/scripts/openclaw_meeting_minutes.py +473 -473
  239. package/skills/notes-hub-assistant/src/scripts/openclaw_notes_crud.py +596 -596
  240. package/skills/notes-hub-assistant/src/scripts/openclaw_wolai_notes_crud.py +364 -364
  241. package/skills/notes-hub-assistant/src/scripts/run_meeting_minutes.py +79 -79
  242. package/skills/notes-hub-assistant/src/scripts/run_note_crud.py +37 -37
  243. package/skills/notes-hub-assistant/src/scripts/run_notionbot.py +36 -36
  244. package/skills/notes-hub-assistant/src/scripts/run_wolai_note_crud.py +27 -27
  245. package/skills/schedule-reminder/skill.json +20 -20
  246. package/skills/schedule-reminder/src/SKILL.md +619 -619
  247. package/skills/schedule-reminder/src/schedule_template.md +68 -68
  248. package/skills/schedule-reminder/src/scripts/append_event.py +204 -204
  249. package/skills/schedule-reminder/src/scripts/create_reminders.sh +163 -163
  250. package/skills/schedule-reminder/src/scripts/daily_activate.sh +175 -175
  251. package/skills/schedule-reminder/src/scripts/parse_schedule.py +704 -704
  252. package/skills/schedule-reminder/src/scripts/setup.sh +242 -242
  253. package/skills/schedule-reminder/src//347/224/250/346/210/267/346/214/207/345/215/227.md +311 -311
  254. package/skills/sessions-analysis/skill.json +34 -34
  255. package/skills/sessions-analysis/src/SKILL.md +81 -81
  256. package/skills/sessions-analysis/src/pyproject.toml +5 -5
  257. package/skills/sessions-analysis/src/scripts/ana_logs.py +205 -205
  258. package/skills/share-skill/skill.json +20 -20
  259. package/skills/share-skill/src/SKILL.md +261 -261
  260. package/skills/share-skill/src/scripts/share_skill_to_friend.py +1031 -1031
  261. package/skills/skill-creator/skill.json +20 -20
  262. package/skills/skill-creator/src/SKILL.md +370 -370
  263. package/skills/skill-creator/src/license.txt +202 -202
  264. package/skills/skill-creator/src/scripts/init_skill.py +378 -378
  265. package/skills/skill-creator/src/scripts/package_skill.py +111 -111
  266. package/skills/skill-creator/src/scripts/quick_validate.py +101 -101
  267. package/skills/skillhub/skill.json +27 -27
  268. package/skills/skillhub/src/SKILL.md +121 -121
  269. package/skills/sophnet-age-appearance/skill.json +20 -20
  270. package/skills/sophnet-age-appearance/src/SKILL.md +83 -83
  271. package/skills/sophnet-age-appearance/src/pyproject.toml +10 -10
  272. package/skills/sophnet-age-appearance/src/scripts/age_appearance.py +395 -395
  273. package/skills/sophnet-age-appearance/src/scripts/age_face_crop.py +313 -313
  274. package/skills/sophnet-bot-client/skill.json +20 -20
  275. package/skills/sophnet-bot-client/src/SKILL.md +255 -255
  276. package/skills/sophnet-bot-client/src/pyproject.toml +13 -13
  277. package/skills/sophnet-bot-client/src/scripts/bot_client_proxy.py +165 -165
  278. package/skills/sophnet-bot-client/src/scripts/bot_client_safe.sh +29 -29
  279. package/skills/sophnet-bot-client/src/scripts/bot_client_setup.py +502 -502
  280. package/skills/sophnet-bot-client/src/tests/test_bot_client_proxy.py +255 -255
  281. package/skills/sophnet-bot-client/src/tests/test_bot_client_setup.py +679 -679
  282. package/skills/sophnet-bot-client/src/uv.lock +8 -8
  283. package/skills/sophnet-customer-management/skill.json +20 -20
  284. package/skills/sophnet-customer-management/src/SKILL.md +270 -270
  285. package/skills/sophnet-customer-management/src/pyproject.toml +15 -15
  286. package/skills/sophnet-customer-management/src/src/customer_mgmt_cli/__init__.py +2 -2
  287. package/skills/sophnet-customer-management/src/src/customer_mgmt_cli/__main__.py +5 -5
  288. package/skills/sophnet-customer-management/src/src/customer_mgmt_cli/cli.py +67 -67
  289. package/skills/sophnet-customer-management/src/src/customer_mgmt_cli/commands/__init__.py +2 -2
  290. package/skills/sophnet-customer-management/src/src/customer_mgmt_cli/commands/customer.py +60 -60
  291. package/skills/sophnet-customer-management/src/src/customer_mgmt_cli/commands/export_file.py +18 -18
  292. package/skills/sophnet-customer-management/src/src/customer_mgmt_cli/commands/import_file.py +15 -15
  293. package/skills/sophnet-customer-management/src/src/customer_mgmt_cli/commands/reminder.py +26 -26
  294. package/skills/sophnet-customer-management/src/src/customer_mgmt_cli/commands/schema.py +28 -28
  295. package/skills/sophnet-customer-management/src/src/customer_mgmt_cli/config.py +54 -54
  296. package/skills/sophnet-customer-management/src/src/customer_mgmt_core/__init__.py +2 -2
  297. package/skills/sophnet-customer-management/src/src/customer_mgmt_core/exporter.py +85 -85
  298. package/skills/sophnet-customer-management/src/src/customer_mgmt_core/models.py +84 -84
  299. package/skills/sophnet-customer-management/src/src/customer_mgmt_core/normalizer.py +144 -144
  300. package/skills/sophnet-customer-management/src/src/customer_mgmt_core/parser.py +241 -241
  301. package/skills/sophnet-customer-management/src/src/customer_mgmt_core/query.py +109 -109
  302. package/skills/sophnet-customer-management/src/src/customer_mgmt_core/reminder.py +121 -121
  303. package/skills/sophnet-customer-management/src/src/customer_mgmt_core/repository.py +397 -397
  304. package/skills/sophnet-customer-management/src/src/customer_mgmt_core/schema.py +106 -106
  305. package/skills/sophnet-customer-management/src/src/customer_mgmt_core/service.py +565 -565
  306. package/skills/sophnet-customer-management/src/uv.lock +48 -48
  307. package/skills/sophnet-customized-marketing/skill.json +28 -28
  308. package/skills/sophnet-customized-marketing/src/SKILL.md +144 -144
  309. package/skills/sophnet-customized-marketing/src/playbooks/campaign-planning.md +187 -187
  310. package/skills/sophnet-customized-marketing/src/playbooks/content-generation.md +124 -124
  311. package/skills/sophnet-customized-marketing/src/playbooks/marketing-calendar.md +59 -59
  312. package/skills/sophnet-customized-marketing/src/playbooks/multi-channel-bundle.md +94 -94
  313. package/skills/sophnet-customized-marketing/src/playbooks/poster-generation.md +182 -182
  314. package/skills/sophnet-customized-marketing/src/playbooks/style-profile-workflow.md +103 -103
  315. package/skills/sophnet-customized-marketing/src/pyproject.toml +8 -8
  316. package/skills/sophnet-customized-marketing/src/references/campaign-mechanics.md +168 -168
  317. package/skills/sophnet-customized-marketing/src/references/content-safety.md +26 -26
  318. package/skills/sophnet-customized-marketing/src/references/marketing-date-checklist.md +99 -99
  319. package/skills/sophnet-customized-marketing/src/references/platform-writing-guidelines.md +88 -88
  320. package/skills/sophnet-customized-marketing/src/references/quality-checklist.md +44 -44
  321. package/skills/sophnet-customized-marketing/src/scripts/generate_poster.py +572 -572
  322. package/skills/sophnet-customized-marketing/src/scripts/style_profile.py +215 -215
  323. package/skills/sophnet-dailynews/skill.json +20 -20
  324. package/skills/sophnet-dailynews/src/SKILL.md +179 -179
  325. package/skills/sophnet-dailynews/src/cache.json +150 -150
  326. package/skills/sophnet-dailynews/src/sources.json +230 -230
  327. package/skills/sophnet-docx/skill.json +20 -20
  328. package/skills/sophnet-docx/src/SKILL.md +463 -463
  329. package/skills/sophnet-docx/src/package-lock.json +208 -208
  330. package/skills/sophnet-docx/src/package.json +16 -16
  331. package/skills/sophnet-docx/src/pyproject.toml +11 -11
  332. package/skills/sophnet-docx/src/scripts/__init__.py +1 -1
  333. package/skills/sophnet-docx/src/scripts/accept_changes.py +135 -135
  334. package/skills/sophnet-docx/src/scripts/comment.py +318 -318
  335. package/skills/sophnet-docx/src/scripts/ensure_uv_env.sh +68 -68
  336. package/skills/sophnet-docx/src/scripts/office/helpers/merge_runs.py +199 -199
  337. package/skills/sophnet-docx/src/scripts/office/helpers/simplify_redlines.py +197 -197
  338. package/skills/sophnet-docx/src/scripts/office/pack.py +159 -159
  339. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -1499
  340. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -146
  341. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -1085
  342. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -11
  343. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -3081
  344. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -23
  345. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -185
  346. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -287
  347. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -1676
  348. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -28
  349. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -144
  350. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -174
  351. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -25
  352. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -18
  353. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -59
  354. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -56
  355. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -195
  356. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -582
  357. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -25
  358. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -4439
  359. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -570
  360. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -509
  361. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -12
  362. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -108
  363. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -96
  364. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -3646
  365. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -116
  366. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -42
  367. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -50
  368. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -49
  369. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -33
  370. package/skills/sophnet-docx/src/scripts/office/schemas/mce/mc.xsd +75 -75
  371. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -560
  372. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -67
  373. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -14
  374. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -20
  375. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -13
  376. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -4
  377. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -8
  378. package/skills/sophnet-docx/src/scripts/office/soffice.py +183 -183
  379. package/skills/sophnet-docx/src/scripts/office/unpack.py +132 -132
  380. package/skills/sophnet-docx/src/scripts/office/validate.py +111 -111
  381. package/skills/sophnet-docx/src/scripts/office/validators/__init__.py +15 -15
  382. package/skills/sophnet-docx/src/scripts/office/validators/base.py +847 -847
  383. package/skills/sophnet-docx/src/scripts/office/validators/docx.py +446 -446
  384. package/skills/sophnet-docx/src/scripts/office/validators/pptx.py +275 -275
  385. package/skills/sophnet-docx/src/scripts/office/validators/redlining.py +247 -247
  386. package/skills/sophnet-docx/src/scripts/templates/comments.xml +3 -3
  387. package/skills/sophnet-docx/src/scripts/templates/commentsExtended.xml +3 -3
  388. package/skills/sophnet-docx/src/scripts/templates/commentsExtensible.xml +3 -3
  389. package/skills/sophnet-docx/src/scripts/templates/commentsIds.xml +3 -3
  390. package/skills/sophnet-docx/src/scripts/templates/people.xml +3 -3
  391. package/skills/sophnet-docx/src/scripts/upload_file.sh +96 -96
  392. package/skills/sophnet-docx/src/uv.lock +320 -320
  393. package/skills/sophnet-face-search/skill.json +20 -20
  394. package/skills/sophnet-face-search/src/SKILL.md +115 -115
  395. package/skills/sophnet-face-search/src/pyproject.toml +11 -11
  396. package/skills/sophnet-face-search/src/scripts/face_search.py +335 -335
  397. package/skills/sophnet-face-search/src/uv.lock +508 -508
  398. package/skills/sophnet-id-photo/skill.json +20 -20
  399. package/skills/sophnet-id-photo/src/SKILL.md +107 -107
  400. package/skills/sophnet-id-photo/src/pyproject.toml +10 -10
  401. package/skills/sophnet-id-photo/src/scripts/id_photo.py +540 -540
  402. package/skills/sophnet-id-photo/src/scripts/id_photo_compliance.py +215 -215
  403. package/skills/sophnet-id-photo/src/scripts/id_photo_face_crop.py +313 -313
  404. package/skills/sophnet-image-edit/skill.json +20 -20
  405. package/skills/sophnet-image-edit/src/SKILL.md +140 -140
  406. package/skills/sophnet-image-edit/src/pyproject.toml +9 -9
  407. package/skills/sophnet-image-edit/src/scripts/edit_and_preview.sh +68 -68
  408. package/skills/sophnet-image-edit/src/scripts/edit_image.py +279 -279
  409. package/skills/sophnet-image-edit/src/uv.lock +234 -234
  410. package/skills/sophnet-image-generate/skill.json +20 -20
  411. package/skills/sophnet-image-generate/src/SKILL.md +62 -62
  412. package/skills/sophnet-image-generate/src/pyproject.toml +9 -9
  413. package/skills/sophnet-image-generate/src/scripts/generate_image.py +156 -156
  414. package/skills/sophnet-image-generate/src/uv.lock +234 -234
  415. package/skills/sophnet-image-ocr/skill.json +20 -20
  416. package/skills/sophnet-image-ocr/src/SKILL.md +167 -167
  417. package/skills/sophnet-image-ocr/src/pyproject.toml +13 -13
  418. package/skills/sophnet-image-ocr/src/scripts/ocr.py +225 -225
  419. package/skills/sophnet-image-ocr/src/uv.lock +234 -234
  420. package/skills/sophnet-infinite-talk/skill.json +20 -20
  421. package/skills/sophnet-infinite-talk/src/SKILL.md +140 -140
  422. package/skills/sophnet-infinite-talk/src/pyproject.toml +9 -9
  423. package/skills/sophnet-infinite-talk/src/scripts/gen.py +172 -172
  424. package/skills/sophnet-oss/skill.json +27 -27
  425. package/skills/sophnet-oss/src/SKILL.md +118 -118
  426. package/skills/sophnet-oss/src/pyproject.toml +8 -8
  427. package/skills/sophnet-oss/src/scripts/upload_file.py +43 -43
  428. package/skills/sophnet-pdf/skill.json +20 -20
  429. package/skills/sophnet-pdf/src/SKILL.md +413 -413
  430. package/skills/sophnet-pdf/src/forms.md +297 -297
  431. package/skills/sophnet-pdf/src/pyproject.toml +14 -14
  432. package/skills/sophnet-pdf/src/reference.md +611 -611
  433. package/skills/sophnet-pdf/src/scripts/check_bounding_boxes.py +65 -65
  434. package/skills/sophnet-pdf/src/scripts/check_fillable_fields.py +11 -11
  435. package/skills/sophnet-pdf/src/scripts/convert_pdf_to_images.py +33 -33
  436. package/skills/sophnet-pdf/src/scripts/create_validation_image.py +37 -37
  437. package/skills/sophnet-pdf/src/scripts/enhance_tutorial.py +557 -557
  438. package/skills/sophnet-pdf/src/scripts/ensure_uv_env.sh +68 -68
  439. package/skills/sophnet-pdf/src/scripts/extract_form_field_info.py +122 -122
  440. package/skills/sophnet-pdf/src/scripts/extract_form_structure.py +115 -115
  441. package/skills/sophnet-pdf/src/scripts/extract_pdf_content.py +34 -34
  442. package/skills/sophnet-pdf/src/scripts/fill_fillable_fields.py +98 -98
  443. package/skills/sophnet-pdf/src/scripts/fill_pdf_form_with_annotations.py +107 -107
  444. package/skills/sophnet-pdf/src/scripts/upload_file.sh +88 -88
  445. package/skills/sophnet-pdf/src/uv.lock +537 -537
  446. package/skills/sophnet-qa-install/skill.json +27 -27
  447. package/skills/sophnet-qa-install/src/SKILL.md +210 -210
  448. package/skills/sophnet-qa-install/src/pyproject.toml +6 -6
  449. package/skills/sophnet-qa-install/src/scripts/backup_md.py +35 -35
  450. package/skills/sophnet-qa-install/src/scripts/check_installed.py +143 -143
  451. package/skills/sophnet-qa-install/src/scripts/update_config.py +142 -142
  452. package/skills/sophnet-qa-install/src/scripts/update_md.py +73 -73
  453. package/skills/sophnet-schedule/skill.json +20 -20
  454. package/skills/sophnet-schedule/src/ARCHITECTURE.md +321 -321
  455. package/skills/sophnet-schedule/src/IMPROVEMENTS.md +145 -145
  456. package/skills/sophnet-schedule/src/SKILL.md +1050 -1050
  457. package/skills/sophnet-schedule/src/_meta.json +6 -6
  458. package/skills/sophnet-schedule/src/api/models.py +245 -245
  459. package/skills/sophnet-schedule/src/apps/add_event.py +237 -237
  460. package/skills/sophnet-schedule/src/apps/check_reminders.py +112 -112
  461. package/skills/sophnet-schedule/src/apps/check_roc.py +246 -246
  462. package/skills/sophnet-schedule/src/apps/generate_daily_plan.py +342 -342
  463. package/skills/sophnet-schedule/src/apps/import_events.py +216 -216
  464. package/skills/sophnet-schedule/src/apps/monitor_calendar_changes.py +140 -140
  465. package/skills/sophnet-schedule/src/apps/register_tasks.py +169 -169
  466. package/skills/sophnet-schedule/src/apps/sync_roc_to_gcal.py +174 -174
  467. package/skills/sophnet-schedule/src/compat.py +66 -66
  468. package/skills/sophnet-schedule/src/config/reminder_rules.yaml +96 -96
  469. package/skills/sophnet-schedule/src/config/roc_events.yaml +44 -44
  470. package/skills/sophnet-schedule/src/config/settings.py +133 -133
  471. package/skills/sophnet-schedule/src/config/task_registry.yaml +92 -92
  472. package/skills/sophnet-schedule/src/docs/FRONTEND_INTEGRATION_GUIDE.md +437 -437
  473. package/skills/sophnet-schedule/src/gcal/client.py +374 -374
  474. package/skills/sophnet-schedule/src/gcal/models.py +91 -91
  475. package/skills/sophnet-schedule/src/requirements.txt +6 -6
  476. package/skills/sophnet-schedule/src/scripts/setup_gcal_token.py +85 -85
  477. package/skills/sophnet-schedule/src/server.py +669 -669
  478. package/skills/sophnet-schedule/src/services/calendar_backend.py +139 -139
  479. package/skills/sophnet-schedule/src/services/conflict_detector.py +96 -96
  480. package/skills/sophnet-schedule/src/services/datetime_utils.py +117 -117
  481. package/skills/sophnet-schedule/src/services/event_classifier.py +100 -100
  482. package/skills/sophnet-schedule/src/services/event_diff.py +160 -160
  483. package/skills/sophnet-schedule/src/services/google_integration.py +500 -500
  484. package/skills/sophnet-schedule/src/services/job_store.py +100 -100
  485. package/skills/sophnet-schedule/src/services/local_event_store.py +266 -266
  486. package/skills/sophnet-schedule/src/services/reminder_planner.py +116 -116
  487. package/skills/sophnet-schedule/src/services/runtime_utils.py +31 -31
  488. package/skills/sophnet-schedule/src/services/table_parser.py +286 -286
  489. package/skills/sophnet-schedule/src/services/task_builder.py +167 -167
  490. package/skills/sophnet-schedule/src/services/time_window.py +72 -72
  491. package/skills/sophnet-sticker-edit/skill.json +27 -27
  492. package/skills/sophnet-sticker-edit/src/SKILL.md +80 -80
  493. package/skills/sophnet-sticker-edit/src/pyproject.toml +9 -9
  494. package/skills/sophnet-sticker-edit/src/scripts/edit_sticker_image.py +403 -403
  495. package/skills/sophnet-stock/skill.json +20 -20
  496. package/skills/sophnet-stock/src/App-Plan.md +442 -442
  497. package/skills/sophnet-stock/src/README.md +214 -214
  498. package/skills/sophnet-stock/src/SKILL.md +236 -236
  499. package/skills/sophnet-stock/src/TODO.md +394 -394
  500. package/skills/sophnet-stock/src/_meta.json +5 -5
  501. package/skills/sophnet-stock/src/docs/ARCHITECTURE.md +408 -408
  502. package/skills/sophnet-stock/src/docs/CONCEPT.md +233 -233
  503. package/skills/sophnet-stock/src/docs/HOT_SCANNER.md +288 -288
  504. package/skills/sophnet-stock/src/docs/README.md +95 -95
  505. package/skills/sophnet-stock/src/docs/USAGE.md +465 -465
  506. package/skills/sophnet-stock/src/scripts/analyze_stock.py +2565 -2565
  507. package/skills/sophnet-stock/src/scripts/dividends.py +365 -365
  508. package/skills/sophnet-stock/src/scripts/hot_scanner.py +582 -582
  509. package/skills/sophnet-stock/src/scripts/portfolio.py +548 -548
  510. package/skills/sophnet-stock/src/scripts/rumor_scanner.py +342 -342
  511. package/skills/sophnet-stock/src/scripts/test_stock_analysis.py +409 -409
  512. package/skills/sophnet-stock/src/scripts/watchlist.py +336 -336
  513. package/skills/sophnet-training-install/skill.json +27 -27
  514. package/skills/sophnet-training-install/src/SKILL.md +211 -211
  515. package/skills/sophnet-training-install/src/pyproject.toml +6 -6
  516. package/skills/sophnet-training-install/src/scripts/backup_md.py +35 -35
  517. package/skills/sophnet-training-install/src/scripts/check_installed.py +144 -144
  518. package/skills/sophnet-training-install/src/scripts/update_config.py +142 -142
  519. package/skills/sophnet-training-install/src/scripts/update_md.py +73 -73
  520. package/skills/sophnet-tts/skill.json +20 -20
  521. package/skills/sophnet-tts/src/SKILL.md +79 -79
  522. package/skills/sophnet-tts/src/pyproject.toml +9 -9
  523. package/skills/sophnet-tts/src/scripts/gen_tts.py +130 -130
  524. package/skills/sophnet-video-generate/skill.json +37 -37
  525. package/skills/sophnet-video-generate/src/SKILL.md +117 -117
  526. package/skills/sophnet-video-generate/src/scripts/gen_video.py +321 -321
  527. package/skills/sophnet-xlsx/skill.json +20 -20
  528. package/skills/sophnet-xlsx/src/SKILL.md +399 -399
  529. package/skills/sophnet-xlsx/src/pyproject.toml +11 -11
  530. package/skills/sophnet-xlsx/src/scripts/ensure_uv_env.sh +68 -68
  531. package/skills/sophnet-xlsx/src/scripts/office/helpers/merge_runs.py +199 -199
  532. package/skills/sophnet-xlsx/src/scripts/office/helpers/simplify_redlines.py +197 -197
  533. package/skills/sophnet-xlsx/src/scripts/office/pack.py +159 -159
  534. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -1499
  535. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -146
  536. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -1085
  537. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -11
  538. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -3081
  539. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -23
  540. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -185
  541. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -287
  542. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -1676
  543. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -28
  544. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -144
  545. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -174
  546. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -25
  547. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -18
  548. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -59
  549. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -56
  550. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -195
  551. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -582
  552. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -25
  553. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -4439
  554. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -570
  555. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -509
  556. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -12
  557. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -108
  558. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -96
  559. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -3646
  560. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -116
  561. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -42
  562. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -50
  563. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -49
  564. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -33
  565. package/skills/sophnet-xlsx/src/scripts/office/schemas/mce/mc.xsd +75 -75
  566. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -560
  567. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -67
  568. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -14
  569. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -20
  570. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -13
  571. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -4
  572. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -8
  573. package/skills/sophnet-xlsx/src/scripts/office/soffice.py +183 -183
  574. package/skills/sophnet-xlsx/src/scripts/office/unpack.py +132 -132
  575. package/skills/sophnet-xlsx/src/scripts/office/validate.py +111 -111
  576. package/skills/sophnet-xlsx/src/scripts/office/validators/__init__.py +15 -15
  577. package/skills/sophnet-xlsx/src/scripts/office/validators/base.py +847 -847
  578. package/skills/sophnet-xlsx/src/scripts/office/validators/docx.py +446 -446
  579. package/skills/sophnet-xlsx/src/scripts/office/validators/pptx.py +275 -275
  580. package/skills/sophnet-xlsx/src/scripts/office/validators/redlining.py +247 -247
  581. package/skills/sophnet-xlsx/src/scripts/recalc.py +184 -184
  582. package/skills/sophnet-xlsx/src/scripts/upload_file.sh +96 -96
  583. package/skills/sophnet-xlsx/src/uv.lock +319 -319
  584. package/skills/ui-ux-pro-max/skill.json +20 -20
  585. package/skills/ui-ux-pro-max/src/SKILL.md +377 -377
  586. package/skills/ui-ux-pro-max/src/data/icons.csv +101 -101
  587. package/skills/ui-ux-pro-max/src/data/react-performance.csv +45 -45
  588. package/skills/ui-ux-pro-max/src/data/stacks/astro.csv +54 -54
  589. package/skills/ui-ux-pro-max/src/data/stacks/jetpack-compose.csv +53 -53
  590. package/skills/ui-ux-pro-max/src/data/stacks/nuxt-ui.csv +51 -51
  591. package/skills/ui-ux-pro-max/src/data/stacks/nuxtjs.csv +59 -59
  592. package/skills/ui-ux-pro-max/src/data/stacks/shadcn.csv +61 -61
  593. package/skills/ui-ux-pro-max/src/data/typography.csv +57 -57
  594. package/skills/ui-ux-pro-max/src/data/ui-reasoning.csv +101 -101
  595. package/skills/ui-ux-pro-max/src/data/web-interface.csv +31 -31
  596. package/skills/ui-ux-pro-max/src/scripts/core.py +253 -253
  597. package/skills/ui-ux-pro-max/src/scripts/design_system.py +1067 -1067
  598. package/skills/video-understand/skill.json +20 -20
  599. package/skills/video-understand/src/SKILL.md +79 -79
  600. package/skills/video-understand/src/scripts/video_understand.py +204 -204
  601. package/skills/weather/skill.json +19 -19
  602. package/skills/weather/src/SKILL.md +112 -112
  603. package/skills/web-scraper/skill.json +20 -20
  604. package/skills/web-scraper/src/SKILL.md +101 -101
  605. package/skills/web-scraper/src/scripts/scrape.py +270 -270
  606. package/skills/website-builder/skill.json +20 -20
  607. package/skills/website-builder/src/SKILL.md +266 -266
  608. package/skills/website-builder/src/scripts/deploy_site.sh +46 -46
  609. package/skills/wechat-article-publisher/skill.json +20 -20
  610. package/skills/wechat-article-publisher/src/SKILL.md +60 -60
  611. package/skills/wechat-article-publisher/src/config.json +6 -6
  612. package/skills/wechat-article-publisher/src/pyproject.toml +12 -12
  613. package/skills/wechat-article-publisher/src/scripts/publish_wechat.py +825 -825
  614. package/skills/xiaohongshu/skill.json +20 -20
  615. package/skills/xiaohongshu/src/SKILL.md +91 -91
  616. package/skills/xiaohongshu/src/_meta.json +5 -5
  617. package/skills/xiaohongshu/src/assets/card.html +216 -216
  618. package/skills/xiaohongshu/src/assets/cover.html +82 -82
  619. package/skills/xiaohongshu/src/assets/example.md +84 -84
  620. package/skills/xiaohongshu/src/assets/styles.css +318 -318
  621. package/skills/xiaohongshu/src/scripts/render_xhs_v2.py +737 -737
  622. package/skills/xiaohongshu/src/scripts/sign_server.py +158 -158
  623. package/skills/xiaohongshu/src/scripts/stealth.min.js +6 -6
  624. package/skills/xiaohongshu/src/scripts/xhs_tool.py +186 -186
  625. package/skills/xiaohongshu/src/workflow.py +185 -185
  626. package/src/commands/agent.js +112 -112
  627. package/src/commands/download.js +101 -101
  628. package/src/commands/info.js +58 -58
  629. package/src/commands/list.js +71 -71
  630. package/src/utils/agents.js +36 -36
  631. package/src/utils/config.js +22 -22
  632. package/src/utils/paths.js +31 -31
  633. package/src/utils/versions.js +57 -57
@@ -1,1237 +1,1237 @@
1
- #!/usr/bin/env python3
2
- """
3
- 机票查询与下单脚本:支持航班查询(返回全部航班,不截断)、创建订单、支付校验与出票、订单状态查询、取消订单、退票。
4
- 乘机人信息从环境变量读取,用户需提前配置。
5
- """
6
-
7
- import argparse
8
- import json
9
- import os
10
- import sys
11
- import time
12
-
13
- import requests
14
-
15
- # API 配置(新版网关,Bearer API_KEY 鉴权)
16
-
17
- GATEWAY_BASE_URL = "https://www.sophnet.com/api"
18
-
19
- TICKET_BASE = f"{GATEWAY_BASE_URL}/open-apis/ticket"
20
- def _resolve_api_key() -> str:
21
- key = os.environ.get("SOPH_API_KEY", "").strip()
22
- if key:
23
- return key
24
- openclaw_path = os.path.expanduser("~/.openclaw/openclaw.json")
25
- try:
26
- with open(openclaw_path, "r", encoding="utf-8") as f:
27
- cfg = json.load(f)
28
- key = cfg.get("models", {}).get("providers", {}).get("sophnet", {}).get("apiKey", "")
29
- if key:
30
- return key
31
- except (OSError, json.JSONDecodeError):
32
- pass
33
- raise RuntimeError("未找到 SOPH_API_KEY:请设置环境变量 SOPH_API_KEY ")
34
-
35
- API_KEY = _resolve_api_key()
36
-
37
- # 乘机人信息从环境变量读取(用户需提前设置)
38
- ENV_PASSENGER_NAME = "PASSENGER_NAME"
39
- ENV_PASSENGER_MOBILE = "PASSENGER_MOBILE"
40
- ENV_PASSENGER_CREDENTIAL_NO = "PASSENGER_CREDENTIAL_NO"
41
- ENV_PASSENGER_GENDER = "PASSENGER_GENDER"
42
-
43
- # 最近一次航班查询结果保存路径(用于 create-order 仅传航班号时读取)
44
- SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
45
- DATA_DIR = os.path.join(os.path.expanduser("~"), ".openclaw", "flight-booking")
46
- os.makedirs(DATA_DIR, exist_ok=True)
47
- LAST_SEARCH_FILE = os.path.join(DATA_DIR, ".last_search.json")
48
- ORDERS_CACHE_FILE = os.path.join(DATA_DIR, ".orders_cache.json")
49
- PASSENGERS_FILE = os.path.join(DATA_DIR, ".passengers.json")
50
-
51
- # ---------- 基础 API 工具 ----------
52
-
53
- def _headers() -> dict:
54
- return {
55
- "Content-Type": "application/json",
56
- "Authorization": f"Bearer {API_KEY}",
57
- }
58
-
59
-
60
- def _extract_ota(body: dict) -> dict:
61
- """从网关响应 {status, message, result} 中提取 OTA 层 {code, msg, data}"""
62
- result = body.get("result")
63
- if result is None:
64
- return {"code": str(body.get("status", "-1")), "msg": body.get("message", "unknown")}
65
- return result
66
-
67
-
68
- def _post(path: str, data: dict | None = None) -> dict:
69
- url = f"{TICKET_BASE}{path}"
70
- resp = requests.post(url, headers=_headers(), json=data or {}, timeout=60)
71
- return _extract_ota(resp.json())
72
-
73
-
74
- def _get(path: str, params: dict | None = None) -> dict:
75
- url = f"{TICKET_BASE}{path}"
76
- resp = requests.get(url, headers=_headers(), params=params, timeout=60)
77
- return _extract_ota(resp.json())
78
-
79
-
80
- def encrypt_fields(fields: dict[str, str]) -> dict[str, str]:
81
- """调用网关加密接口,将明文字段加密后返回。fields 如 {"credentialNo": "...", "mobile": "..."}"""
82
- url = f"{TICKET_BASE}/encrypt"
83
- resp = requests.post(url, headers=_headers(), json={"fields": fields}, timeout=30)
84
- body = resp.json()
85
- if body.get("status") != 0:
86
- raise RuntimeError(f"加密接口调用失败: {body.get('message', 'unknown')}")
87
- return body.get("result", {})
88
-
89
-
90
- # ---------- 航班查询 ----------
91
-
92
- # 常用城市名 -> 机场三字码(出发/到达城市解析用)
93
- CITY_NAME_TO_CODE = {
94
- "杭州": "HGH", "北京": "BJS", "上海": "PVG", "广州": "CAN", "深圳": "SZX",
95
- "成都": "CTU", "西安": "XIY", "重庆": "CKG", "南京": "NKG", "武汉": "WUH",
96
- "青岛": "TAO", "厦门": "XMN", "昆明": "KMG", "哈尔滨": "HRB", "大连": "DLC",
97
- "沈阳": "SHE", "长沙": "CSX", "郑州": "CGO", "天津": "TSN", "海口": "HAK",
98
- "三亚": "SYX", "乌鲁木齐": "URC", "贵阳": "KWE", "济南": "TNA", "福州": "FOC",
99
- "南昌": "KHN", "合肥": "HFE", "石家庄": "SJW", "太原": "TYN", "长春": "CGQ",
100
- "兰州": "LHW", "南宁": "NNG", "呼和浩特": "HET", "拉萨": "LXA", "银川": "INC",
101
- "西宁": "XNN", "无锡": "WUX", "宁波": "NGB", "温州": "WNZ", "珠海": "ZUH",
102
- "首都": "PEK", "大兴": "PKX", "浦东": "PVG", "虹桥": "SHA", "萧山": "HGH",
103
- }
104
-
105
-
106
- def resolve_city(name_or_code: str) -> str:
107
- """将城市名或三字码转为 API 使用的三字码。若已是三字码则原样返回大写。"""
108
- if not name_or_code or not isinstance(name_or_code, str):
109
- return ""
110
- s = name_or_code.strip()
111
- if len(s) == 3 and s.isalpha():
112
- return s.upper()
113
- return CITY_NAME_TO_CODE.get(s) or ""
114
-
115
-
116
- def flight_search(
117
- from_city: str,
118
- to_city: str,
119
- from_date: str,
120
- flight_no: str | None = None,
121
- all_data: bool | None = None,
122
- ) -> dict:
123
- search_data = {"fromCityCode": from_city, "toCityCode": to_city, "fromDate": from_date}
124
- if flight_no:
125
- search_data["flightNo"] = flight_no
126
- if all_data is not None:
127
- search_data["allData"] = all_data
128
- return _post("/flight/search", search_data)
129
-
130
-
131
- def _baggage_desc(cabin: dict | None) -> str:
132
- """从舱位的 refundChange 中提取行李额描述,优先用 baggageKg/baggageNum,其次用 baggage 文本。"""
133
- if not cabin:
134
- return "-"
135
- rc = cabin.get("refundChange") or {}
136
- kg = rc.get("baggageKg")
137
- num = rc.get("baggageNum")
138
- if kg is not None and kg > 0 and num is not None and num > 0:
139
- return f"{kg}kg×{num}件"
140
- if kg is not None and kg > 0:
141
- return f"{kg}kg"
142
- if num is not None and num > 0:
143
- return f"{num}件"
144
- text = (rc.get("baggage") or "").strip()
145
- return text if text else "-"
146
-
147
-
148
- def _extract_hhmm(time_str: str) -> str:
149
- """从 'YYYY-MM-DD HH:MM:SS' 或 'HH:MM' 等格式中提取 HH:MM。"""
150
- if not time_str:
151
- return ""
152
- s = time_str.strip()
153
- if " " in s:
154
- s = s.split(" ", 1)[1]
155
- return s[:5] if len(s) >= 5 else s
156
-
157
-
158
- def list_flights_simple(result: dict, top_n: int | None = None) -> list[dict]:
159
- """提取航班列表,每航班一行含经济舱/公务舱最低价、机建燃油费,按经济舱价格排序。
160
- top_n=None 时返回全部航班(默认),不做条数截断。"""
161
- if result.get("code") != "0":
162
- return []
163
- data = result.get("data", {})
164
- flights = data.get("flightDetails", [])
165
- out = []
166
- for f in flights:
167
- carrier = f.get("carrier", {})
168
- from_airport = f.get("fromAirportName") or f.get("fromAirportCode", "")
169
- to_airport = f.get("toAirportName") or f.get("toAirportCode", "")
170
- from_terminal = f.get("fromTerminal") or ""
171
- to_terminal = f.get("toTerminal") or ""
172
- if from_terminal:
173
- from_airport = f"{from_airport}{from_terminal}"
174
- if to_terminal:
175
- to_airport = f"{to_airport}{to_terminal}"
176
- cabins = [c for c in f.get("cabins", []) if c.get("salePrice") is not None]
177
- economy = [c for c in cabins if c.get("grade") == "Y"]
178
- business = [c for c in cabins if c.get("grade") == "C"]
179
- cheapest_economy = min(economy, key=lambda c: c["salePrice"]) if economy else None
180
- cheapest_business = min(business, key=lambda c: c["salePrice"]) if business else None
181
- out.append({
182
- "flightNo": f.get("flightNo"),
183
- "carrierName": carrier.get("name"),
184
- "fromAirport": from_airport,
185
- "toAirport": to_airport,
186
- "fromTime": f.get("fromTime", ""),
187
- "toTime": f.get("toTime", ""),
188
- "economyPrice": cheapest_economy["salePrice"] if cheapest_economy else None,
189
- "economyCabin": (cheapest_economy.get("gradeDesc") or cheapest_economy.get("code")) if cheapest_economy else None,
190
- "businessPrice": cheapest_business["salePrice"] if cheapest_business else None,
191
- "businessCabin": (cheapest_business.get("gradeDesc") or cheapest_business.get("code")) if cheapest_business else None,
192
- "departureTax": f.get("departureTax", 0),
193
- "fuelTax": f.get("fuelTax", 0),
194
- })
195
- out.sort(key=lambda r: (r.get("economyPrice") is None, r.get("economyPrice") or 0))
196
- if top_n is None:
197
- return out
198
- return out[:top_n]
199
-
200
-
201
- # ---------- 表格展示(与 flight_search.py 一致) ----------
202
-
203
- def _display_width(s: str) -> int:
204
- """字符串在终端中的显示宽度:ASCII=1,中文等宽字符=2"""
205
- w = 0
206
- for c in s:
207
- w += 2 if "\u4e00" <= c <= "\u9fff" or "\u3000" <= c <= "\u303f" or "\uff00" <= c <= "\uffef" else 1
208
- return w
209
-
210
-
211
- def _pad_to_width(s: str, width: int, align: str = "left") -> str:
212
- """将字符串按显示宽度对齐到 width,不足用空格补齐"""
213
- s = s or ""
214
- current = _display_width(s)
215
- if current >= width:
216
- return s
217
- pad = " " * (width - current)
218
- return (s + pad) if align == "left" else (pad + s)
219
-
220
-
221
- def _truncate_to_width(s: str, width: int) -> str:
222
- """按显示宽度截断字符串"""
223
- s = s or ""
224
- w = 0
225
- for i, c in enumerate(s):
226
- add = 2 if "\u4e00" <= c <= "\u9fff" or "\u3000" <= c <= "\u303f" or "\uff00" <= c <= "\uffef" else 1
227
- if w + add > width:
228
- return s[:i]
229
- w += add
230
- return s
231
-
232
-
233
- def _print_flights_table(rows: list[dict]) -> None:
234
- """打印航班表格:起飞/到达时间分两列,含机建费、燃油费"""
235
- W = [10, 22, 20, 20, 8, 8, 10, 10, 8, 8]
236
- sep = " "
237
- head = [
238
- _pad_to_width("航班号", W[0]),
239
- _pad_to_width("航空公司", W[1]),
240
- _pad_to_width("出发机场", W[2]),
241
- _pad_to_width("到达机场", W[3]),
242
- _pad_to_width("起飞", W[4]),
243
- _pad_to_width("到达", W[5]),
244
- _pad_to_width("经济舱", W[6], "right"),
245
- _pad_to_width("公务舱", W[7], "right"),
246
- _pad_to_width("机建", W[8], "right"),
247
- _pad_to_width("燃油", W[9], "right"),
248
- ]
249
- print(sep.join(head))
250
- print("-" * (sum(W) + len(sep) * (len(W) - 1)))
251
- for r in rows:
252
- from_time = _extract_hhmm(r.get("fromTime") or "")
253
- to_time = _extract_hhmm(r.get("toTime") or "")
254
- economy_str = f"¥{r['economyPrice']}" if r.get("economyPrice") is not None else "-"
255
- business_str = f"¥{r['businessPrice']}" if r.get("businessPrice") is not None else "-"
256
- dep_tax = r.get("departureTax", 0)
257
- fuel_tax = r.get("fuelTax", 0)
258
- dep_tax_str = f"¥{dep_tax}" if dep_tax else "-"
259
- fuel_tax_str = f"¥{fuel_tax}" if fuel_tax else "-"
260
- line = [
261
- _pad_to_width(_truncate_to_width(str(r.get("flightNo", "")), W[0]), W[0]),
262
- _pad_to_width(_truncate_to_width(r.get("carrierName") or "", W[1]), W[1]),
263
- _pad_to_width(_truncate_to_width(r.get("fromAirport") or "", W[2]), W[2]),
264
- _pad_to_width(_truncate_to_width(r.get("toAirport") or "", W[3]), W[3]),
265
- _pad_to_width(_truncate_to_width(from_time, W[4]), W[4]),
266
- _pad_to_width(_truncate_to_width(to_time, W[5]), W[5]),
267
- _pad_to_width(economy_str, W[6], "right"),
268
- _pad_to_width(business_str, W[7], "right"),
269
- _pad_to_width(dep_tax_str, W[8], "right"),
270
- _pad_to_width(fuel_tax_str, W[9], "right"),
271
- ]
272
- print(sep.join(line))
273
-
274
-
275
- # ---------- 舱位报价查询(实时) ----------
276
-
277
- def cabin_search(
278
- from_city: str,
279
- to_city: str,
280
- from_date: str,
281
- flight_no: str,
282
- air_range_type: str = "OW",
283
- ) -> dict:
284
- return _post("/flight/cabin-search", {
285
- "fromCityCode": from_city,
286
- "toCityCode": to_city,
287
- "fromDate": from_date,
288
- "flightNo": flight_no,
289
- "airRangeType": air_range_type,
290
- })
291
-
292
-
293
- # ---------- 验舱验价 ----------
294
-
295
- def check_price(flight_detail: dict, cabin: dict, from_city: str, to_city: str) -> dict:
296
- detail = {
297
- "cabinGrade": cabin.get("grade", "Y"),
298
- "cabinCode": cabin.get("code"),
299
- "flightNo": flight_detail.get("flightNo"),
300
- "fromAirportCode": flight_detail.get("fromAirportCode"),
301
- "toAirportCode": flight_detail.get("toAirportCode"),
302
- }
303
- if cabin.get("subCabinCode"):
304
- detail["subCabinCode"] = cabin["subCabinCode"]
305
- if flight_detail.get("fromTime"):
306
- detail["fromTime"] = flight_detail["fromTime"]
307
- if flight_detail.get("toTime"):
308
- detail["toTime"] = flight_detail["toTime"]
309
- if flight_detail.get("isShareFlight") and flight_detail.get("realFlightNo"):
310
- detail["realFlightNo"] = flight_detail["realFlightNo"]
311
- return _post("/flight/check-price", {
312
- "airRangeType": flight_detail.get("airRangeType", "OW"),
313
- "departureTax": flight_detail.get("departureTax", 0),
314
- "fuelTax": flight_detail.get("fuelTax", 0),
315
- "facePrice": cabin.get("facePrice"),
316
- "salePrice": cabin.get("salePrice"),
317
- "originalPrice": cabin.get("originalPrice"),
318
- "productType": cabin.get("productType", "STANDARD_REFUND"),
319
- "fromCityCode": from_city,
320
- "toCityCode": to_city,
321
- "shoppingCode": cabin.get("shoppingCode", ""),
322
- "flightCheckPriceDetails": [detail],
323
- })
324
-
325
-
326
- # ---------- 从查询结果中选取航班和舱位 ----------
327
-
328
- def pick_flight_and_cabin(
329
- search_result: dict,
330
- flight_no: str,
331
- cabin_grade: str = "Y",
332
- cheapest: bool = True,
333
- ) -> tuple[dict, dict]:
334
- data = search_result.get("data", {})
335
- flights = data.get("flightDetails", [])
336
- if not flights:
337
- raise ValueError("查询结果中没有航班")
338
- target_flight = None
339
- for f in flights:
340
- if f.get("flightNo") == flight_no:
341
- target_flight = f
342
- break
343
- if not target_flight:
344
- raise ValueError(f"未找到航班 {flight_no}")
345
- cabins = target_flight.get("cabins", [])
346
- matched = [c for c in cabins if c.get("grade") == cabin_grade]
347
- if not matched:
348
- matched = cabins
349
- if not matched:
350
- raise ValueError(f"该航班无可用舱位(舱位等级 {cabin_grade})")
351
- if cheapest:
352
- matched.sort(key=lambda c: c.get("salePrice", 999999))
353
- return target_flight, matched[0]
354
-
355
-
356
- # ---------- 创建订单(常规) ----------
357
-
358
- def create_order(
359
- flight_detail: dict,
360
- cabin: dict,
361
- from_city: str,
362
- to_city: str,
363
- from_date: str,
364
- shopping_code: str,
365
- price_info: str | None,
366
- passenger: dict,
367
- estimated_total: float,
368
- ) -> dict:
369
- to_encrypt: dict[str, str] = {}
370
- if passenger.get("credentialNo"):
371
- to_encrypt["credentialNo"] = passenger["credentialNo"]
372
- if passenger.get("mobile"):
373
- to_encrypt["mobile"] = passenger["mobile"]
374
- if passenger.get("email"):
375
- to_encrypt["email"] = passenger["email"]
376
- encrypted = encrypt_fields(to_encrypt) if to_encrypt else {}
377
- encrypted_passenger = dict(passenger)
378
- for k, v in encrypted.items():
379
- encrypted_passenger[k] = v
380
- order_data: dict = {
381
- "flightInfo": {
382
- "fromCityCode": from_city,
383
- "toCityCode": to_city,
384
- "fromDate": from_date,
385
- "flightNo": flight_detail.get("flightNo"),
386
- "cabinCode": cabin.get("code"),
387
- },
388
- "shoppingCode": shopping_code or "",
389
- "estimatedTotal": estimated_total,
390
- "travelBusiness": True,
391
- "passengerInfo": encrypted_passenger,
392
- }
393
- if price_info:
394
- order_data["priceInfo"] = price_info
395
- if cabin.get("subCabinCode"):
396
- order_data["flightInfo"]["subCabinCode"] = cabin["subCabinCode"]
397
- if flight_detail.get("isShareFlight") and flight_detail.get("realFlightNo"):
398
- order_data["flightInfo"]["realFlightNo"] = flight_detail["realFlightNo"]
399
- return _post("/order/create", order_data)
400
-
401
-
402
- # ---------- 支付前校验 ----------
403
-
404
- def pay_validate(order_no: str) -> dict:
405
- return _post("/order/pay-validate", {"orderNo": order_no})
406
-
407
-
408
- # ---------- 申请出票 ----------
409
-
410
- def order_issue(
411
- order_no: str,
412
- payment_total: float = 0,
413
- payments: list[dict] | None = None,
414
- ) -> dict:
415
- issue_data: dict = {"orderNo": order_no}
416
- issue_data["paymentTotalAmount"] = payment_total
417
- if payments:
418
- issue_data["paymentInfos"] = payments
419
- return _post("/order/issue", issue_data)
420
-
421
-
422
- # ---------- 订单详情 ----------
423
-
424
- def order_detail(order_no: str) -> dict:
425
- return _get(f"/order/{order_no}")
426
-
427
-
428
- def order_cancel(order_no: str) -> dict:
429
- return _post(f"/order/{order_no}/cancel")
430
-
431
-
432
- def order_change(
433
- original_order_no: str,
434
- flight_detail: dict,
435
- cabin: dict,
436
- from_city: str,
437
- to_city: str,
438
- from_date: str,
439
- shopping_code: str,
440
- price_info: str | None,
441
- passenger: dict,
442
- estimated_total: float,
443
- ) -> dict:
444
- to_encrypt: dict[str, str] = {}
445
- if passenger.get("credentialNo"):
446
- to_encrypt["credentialNo"] = passenger["credentialNo"]
447
- if passenger.get("mobile"):
448
- to_encrypt["mobile"] = passenger["mobile"]
449
- if passenger.get("email"):
450
- to_encrypt["email"] = passenger["email"]
451
- encrypted = encrypt_fields(to_encrypt) if to_encrypt else {}
452
- encrypted_passenger = dict(passenger)
453
- for k, v in encrypted.items():
454
- encrypted_passenger[k] = v
455
- change_data: dict = {
456
- "orderNo": original_order_no,
457
- "changeType": 2,
458
- "passengerInfo": encrypted_passenger,
459
- "voluntary": True,
460
- "flightInfo": {
461
- "fromCityCode": from_city,
462
- "toCityCode": to_city,
463
- "fromDate": from_date,
464
- "flightNo": flight_detail.get("flightNo"),
465
- "cabinCode": cabin.get("code"),
466
- },
467
- "shoppingCode": shopping_code or "",
468
- "travelBusiness": True,
469
- "estimatedTotal": 1,
470
- }
471
- if price_info:
472
- change_data["priceInfo"] = price_info
473
- if cabin.get("subCabinCode"):
474
- change_data["flightInfo"]["subCabinCode"] = cabin["subCabinCode"]
475
- if flight_detail.get("isShareFlight") and flight_detail.get("realFlightNo"):
476
- change_data["flightInfo"]["realFlightNo"] = flight_detail["realFlightNo"]
477
- return _post("/order/change", change_data)
478
-
479
-
480
- def order_refund(order_no: str, passengers: list[dict]) -> dict:
481
- refund_passengers = []
482
- for p in passengers:
483
- encrypted = encrypt_fields({"credentialNo": p["credentialNo"]})
484
- refund_passengers.append({
485
- "name": p["name"],
486
- "credentialNo": encrypted.get("credentialNo", p["credentialNo"]),
487
- "credentialType": p.get("credentialType", "IDENTITY"),
488
- })
489
-
490
- return _post(f"/order/{order_no}/refund", {
491
- "reason": "行程变更",
492
- "voluntary": True,
493
- "passengers": refund_passengers,
494
- "customerRefundOrderNo": f"REF{int(time.time() * 1000)}",
495
- "channelRefundOrderNo": f"REF{int(time.time() * 1000)}",
496
- "callbackUrl": "",
497
- })
498
-
499
-
500
- # ---------- CLI ----------
501
-
502
- def _birth_day_from_id_card(credential_no: str) -> str | None:
503
- """从 18 位身份证号中解析出生日期,返回 YYYY-MM-DD;非 18 位或非身份证则返回 None"""
504
- if not credential_no or len(credential_no) != 18:
505
- return None
506
- s = credential_no.strip()
507
- if not s[:17].isdigit():
508
- return None
509
- # 第 7–14 位为出生日期 YYYYMMDD
510
- ymd = s[6:14]
511
- if len(ymd) != 8 or not ymd.isdigit():
512
- return None
513
- return f"{ymd[:4]}-{ymd[4:6]}-{ymd[6:8]}"
514
-
515
-
516
- # 订单状态中文展示
517
- ORDER_SHOW_STATUS_ZH = {
518
- "WAIT_PAYMENT": "待支付",
519
- "WAIT_ISSUE": "待出票",
520
- "ISSUED": "已出票",
521
- "CANCELLED": "已取消",
522
- "REFUNDED": "已退票",
523
- }
524
- ORDER_STATUS_ZH = {"ORDERED": "已下单", "ISSUED": "已出票", "CANCELLED": "已取消", "REFUNDED": "已退票"}
525
- PAYMENT_STATUS_ZH = {"WAIT_PAYMENT": "待支付", "PAID": "已支付", "REFUNDED": "已退款"}
526
- PASSENGER_STATUS_ZH = {"NOT_ISSUE": "待出票", "ISSUED": "已出票", "REFUNDED": "已退票"}
527
-
528
-
529
- def _print_order_summary_zh(data: dict) -> None:
530
- """打印订单关键信息的中文摘要"""
531
- if not data:
532
- return
533
- order_no = data.get("orderNo")
534
- total = data.get("totalAmount")
535
- show_status = data.get("orderShowStatus", "")
536
- order_status = data.get("orderStatus", "")
537
- pay_status = data.get("paymentStatus", "")
538
- cabin_zh = {"Y": "经济舱", "C": "公务舱"}
539
- print("---------- 订单关键信息 ----------")
540
- print(f" 订单号:{order_no}")
541
- print(f" 订单状态:{ORDER_SHOW_STATUS_ZH.get(show_status, show_status)}")
542
- print(f" 处理状态:{ORDER_STATUS_ZH.get(order_status, order_status)}")
543
- print(f" 支付状态:{PAYMENT_STATUS_ZH.get(pay_status, pay_status)}")
544
- if total is not None:
545
- print(f" 订单总额:¥{total}")
546
- flights = data.get("orderFlights") or []
547
- for i, f in enumerate(flights, 1):
548
- cabin = cabin_zh.get(f.get("cabinClass"), f.get("cabinClass") or "")
549
- from_t = f.get("fromTerminal") or ""
550
- to_t = f.get("toTerminal") or ""
551
- seg = f"{f.get('fromCityName','')}{f.get('fromAirportName','')}{from_t} {f.get('fromDate','')} {f.get('fromTime','')} → {f.get('toCityName','')}{f.get('toAirportName','')}{to_t} {f.get('toTime','')}"
552
- print(f" 航程{i}:{f.get('flightNo','')} {seg} {cabin}")
553
- rcd = f.get("refundChangeDetail") or {}
554
- baggage = (rcd.get("baggage") or "").strip()
555
- if baggage:
556
- print(f" 行李额:{baggage}")
557
- refund_headers = rcd.get("refundHeaders") or []
558
- refund_amounts = rcd.get("refundAmountList") or []
559
- if refund_headers and refund_amounts:
560
- pairs = [f"{h} ¥{a}" for h, a in zip(refund_headers, refund_amounts)]
561
- print(f" 退票手续费:{' / '.join(pairs)}")
562
- change_headers = rcd.get("changeHeaders") or []
563
- change_amounts = rcd.get("changeAmountList") or []
564
- if change_headers and change_amounts:
565
- pairs = [f"{h} ¥{a}" for h, a in zip(change_headers, change_amounts)]
566
- print(f" 改签手续费:{' / '.join(pairs)}")
567
- passengers = data.get("passengers") or []
568
- for p in passengers:
569
- st = p.get("status", "")
570
- ticket = p.get("ticketNo") or "—"
571
- print(f" 乘客:{p.get('name','')} 状态:{PASSENGER_STATUS_ZH.get(st, st)} 票号:{ticket}")
572
- print("----------------------------------")
573
-
574
-
575
- def _format_refund_change_rules(order_data: dict) -> dict:
576
- """从订单详情中提取退改签规则,返回结构化 dict 供 Agent 展示给用户。"""
577
- flights = order_data.get("orderFlights") or []
578
- rules = []
579
- for f in flights:
580
- rcd = f.get("refundChangeDetail") or {}
581
- segment = (
582
- f"{f.get('fromCityName', '')} → {f.get('toCityName', '')} "
583
- f"{f.get('flightNo', '')} {f.get('fromDate', '')} {f.get('fromTime', '')}"
584
- )
585
- refund_table = list(zip(
586
- rcd.get("refundHeaders", []),
587
- rcd.get("refundAmountList", []),
588
- ))
589
- change_table = list(zip(
590
- rcd.get("changeHeaders", []),
591
- rcd.get("changeAmountList", []),
592
- ))
593
- rules.append({
594
- "segment": segment,
595
- "facePrice": f.get("facePrice"),
596
- "refund_rules": [{"period": h, "fee": a} for h, a in refund_table],
597
- "change_rules": [{"period": h, "fee": a} for h, a in change_table],
598
- "baggage": rcd.get("baggage", ""),
599
- "endorseRule": rcd.get("endorseRule", ""),
600
- "remark": rcd.get("remark", ""),
601
- })
602
- return {"refundChangeRules": rules}
603
-
604
-
605
- # ---------- 订单缓存 ----------
606
-
607
- def _load_orders_cache() -> dict:
608
- """读取本地订单缓存文件,返回 {orderNo: summary} 字典。"""
609
- if os.path.isfile(ORDERS_CACHE_FILE):
610
- try:
611
- with open(ORDERS_CACHE_FILE, "r", encoding="utf-8") as f:
612
- return json.load(f)
613
- except (OSError, json.JSONDecodeError):
614
- pass
615
- return {}
616
-
617
-
618
- def _save_order_to_cache(order_no: str, order_data: dict) -> None:
619
- """将订单详情的关键摘要追加/更新到缓存文件。"""
620
- cache = _load_orders_cache()
621
- cache[str(order_no)] = {
622
- "orderNo": str(order_no),
623
- "orderShowStatus": order_data.get("orderShowStatus"),
624
- "orderStatus": order_data.get("orderStatus"),
625
- "paymentStatus": order_data.get("paymentStatus"),
626
- "totalAmount": order_data.get("totalAmount"),
627
- "flights": [
628
- {
629
- "flightNo": fl.get("flightNo"),
630
- "fromCityName": fl.get("fromCityName"),
631
- "toCityName": fl.get("toCityName"),
632
- "fromDate": fl.get("fromDate"),
633
- "fromTime": fl.get("fromTime"),
634
- }
635
- for fl in (order_data.get("orderFlights") or [])
636
- ],
637
- "passengers": [
638
- {
639
- "name": p.get("name"),
640
- "status": p.get("status"),
641
- "ticketNo": p.get("ticketNo"),
642
- }
643
- for p in (order_data.get("passengers") or [])
644
- ],
645
- "refundOrders": [
646
- {
647
- "refundOrderNo": str(r.get("refundOrderNo", "")),
648
- "refundOrderShowStatus": r.get("refundOrderShowStatus"),
649
- "refundableTotalAmount": r.get("refundableTotalAmount"),
650
- "refundTotalAmount": r.get("refundTotalAmount"),
651
- }
652
- for r in (order_data.get("refundOrders") or [])
653
- ],
654
- "changeOrders": [
655
- {
656
- "changeOrderNo": str(c.get("orderNo", "")),
657
- "changeOrderShowStatus": c.get("orderShowStatus"),
658
- }
659
- for c in (order_data.get("changeOrders") or [])
660
- ],
661
- "updatedAt": time.strftime("%Y-%m-%d %H:%M:%S"),
662
- }
663
- try:
664
- with open(ORDERS_CACHE_FILE, "w", encoding="utf-8") as f:
665
- json.dump(cache, f, ensure_ascii=False, indent=2)
666
- except OSError:
667
- pass
668
-
669
-
670
- # ---------- 乘客身份管理 ----------
671
-
672
- def _load_passengers() -> list[dict]:
673
- """读取已保存的乘客身份列表。"""
674
- if os.path.isfile(PASSENGERS_FILE):
675
- try:
676
- with open(PASSENGERS_FILE, "r", encoding="utf-8") as f:
677
- return json.load(f)
678
- except (OSError, json.JSONDecodeError):
679
- pass
680
- return []
681
-
682
-
683
- def _save_passenger(name: str, mobile: str, credential_no: str, gender: str) -> dict:
684
- """保存乘客身份到文件,以证件号去重。返回保存后的乘客记录。"""
685
- passengers = _load_passengers()
686
- record = {
687
- "name": name,
688
- "mobile": mobile,
689
- "credentialNo": credential_no,
690
- "gender": gender,
691
- "savedAt": time.strftime("%Y-%m-%d %H:%M:%S"),
692
- }
693
- for i, p in enumerate(passengers):
694
- if p.get("credentialNo") == credential_no:
695
- passengers[i] = record
696
- break
697
- else:
698
- passengers.append(record)
699
- try:
700
- with open(PASSENGERS_FILE, "w", encoding="utf-8") as f:
701
- json.dump(passengers, f, ensure_ascii=False, indent=2)
702
- except OSError:
703
- pass
704
- return record
705
-
706
-
707
- def cmd_search(args: argparse.Namespace) -> int:
708
- from_code = resolve_city(args.from_city) or args.from_city.strip()
709
- to_code = resolve_city(args.to_city) or args.to_city.strip()
710
- if not from_code:
711
- print(json.dumps({"ok": False, "error": f"无法识别出发城市: {args.from_city},请填写三字码或支持的城市名"}, ensure_ascii=False))
712
- return 1
713
- if not to_code:
714
- print(json.dumps({"ok": False, "error": f"无法识别到达城市: {args.to_city},请填写三字码或支持的城市名"}, ensure_ascii=False))
715
- return 1
716
- res = flight_search(from_code, to_code, args.from_date, flight_no=getattr(args, "flight_no", None))
717
- if res.get("code") != "0":
718
- print(json.dumps({"ok": False, "code": res.get("code"), "msg": res.get("msg")}, ensure_ascii=False))
719
- return 1
720
- # 保存完整查询结果,供 create-order 仅传航班号时使用
721
- state = {
722
- "from_city": from_code,
723
- "to_city": to_code,
724
- "from_date": args.from_date,
725
- "result": res,
726
- }
727
- try:
728
- with open(LAST_SEARCH_FILE, "w", encoding="utf-8") as f:
729
- json.dump(state, f, ensure_ascii=False, indent=2)
730
- except OSError:
731
- pass # 忽略写入失败,仅影响后续 create-order 需传全参
732
- rows = list_flights_simple(res, top_n=None)
733
- _print_flights_table(rows)
734
- print()
735
- print(f"共 {len(rows)} 条航班(已按经济舱价格升序展示全部,未截断)。")
736
- print("已保存最近一次查询结果。下单请使用: create-order --flight-no <航班号> [--cabin-grade Y|C]")
737
- return 0
738
-
739
-
740
- def cmd_create_order(args: argparse.Namespace) -> int:
741
- passenger_name = os.environ.get(ENV_PASSENGER_NAME, "").strip()
742
- passenger_mobile = os.environ.get(ENV_PASSENGER_MOBILE, "").strip()
743
- passenger_credential_no = os.environ.get(ENV_PASSENGER_CREDENTIAL_NO, "").strip()
744
- passenger_gender = os.environ.get(ENV_PASSENGER_GENDER, "").strip().upper()
745
-
746
- missing = []
747
- if not passenger_name:
748
- missing.append(ENV_PASSENGER_NAME)
749
- if not passenger_mobile:
750
- missing.append(ENV_PASSENGER_MOBILE)
751
- if not passenger_credential_no:
752
- missing.append(ENV_PASSENGER_CREDENTIAL_NO)
753
- if passenger_gender not in ("M", "F"):
754
- missing.append(f"{ENV_PASSENGER_GENDER} (需为 M 或 F)")
755
- if missing:
756
- print(json.dumps({
757
- "ok": False,
758
- "error": "创建订单前请设置以下环境变量",
759
- "missing": missing,
760
- }, ensure_ascii=False))
761
- return 1
762
-
763
- passenger = {
764
- "name": passenger_name,
765
- "gender": passenger_gender,
766
- "credentialNo": passenger_credential_no,
767
- "credentialType": getattr(args, "credential_type", "IDENTITY"),
768
- "mobile": passenger_mobile,
769
- "passengerType": getattr(args, "passenger_type", "ADU"),
770
- }
771
- birth_day = getattr(args, "passenger_birth_day", None)
772
- if not birth_day and passenger_credential_no:
773
- birth_day = _birth_day_from_id_card(passenger_credential_no)
774
- if birth_day:
775
- passenger["birthDay"] = birth_day
776
-
777
- # 从最近一次查询结果读取行程与航班详情
778
- from_city = to_city = from_date = None
779
- search_res = None
780
- if os.path.isfile(LAST_SEARCH_FILE):
781
- try:
782
- with open(LAST_SEARCH_FILE, "r", encoding="utf-8") as f:
783
- state = json.load(f)
784
- from_city = state.get("from_city")
785
- to_city = state.get("to_city")
786
- from_date = state.get("from_date")
787
- search_res = state.get("result")
788
- except (OSError, json.JSONDecodeError):
789
- pass
790
- if not search_res or not from_city or not to_city or not from_date:
791
- print(json.dumps({
792
- "ok": False,
793
- "error": "未找到最近一次航班查询结果,请先执行查询航班后再下单",
794
- }, ensure_ascii=False))
795
- return 1
796
- try:
797
- flight, cabin = pick_flight_and_cabin(search_res, args.flight_no, args.cabin_grade)
798
- except ValueError as e:
799
- print(json.dumps({"ok": False, "error": str(e)}, ensure_ascii=False))
800
- return 1
801
-
802
- verify_res = check_price(flight, cabin, from_city, to_city)
803
- shopping_code = ""
804
- price_info = None
805
- if verify_res.get("code") == "0":
806
- vdata = verify_res.get("data", {})
807
- shopping_code = vdata.get("shoppingCode", cabin.get("shoppingCode", ""))
808
- price_info = vdata.get("priceInfo")
809
- else:
810
- shopping_code = cabin.get("shoppingCode", "")
811
-
812
- estimated_total = float(cabin.get("salePrice", 0)) + float(flight.get("departureTax", 0)) + float(flight.get("fuelTax", 0))
813
- order_res = create_order(
814
- flight_detail=flight,
815
- cabin=cabin,
816
- from_city=from_city,
817
- to_city=to_city,
818
- from_date=from_date,
819
- shopping_code=shopping_code,
820
- price_info=price_info,
821
- passenger=passenger,
822
- estimated_total=estimated_total,
823
- )
824
- if order_res.get("code") != "0":
825
- print(json.dumps({
826
- "ok": False,
827
- "code": order_res.get("code"),
828
- "msg": order_res.get("msg"),
829
- }, ensure_ascii=False))
830
- return 1
831
- order_data = order_res.get("data", {})
832
- print(json.dumps({
833
- "ok": True,
834
- "orderNo": str(order_data.get("orderNo", "")),
835
- "pnrCode": order_data.get("pnrCode"),
836
- "expireTime": order_data.get("expireTime"),
837
- "paymentTotalAmount": estimated_total,
838
- }, ensure_ascii=False, indent=2))
839
- return 0
840
-
841
-
842
- def cmd_pay_issue(args: argparse.Namespace) -> int:
843
- order_no = args.order_no
844
- # 从订单详情获取应付金额,无需用户传入
845
- detail_res = order_detail(order_no=order_no)
846
- if detail_res.get("code") != "0":
847
- print(json.dumps({"ok": False, "step": "order_detail", "code": detail_res.get("code"), "msg": detail_res.get("msg")}, ensure_ascii=False))
848
- return 1
849
- data = detail_res.get("data", {})
850
- _save_order_to_cache(order_no, data)
851
- order_status = data.get("orderStatus", "")
852
- if order_status == "WAIT_CHECK":
853
- print("---------- 订单状态异常 ----------")
854
- print(f" 订单号:{order_no}")
855
- print(f" 当前状态:{order_status}(等待审核)")
856
- print(" 订单审核还未通过,暂时无法出票,请等待审核完成后再操作。")
857
- print("----------------------------------")
858
- print(json.dumps({"ok": False, "error": "订单审核未通过(WAIT_CHECK),无法出票", "orderStatus": order_status}, ensure_ascii=False))
859
- return 1
860
- total = data.get("totalAmount")
861
- if total is None:
862
- print(json.dumps({"ok": False, "error": "订单详情中无 totalAmount,无法申请出票"}, ensure_ascii=False))
863
- return 1
864
- total = float(total)
865
- pay_res = pay_validate(order_no)
866
- if pay_res.get("code") != "0":
867
- print(json.dumps({"ok": False, "step": "pay_validate", "code": pay_res.get("code"), "msg": pay_res.get("msg")}, ensure_ascii=False))
868
- return 1
869
- issue_res = order_issue(
870
- order_no=order_no,
871
- payment_total=total,
872
- payments=[{
873
- "paymentMethod": "BP_ACCOUNT",
874
- "payAmount": total,
875
- "paymentTradeId": f"PAY{int(time.time() * 1000)}",
876
- }],
877
- )
878
- if issue_res.get("code") != "0":
879
- print(json.dumps({
880
- "ok": False,
881
- "step": "order_issue",
882
- "code": issue_res.get("code"),
883
- "msg": issue_res.get("msg"),
884
- }, ensure_ascii=False))
885
- return 1
886
- print("---------- 出票申请 ----------")
887
- print(f" 订单号:{order_no}")
888
- print(f" 支付金额:¥{total}")
889
- print(" 出票申请已提交(异步处理),请等待结果通知。")
890
- print("------------------------------")
891
- print(json.dumps({"ok": True, "message": "出票申请已提交(异步处理)"}, ensure_ascii=False, indent=2))
892
- return 0
893
-
894
-
895
- def cmd_order_status(args: argparse.Namespace) -> int:
896
- order_no = getattr(args, "order_no", None) or ""
897
- if not order_no:
898
- print(json.dumps({"ok": False, "error": "请提供 --order-no"}, ensure_ascii=False))
899
- return 1
900
- res = order_detail(order_no=order_no)
901
- if res.get("code") != "0":
902
- print(json.dumps({"ok": False, "code": res.get("code"), "msg": res.get("msg")}, ensure_ascii=False))
903
- return 1
904
- data = res.get("data", {})
905
- _save_order_to_cache(order_no, data)
906
- _print_order_summary_zh(data)
907
- print(json.dumps({"ok": True, "data": data}, ensure_ascii=False, indent=2))
908
- return 0
909
-
910
-
911
- def cmd_cancel_order(args: argparse.Namespace) -> int:
912
- order_no = (getattr(args, "order_no", None) or "").strip()
913
- if not order_no:
914
- print(json.dumps({"ok": False, "error": "请提供 --order-no"}, ensure_ascii=False))
915
- return 1
916
-
917
-
918
- res = order_cancel(order_no)
919
- if res.get("code") != "0":
920
- print(json.dumps({"ok": False, "code": res.get("code"), "msg": res.get("msg")}, ensure_ascii=False))
921
- return 1
922
- print(json.dumps({"ok": True, "message": f"订单 {order_no} 已取消"}, ensure_ascii=False, indent=2))
923
- return 0
924
-
925
-
926
- def cmd_refund_order(args: argparse.Namespace) -> int:
927
- order_no = (getattr(args, "order_no", None) or "").strip()
928
- if not order_no:
929
- print(json.dumps({"ok": False, "error": "请提供 --order-no"}, ensure_ascii=False))
930
- return 1
931
-
932
- dry_run = getattr(args, "dry_run", False)
933
-
934
- detail_res = order_detail(order_no=order_no)
935
- if detail_res.get("code") != "0":
936
- print(json.dumps({"ok": False, "step": "order_detail", "code": detail_res.get("code"), "msg": detail_res.get("msg")}, ensure_ascii=False))
937
- return 1
938
- data = detail_res.get("data", {})
939
- _save_order_to_cache(order_no, data)
940
-
941
- show_status = data.get("orderShowStatus", "")
942
- if show_status != "ISSUED":
943
- print(json.dumps({
944
- "ok": False,
945
- "error": f"订单当前状态为 {ORDER_SHOW_STATUS_ZH.get(show_status, show_status)},仅已出票(ISSUED)订单可退票",
946
- "orderShowStatus": show_status,
947
- }, ensure_ascii=False))
948
- return 1
949
-
950
- rules = _format_refund_change_rules(data)
951
-
952
- if dry_run:
953
- _print_order_summary_zh(data)
954
- print(json.dumps({"ok": True, "dryRun": True, **rules}, ensure_ascii=False, indent=2))
955
- return 0
956
-
957
- passenger_name = os.environ.get(ENV_PASSENGER_NAME, "").strip()
958
- passenger_credential_no = os.environ.get(ENV_PASSENGER_CREDENTIAL_NO, "").strip()
959
- if not passenger_name or not passenger_credential_no:
960
- print(json.dumps({
961
- "ok": False,
962
- "error": "退票需要乘客信息,请设置环境变量",
963
- "missing": [k for k, v in [
964
- (ENV_PASSENGER_NAME, passenger_name),
965
- (ENV_PASSENGER_CREDENTIAL_NO, passenger_credential_no),
966
- ] if not v],
967
- }, ensure_ascii=False))
968
- return 1
969
-
970
- passengers = [{
971
- "name": passenger_name,
972
- "credentialNo": passenger_credential_no,
973
- "credentialType": "IDENTITY",
974
- }]
975
-
976
- res = order_refund(order_no, passengers)
977
- if res.get("code") != "0":
978
- print(json.dumps({"ok": False, "code": res.get("code"), "msg": res.get("msg")}, ensure_ascii=False))
979
- return 1
980
- print(json.dumps({"ok": True, "message": f"订单 {order_no} 退票申请已提交", **rules}, ensure_ascii=False, indent=2))
981
- return 0
982
-
983
-
984
- def cmd_change_order(args: argparse.Namespace) -> int:
985
- original_order_no = (getattr(args, "order_no", None) or "").strip()
986
- if not original_order_no:
987
- print(json.dumps({"ok": False, "error": "请提供 --order-no"}, ensure_ascii=False))
988
- return 1
989
-
990
- detail_res = order_detail(order_no=original_order_no)
991
- if detail_res.get("code") != "0":
992
- print(json.dumps({"ok": False, "step": "order_detail", "code": detail_res.get("code"), "msg": detail_res.get("msg")}, ensure_ascii=False))
993
- return 1
994
- detail_data = detail_res.get("data", {})
995
- _save_order_to_cache(original_order_no, detail_data)
996
-
997
- show_status = detail_data.get("orderShowStatus", "")
998
- if show_status != "ISSUED":
999
- print(json.dumps({
1000
- "ok": False,
1001
- "error": f"订单当前状态为 {ORDER_SHOW_STATUS_ZH.get(show_status, show_status)},仅已出票(ISSUED)订单可改签",
1002
- "orderShowStatus": show_status,
1003
- }, ensure_ascii=False))
1004
- return 1
1005
-
1006
- rules = _format_refund_change_rules(detail_data)
1007
-
1008
- passenger_name = os.environ.get(ENV_PASSENGER_NAME, "").strip()
1009
- passenger_mobile = os.environ.get(ENV_PASSENGER_MOBILE, "").strip()
1010
- passenger_credential_no = os.environ.get(ENV_PASSENGER_CREDENTIAL_NO, "").strip()
1011
- passenger_gender = os.environ.get(ENV_PASSENGER_GENDER, "").strip().upper()
1012
-
1013
- missing = []
1014
- if not passenger_name:
1015
- missing.append(ENV_PASSENGER_NAME)
1016
- if not passenger_mobile:
1017
- missing.append(ENV_PASSENGER_MOBILE)
1018
- if not passenger_credential_no:
1019
- missing.append(ENV_PASSENGER_CREDENTIAL_NO)
1020
- if passenger_gender not in ("M", "F"):
1021
- missing.append(f"{ENV_PASSENGER_GENDER} (需为 M 或 F)")
1022
- if missing:
1023
- print(json.dumps({
1024
- "ok": False,
1025
- "error": "改签前请设置以下环境变量",
1026
- "missing": missing,
1027
- }, ensure_ascii=False))
1028
- return 1
1029
-
1030
- passenger = {
1031
- "name": passenger_name,
1032
- "gender": passenger_gender,
1033
- "credentialNo": passenger_credential_no,
1034
- "credentialType": getattr(args, "credential_type", "IDENTITY"),
1035
- "mobile": passenger_mobile,
1036
- "passengerType": getattr(args, "passenger_type", "ADU"),
1037
- }
1038
- birth_day = getattr(args, "passenger_birth_day", None)
1039
- if not birth_day and passenger_credential_no:
1040
- birth_day = _birth_day_from_id_card(passenger_credential_no)
1041
- if birth_day:
1042
- passenger["birthDay"] = birth_day
1043
-
1044
- from_city = to_city = from_date = None
1045
- search_res = None
1046
- if os.path.isfile(LAST_SEARCH_FILE):
1047
- try:
1048
- with open(LAST_SEARCH_FILE, "r", encoding="utf-8") as f:
1049
- state = json.load(f)
1050
- from_city = state.get("from_city")
1051
- to_city = state.get("to_city")
1052
- from_date = state.get("from_date")
1053
- search_res = state.get("result")
1054
- except (OSError, json.JSONDecodeError):
1055
- pass
1056
- if not search_res or not from_city or not to_city or not from_date:
1057
- print(json.dumps({
1058
- "ok": False,
1059
- "error": "未找到最近一次航班查询结果,请先执行查询航班后再改签",
1060
- }, ensure_ascii=False))
1061
- return 1
1062
-
1063
- try:
1064
- flight, cabin = pick_flight_and_cabin(search_res, args.flight_no, args.cabin_grade)
1065
- except ValueError as e:
1066
- print(json.dumps({"ok": False, "error": str(e)}, ensure_ascii=False))
1067
- return 1
1068
-
1069
- verify_res = check_price(flight, cabin, from_city, to_city)
1070
- shopping_code = ""
1071
- price_info = None
1072
- if verify_res.get("code") == "0":
1073
- vdata = verify_res.get("data", {})
1074
- shopping_code = vdata.get("shoppingCode", cabin.get("shoppingCode", ""))
1075
- price_info = vdata.get("priceInfo")
1076
- else:
1077
- shopping_code = cabin.get("shoppingCode", "")
1078
-
1079
- estimated_total = float(cabin.get("salePrice", 0)) + float(flight.get("departureTax", 0)) + float(flight.get("fuelTax", 0))
1080
- change_res = order_change(
1081
- original_order_no=original_order_no,
1082
- flight_detail=flight,
1083
- cabin=cabin,
1084
- from_city=from_city,
1085
- to_city=to_city,
1086
- from_date=from_date,
1087
- shopping_code=shopping_code,
1088
- price_info=price_info,
1089
- passenger=passenger,
1090
- estimated_total=estimated_total,
1091
- )
1092
- if change_res.get("code") != "0":
1093
- print(json.dumps({
1094
- "ok": False,
1095
- "code": change_res.get("code"),
1096
- "msg": change_res.get("msg"),
1097
- }, ensure_ascii=False))
1098
- return 1
1099
- change_data = change_res.get("data", {})
1100
- print(json.dumps({
1101
- "ok": True,
1102
- "changeOrderNo": str(change_data.get("orderNo", "")),
1103
- "pnrCode": change_data.get("pnrCode"),
1104
- "expireTime": change_data.get("expireTime"),
1105
- "paymentTotalAmount": estimated_total,
1106
- **rules,
1107
- }, ensure_ascii=False, indent=2))
1108
- return 0
1109
-
1110
-
1111
- def cmd_list_orders(args: argparse.Namespace) -> int:
1112
- cache = _load_orders_cache()
1113
- if not cache:
1114
- print(json.dumps({"ok": True, "orders": [], "message": "暂无缓存的订单记录"}, ensure_ascii=False, indent=2))
1115
- return 0
1116
- orders = sorted(cache.values(), key=lambda o: o.get("updatedAt", ""), reverse=True)
1117
- print(json.dumps({"ok": True, "orders": orders}, ensure_ascii=False, indent=2))
1118
- return 0
1119
-
1120
-
1121
- def cmd_save_passenger(args: argparse.Namespace) -> int:
1122
- name = os.environ.get(ENV_PASSENGER_NAME, "").strip()
1123
- mobile = os.environ.get(ENV_PASSENGER_MOBILE, "").strip()
1124
- credential_no = os.environ.get(ENV_PASSENGER_CREDENTIAL_NO, "").strip()
1125
- gender = os.environ.get(ENV_PASSENGER_GENDER, "").strip().upper()
1126
- missing = []
1127
- if not name:
1128
- missing.append(ENV_PASSENGER_NAME)
1129
- if not mobile:
1130
- missing.append(ENV_PASSENGER_MOBILE)
1131
- if not credential_no:
1132
- missing.append(ENV_PASSENGER_CREDENTIAL_NO)
1133
- if gender not in ("M", "F"):
1134
- missing.append(f"{ENV_PASSENGER_GENDER} (需为 M 或 F)")
1135
- if missing:
1136
- print(json.dumps({
1137
- "ok": False,
1138
- "error": "保存乘客身份前请设置以下环境变量",
1139
- "missing": missing,
1140
- }, ensure_ascii=False))
1141
- return 1
1142
- record = _save_passenger(name, mobile, credential_no, gender)
1143
- print(json.dumps({"ok": True, "message": f"乘客 {name} 的身份信息已保存", "passenger": record}, ensure_ascii=False, indent=2))
1144
- return 0
1145
-
1146
-
1147
- def cmd_list_passengers(args: argparse.Namespace) -> int:
1148
- passengers = _load_passengers()
1149
- if not passengers:
1150
- print(json.dumps({"ok": True, "passengers": [], "message": "暂无已保存的乘客身份"}, ensure_ascii=False, indent=2))
1151
- return 0
1152
- display = []
1153
- for i, p in enumerate(passengers):
1154
- cred = p.get("credentialNo", "")
1155
- masked = cred[:6] + "****" + cred[-4:] if len(cred) >= 10 else cred
1156
- display.append({
1157
- "index": i + 1,
1158
- "name": p.get("name"),
1159
- "gender": p.get("gender"),
1160
- "credentialNo_masked": masked,
1161
- "mobile": p.get("mobile", "")[:3] + "****" + p.get("mobile", "")[-4:] if len(p.get("mobile", "")) >= 7 else p.get("mobile", ""),
1162
- "savedAt": p.get("savedAt"),
1163
- })
1164
- print(json.dumps({"ok": True, "passengers": display, "_raw": passengers}, ensure_ascii=False, indent=2))
1165
- return 0
1166
-
1167
-
1168
- def main() -> int:
1169
- parser = argparse.ArgumentParser(description="机票查询与下单")
1170
- sub = parser.add_subparsers(dest="command", required=True)
1171
-
1172
- # search:支持三字码或城市名(如 杭州、北京)
1173
- p_search = sub.add_parser("search", help="航班查询,返回当日全部航班(按经济舱价格排序,不截断)")
1174
- p_search.add_argument("--from-city", required=True, help="出发城市:三字码(如 HGH)或城市名(如 杭州)")
1175
- p_search.add_argument("--to-city", required=True, help="到达城市:三字码(如 BJS)或城市名(如 北京)")
1176
- p_search.add_argument("--from-date", required=True, help="出发日期 YYYY-MM-DD")
1177
- p_search.add_argument("--flight-no", default=None, help="可选,指定航班号只查该航班(如 CA1723)")
1178
- p_search.set_defaults(func=cmd_search)
1179
-
1180
- # create-order:仅需航班号,行程与舱位从最近一次 search 结果读取
1181
- p_order = sub.add_parser("create-order", help="创建订单(需先执行 search;行程与舱位从上次查询结果读取)")
1182
- p_order.add_argument("--flight-no", required=True, help="航班号(从 search 结果中选择)")
1183
- p_order.add_argument("--cabin-grade", default="Y", choices=["Y", "C"], help="Y=经济舱 C=公务舱,默认 Y")
1184
- p_order.add_argument("--passenger-birth-day", default=None, help="乘机人生日 YYYY-MM-DD(可选,默认从身份证号解析)")
1185
- p_order.add_argument("--passenger-type", default="ADU", help="ADU/CHD/INF")
1186
- p_order.add_argument("--credential-type", default="IDENTITY")
1187
- p_order.set_defaults(func=cmd_create_order)
1188
-
1189
- # pay-issue:仅需订单号,支付金额从订单详情接口自动获取
1190
- p_issue = sub.add_parser("pay-issue", help="支付前校验并申请出票(金额从订单详情自动获取)")
1191
- p_issue.add_argument("--order-no", required=True, help="订单号")
1192
- p_issue.set_defaults(func=cmd_pay_issue)
1193
-
1194
- # order-status:仅需订单号
1195
- p_status = sub.add_parser("order-status", help="查询订单状态")
1196
- p_status.add_argument("--order-no", required=True, help="订单号")
1197
- p_status.set_defaults(func=cmd_order_status)
1198
-
1199
- # cancel-order:取消订单
1200
- p_cancel = sub.add_parser("cancel-order", help="取消订单(待支付等可取消状态)")
1201
- p_cancel.add_argument("--order-no", required=True, help="订单号")
1202
- p_cancel.set_defaults(func=cmd_cancel_order)
1203
-
1204
- # refund-order:退票
1205
- p_refund = sub.add_parser("refund-order", help="申请退票(已出票订单)")
1206
- p_refund.add_argument("--order-no", required=True, help="订单号")
1207
- p_refund.add_argument("--dry-run", action="store_true", default=False, help="仅查询退改签规则,不实际执行退票")
1208
- p_refund.set_defaults(func=cmd_refund_order)
1209
-
1210
- # change-order:改签
1211
- p_change = sub.add_parser("change-order", help="改签(需先执行 search;从上次查询结果中选取改签航班)")
1212
- p_change.add_argument("--order-no", required=True, help="原订单号")
1213
- p_change.add_argument("--flight-no", required=True, help="改签目标航班号(从 search 结果中选择)")
1214
- p_change.add_argument("--cabin-grade", default="Y", choices=["Y", "C"], help="Y=经济舱 C=公务舱,默认 Y")
1215
- p_change.add_argument("--passenger-birth-day", default=None, help="乘机人生日 YYYY-MM-DD(可选,默认从身份证号解析)")
1216
- p_change.add_argument("--passenger-type", default="ADU", help="ADU/CHD/INF")
1217
- p_change.add_argument("--credential-type", default="IDENTITY")
1218
- p_change.set_defaults(func=cmd_change_order)
1219
-
1220
- # list-orders:列出缓存的订单
1221
- p_list = sub.add_parser("list-orders", help="列出本地缓存的历史订单摘要")
1222
- p_list.set_defaults(func=cmd_list_orders)
1223
-
1224
- # save-passenger:保存当前乘客身份到文件
1225
- p_save_pax = sub.add_parser("save-passenger", help="将当前环境变量中的乘客身份保存到本地文件")
1226
- p_save_pax.set_defaults(func=cmd_save_passenger)
1227
-
1228
- # list-passengers:列出已保存的乘客身份
1229
- p_list_pax = sub.add_parser("list-passengers", help="列出已保存的所有乘客身份")
1230
- p_list_pax.set_defaults(func=cmd_list_passengers)
1231
-
1232
- args = parser.parse_args()
1233
- return args.func(args)
1234
-
1235
-
1236
- if __name__ == "__main__":
1237
- sys.exit(main())
1
+ #!/usr/bin/env python3
2
+ """
3
+ 机票查询与下单脚本:支持航班查询(返回全部航班,不截断)、创建订单、支付校验与出票、订单状态查询、取消订单、退票。
4
+ 乘机人信息从环境变量读取,用户需提前配置。
5
+ """
6
+
7
+ import argparse
8
+ import json
9
+ import os
10
+ import sys
11
+ import time
12
+
13
+ import requests
14
+
15
+ # API 配置(新版网关,Bearer API_KEY 鉴权)
16
+
17
+ GATEWAY_BASE_URL = "https://www.sophnet.com/api"
18
+
19
+ TICKET_BASE = f"{GATEWAY_BASE_URL}/open-apis/ticket"
20
+ def _resolve_api_key() -> str:
21
+ key = os.environ.get("SOPH_API_KEY", "").strip()
22
+ if key:
23
+ return key
24
+ openclaw_path = os.path.expanduser("~/.openclaw/openclaw.json")
25
+ try:
26
+ with open(openclaw_path, "r", encoding="utf-8") as f:
27
+ cfg = json.load(f)
28
+ key = cfg.get("models", {}).get("providers", {}).get("sophnet", {}).get("apiKey", "")
29
+ if key:
30
+ return key
31
+ except (OSError, json.JSONDecodeError):
32
+ pass
33
+ raise RuntimeError("未找到 SOPH_API_KEY:请设置环境变量 SOPH_API_KEY ")
34
+
35
+ API_KEY = _resolve_api_key()
36
+
37
+ # 乘机人信息从环境变量读取(用户需提前设置)
38
+ ENV_PASSENGER_NAME = "PASSENGER_NAME"
39
+ ENV_PASSENGER_MOBILE = "PASSENGER_MOBILE"
40
+ ENV_PASSENGER_CREDENTIAL_NO = "PASSENGER_CREDENTIAL_NO"
41
+ ENV_PASSENGER_GENDER = "PASSENGER_GENDER"
42
+
43
+ # 最近一次航班查询结果保存路径(用于 create-order 仅传航班号时读取)
44
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
45
+ DATA_DIR = os.path.join(os.path.expanduser("~"), ".openclaw", "flight-booking")
46
+ os.makedirs(DATA_DIR, exist_ok=True)
47
+ LAST_SEARCH_FILE = os.path.join(DATA_DIR, ".last_search.json")
48
+ ORDERS_CACHE_FILE = os.path.join(DATA_DIR, ".orders_cache.json")
49
+ PASSENGERS_FILE = os.path.join(DATA_DIR, ".passengers.json")
50
+
51
+ # ---------- 基础 API 工具 ----------
52
+
53
+ def _headers() -> dict:
54
+ return {
55
+ "Content-Type": "application/json",
56
+ "Authorization": f"Bearer {API_KEY}",
57
+ }
58
+
59
+
60
+ def _extract_ota(body: dict) -> dict:
61
+ """从网关响应 {status, message, result} 中提取 OTA 层 {code, msg, data}"""
62
+ result = body.get("result")
63
+ if result is None:
64
+ return {"code": str(body.get("status", "-1")), "msg": body.get("message", "unknown")}
65
+ return result
66
+
67
+
68
+ def _post(path: str, data: dict | None = None) -> dict:
69
+ url = f"{TICKET_BASE}{path}"
70
+ resp = requests.post(url, headers=_headers(), json=data or {}, timeout=60)
71
+ return _extract_ota(resp.json())
72
+
73
+
74
+ def _get(path: str, params: dict | None = None) -> dict:
75
+ url = f"{TICKET_BASE}{path}"
76
+ resp = requests.get(url, headers=_headers(), params=params, timeout=60)
77
+ return _extract_ota(resp.json())
78
+
79
+
80
+ def encrypt_fields(fields: dict[str, str]) -> dict[str, str]:
81
+ """调用网关加密接口,将明文字段加密后返回。fields 如 {"credentialNo": "...", "mobile": "..."}"""
82
+ url = f"{TICKET_BASE}/encrypt"
83
+ resp = requests.post(url, headers=_headers(), json={"fields": fields}, timeout=30)
84
+ body = resp.json()
85
+ if body.get("status") != 0:
86
+ raise RuntimeError(f"加密接口调用失败: {body.get('message', 'unknown')}")
87
+ return body.get("result", {})
88
+
89
+
90
+ # ---------- 航班查询 ----------
91
+
92
+ # 常用城市名 -> 机场三字码(出发/到达城市解析用)
93
+ CITY_NAME_TO_CODE = {
94
+ "杭州": "HGH", "北京": "BJS", "上海": "PVG", "广州": "CAN", "深圳": "SZX",
95
+ "成都": "CTU", "西安": "XIY", "重庆": "CKG", "南京": "NKG", "武汉": "WUH",
96
+ "青岛": "TAO", "厦门": "XMN", "昆明": "KMG", "哈尔滨": "HRB", "大连": "DLC",
97
+ "沈阳": "SHE", "长沙": "CSX", "郑州": "CGO", "天津": "TSN", "海口": "HAK",
98
+ "三亚": "SYX", "乌鲁木齐": "URC", "贵阳": "KWE", "济南": "TNA", "福州": "FOC",
99
+ "南昌": "KHN", "合肥": "HFE", "石家庄": "SJW", "太原": "TYN", "长春": "CGQ",
100
+ "兰州": "LHW", "南宁": "NNG", "呼和浩特": "HET", "拉萨": "LXA", "银川": "INC",
101
+ "西宁": "XNN", "无锡": "WUX", "宁波": "NGB", "温州": "WNZ", "珠海": "ZUH",
102
+ "首都": "PEK", "大兴": "PKX", "浦东": "PVG", "虹桥": "SHA", "萧山": "HGH",
103
+ }
104
+
105
+
106
+ def resolve_city(name_or_code: str) -> str:
107
+ """将城市名或三字码转为 API 使用的三字码。若已是三字码则原样返回大写。"""
108
+ if not name_or_code or not isinstance(name_or_code, str):
109
+ return ""
110
+ s = name_or_code.strip()
111
+ if len(s) == 3 and s.isalpha():
112
+ return s.upper()
113
+ return CITY_NAME_TO_CODE.get(s) or ""
114
+
115
+
116
+ def flight_search(
117
+ from_city: str,
118
+ to_city: str,
119
+ from_date: str,
120
+ flight_no: str | None = None,
121
+ all_data: bool | None = None,
122
+ ) -> dict:
123
+ search_data = {"fromCityCode": from_city, "toCityCode": to_city, "fromDate": from_date}
124
+ if flight_no:
125
+ search_data["flightNo"] = flight_no
126
+ if all_data is not None:
127
+ search_data["allData"] = all_data
128
+ return _post("/flight/search", search_data)
129
+
130
+
131
+ def _baggage_desc(cabin: dict | None) -> str:
132
+ """从舱位的 refundChange 中提取行李额描述,优先用 baggageKg/baggageNum,其次用 baggage 文本。"""
133
+ if not cabin:
134
+ return "-"
135
+ rc = cabin.get("refundChange") or {}
136
+ kg = rc.get("baggageKg")
137
+ num = rc.get("baggageNum")
138
+ if kg is not None and kg > 0 and num is not None and num > 0:
139
+ return f"{kg}kg×{num}件"
140
+ if kg is not None and kg > 0:
141
+ return f"{kg}kg"
142
+ if num is not None and num > 0:
143
+ return f"{num}件"
144
+ text = (rc.get("baggage") or "").strip()
145
+ return text if text else "-"
146
+
147
+
148
+ def _extract_hhmm(time_str: str) -> str:
149
+ """从 'YYYY-MM-DD HH:MM:SS' 或 'HH:MM' 等格式中提取 HH:MM。"""
150
+ if not time_str:
151
+ return ""
152
+ s = time_str.strip()
153
+ if " " in s:
154
+ s = s.split(" ", 1)[1]
155
+ return s[:5] if len(s) >= 5 else s
156
+
157
+
158
+ def list_flights_simple(result: dict, top_n: int | None = None) -> list[dict]:
159
+ """提取航班列表,每航班一行含经济舱/公务舱最低价、机建燃油费,按经济舱价格排序。
160
+ top_n=None 时返回全部航班(默认),不做条数截断。"""
161
+ if result.get("code") != "0":
162
+ return []
163
+ data = result.get("data", {})
164
+ flights = data.get("flightDetails", [])
165
+ out = []
166
+ for f in flights:
167
+ carrier = f.get("carrier", {})
168
+ from_airport = f.get("fromAirportName") or f.get("fromAirportCode", "")
169
+ to_airport = f.get("toAirportName") or f.get("toAirportCode", "")
170
+ from_terminal = f.get("fromTerminal") or ""
171
+ to_terminal = f.get("toTerminal") or ""
172
+ if from_terminal:
173
+ from_airport = f"{from_airport}{from_terminal}"
174
+ if to_terminal:
175
+ to_airport = f"{to_airport}{to_terminal}"
176
+ cabins = [c for c in f.get("cabins", []) if c.get("salePrice") is not None]
177
+ economy = [c for c in cabins if c.get("grade") == "Y"]
178
+ business = [c for c in cabins if c.get("grade") == "C"]
179
+ cheapest_economy = min(economy, key=lambda c: c["salePrice"]) if economy else None
180
+ cheapest_business = min(business, key=lambda c: c["salePrice"]) if business else None
181
+ out.append({
182
+ "flightNo": f.get("flightNo"),
183
+ "carrierName": carrier.get("name"),
184
+ "fromAirport": from_airport,
185
+ "toAirport": to_airport,
186
+ "fromTime": f.get("fromTime", ""),
187
+ "toTime": f.get("toTime", ""),
188
+ "economyPrice": cheapest_economy["salePrice"] if cheapest_economy else None,
189
+ "economyCabin": (cheapest_economy.get("gradeDesc") or cheapest_economy.get("code")) if cheapest_economy else None,
190
+ "businessPrice": cheapest_business["salePrice"] if cheapest_business else None,
191
+ "businessCabin": (cheapest_business.get("gradeDesc") or cheapest_business.get("code")) if cheapest_business else None,
192
+ "departureTax": f.get("departureTax", 0),
193
+ "fuelTax": f.get("fuelTax", 0),
194
+ })
195
+ out.sort(key=lambda r: (r.get("economyPrice") is None, r.get("economyPrice") or 0))
196
+ if top_n is None:
197
+ return out
198
+ return out[:top_n]
199
+
200
+
201
+ # ---------- 表格展示(与 flight_search.py 一致) ----------
202
+
203
+ def _display_width(s: str) -> int:
204
+ """字符串在终端中的显示宽度:ASCII=1,中文等宽字符=2"""
205
+ w = 0
206
+ for c in s:
207
+ w += 2 if "\u4e00" <= c <= "\u9fff" or "\u3000" <= c <= "\u303f" or "\uff00" <= c <= "\uffef" else 1
208
+ return w
209
+
210
+
211
+ def _pad_to_width(s: str, width: int, align: str = "left") -> str:
212
+ """将字符串按显示宽度对齐到 width,不足用空格补齐"""
213
+ s = s or ""
214
+ current = _display_width(s)
215
+ if current >= width:
216
+ return s
217
+ pad = " " * (width - current)
218
+ return (s + pad) if align == "left" else (pad + s)
219
+
220
+
221
+ def _truncate_to_width(s: str, width: int) -> str:
222
+ """按显示宽度截断字符串"""
223
+ s = s or ""
224
+ w = 0
225
+ for i, c in enumerate(s):
226
+ add = 2 if "\u4e00" <= c <= "\u9fff" or "\u3000" <= c <= "\u303f" or "\uff00" <= c <= "\uffef" else 1
227
+ if w + add > width:
228
+ return s[:i]
229
+ w += add
230
+ return s
231
+
232
+
233
+ def _print_flights_table(rows: list[dict]) -> None:
234
+ """打印航班表格:起飞/到达时间分两列,含机建费、燃油费"""
235
+ W = [10, 22, 20, 20, 8, 8, 10, 10, 8, 8]
236
+ sep = " "
237
+ head = [
238
+ _pad_to_width("航班号", W[0]),
239
+ _pad_to_width("航空公司", W[1]),
240
+ _pad_to_width("出发机场", W[2]),
241
+ _pad_to_width("到达机场", W[3]),
242
+ _pad_to_width("起飞", W[4]),
243
+ _pad_to_width("到达", W[5]),
244
+ _pad_to_width("经济舱", W[6], "right"),
245
+ _pad_to_width("公务舱", W[7], "right"),
246
+ _pad_to_width("机建", W[8], "right"),
247
+ _pad_to_width("燃油", W[9], "right"),
248
+ ]
249
+ print(sep.join(head))
250
+ print("-" * (sum(W) + len(sep) * (len(W) - 1)))
251
+ for r in rows:
252
+ from_time = _extract_hhmm(r.get("fromTime") or "")
253
+ to_time = _extract_hhmm(r.get("toTime") or "")
254
+ economy_str = f"¥{r['economyPrice']}" if r.get("economyPrice") is not None else "-"
255
+ business_str = f"¥{r['businessPrice']}" if r.get("businessPrice") is not None else "-"
256
+ dep_tax = r.get("departureTax", 0)
257
+ fuel_tax = r.get("fuelTax", 0)
258
+ dep_tax_str = f"¥{dep_tax}" if dep_tax else "-"
259
+ fuel_tax_str = f"¥{fuel_tax}" if fuel_tax else "-"
260
+ line = [
261
+ _pad_to_width(_truncate_to_width(str(r.get("flightNo", "")), W[0]), W[0]),
262
+ _pad_to_width(_truncate_to_width(r.get("carrierName") or "", W[1]), W[1]),
263
+ _pad_to_width(_truncate_to_width(r.get("fromAirport") or "", W[2]), W[2]),
264
+ _pad_to_width(_truncate_to_width(r.get("toAirport") or "", W[3]), W[3]),
265
+ _pad_to_width(_truncate_to_width(from_time, W[4]), W[4]),
266
+ _pad_to_width(_truncate_to_width(to_time, W[5]), W[5]),
267
+ _pad_to_width(economy_str, W[6], "right"),
268
+ _pad_to_width(business_str, W[7], "right"),
269
+ _pad_to_width(dep_tax_str, W[8], "right"),
270
+ _pad_to_width(fuel_tax_str, W[9], "right"),
271
+ ]
272
+ print(sep.join(line))
273
+
274
+
275
+ # ---------- 舱位报价查询(实时) ----------
276
+
277
+ def cabin_search(
278
+ from_city: str,
279
+ to_city: str,
280
+ from_date: str,
281
+ flight_no: str,
282
+ air_range_type: str = "OW",
283
+ ) -> dict:
284
+ return _post("/flight/cabin-search", {
285
+ "fromCityCode": from_city,
286
+ "toCityCode": to_city,
287
+ "fromDate": from_date,
288
+ "flightNo": flight_no,
289
+ "airRangeType": air_range_type,
290
+ })
291
+
292
+
293
+ # ---------- 验舱验价 ----------
294
+
295
+ def check_price(flight_detail: dict, cabin: dict, from_city: str, to_city: str) -> dict:
296
+ detail = {
297
+ "cabinGrade": cabin.get("grade", "Y"),
298
+ "cabinCode": cabin.get("code"),
299
+ "flightNo": flight_detail.get("flightNo"),
300
+ "fromAirportCode": flight_detail.get("fromAirportCode"),
301
+ "toAirportCode": flight_detail.get("toAirportCode"),
302
+ }
303
+ if cabin.get("subCabinCode"):
304
+ detail["subCabinCode"] = cabin["subCabinCode"]
305
+ if flight_detail.get("fromTime"):
306
+ detail["fromTime"] = flight_detail["fromTime"]
307
+ if flight_detail.get("toTime"):
308
+ detail["toTime"] = flight_detail["toTime"]
309
+ if flight_detail.get("isShareFlight") and flight_detail.get("realFlightNo"):
310
+ detail["realFlightNo"] = flight_detail["realFlightNo"]
311
+ return _post("/flight/check-price", {
312
+ "airRangeType": flight_detail.get("airRangeType", "OW"),
313
+ "departureTax": flight_detail.get("departureTax", 0),
314
+ "fuelTax": flight_detail.get("fuelTax", 0),
315
+ "facePrice": cabin.get("facePrice"),
316
+ "salePrice": cabin.get("salePrice"),
317
+ "originalPrice": cabin.get("originalPrice"),
318
+ "productType": cabin.get("productType", "STANDARD_REFUND"),
319
+ "fromCityCode": from_city,
320
+ "toCityCode": to_city,
321
+ "shoppingCode": cabin.get("shoppingCode", ""),
322
+ "flightCheckPriceDetails": [detail],
323
+ })
324
+
325
+
326
+ # ---------- 从查询结果中选取航班和舱位 ----------
327
+
328
+ def pick_flight_and_cabin(
329
+ search_result: dict,
330
+ flight_no: str,
331
+ cabin_grade: str = "Y",
332
+ cheapest: bool = True,
333
+ ) -> tuple[dict, dict]:
334
+ data = search_result.get("data", {})
335
+ flights = data.get("flightDetails", [])
336
+ if not flights:
337
+ raise ValueError("查询结果中没有航班")
338
+ target_flight = None
339
+ for f in flights:
340
+ if f.get("flightNo") == flight_no:
341
+ target_flight = f
342
+ break
343
+ if not target_flight:
344
+ raise ValueError(f"未找到航班 {flight_no}")
345
+ cabins = target_flight.get("cabins", [])
346
+ matched = [c for c in cabins if c.get("grade") == cabin_grade]
347
+ if not matched:
348
+ matched = cabins
349
+ if not matched:
350
+ raise ValueError(f"该航班无可用舱位(舱位等级 {cabin_grade})")
351
+ if cheapest:
352
+ matched.sort(key=lambda c: c.get("salePrice", 999999))
353
+ return target_flight, matched[0]
354
+
355
+
356
+ # ---------- 创建订单(常规) ----------
357
+
358
+ def create_order(
359
+ flight_detail: dict,
360
+ cabin: dict,
361
+ from_city: str,
362
+ to_city: str,
363
+ from_date: str,
364
+ shopping_code: str,
365
+ price_info: str | None,
366
+ passenger: dict,
367
+ estimated_total: float,
368
+ ) -> dict:
369
+ to_encrypt: dict[str, str] = {}
370
+ if passenger.get("credentialNo"):
371
+ to_encrypt["credentialNo"] = passenger["credentialNo"]
372
+ if passenger.get("mobile"):
373
+ to_encrypt["mobile"] = passenger["mobile"]
374
+ if passenger.get("email"):
375
+ to_encrypt["email"] = passenger["email"]
376
+ encrypted = encrypt_fields(to_encrypt) if to_encrypt else {}
377
+ encrypted_passenger = dict(passenger)
378
+ for k, v in encrypted.items():
379
+ encrypted_passenger[k] = v
380
+ order_data: dict = {
381
+ "flightInfo": {
382
+ "fromCityCode": from_city,
383
+ "toCityCode": to_city,
384
+ "fromDate": from_date,
385
+ "flightNo": flight_detail.get("flightNo"),
386
+ "cabinCode": cabin.get("code"),
387
+ },
388
+ "shoppingCode": shopping_code or "",
389
+ "estimatedTotal": estimated_total,
390
+ "travelBusiness": True,
391
+ "passengerInfo": encrypted_passenger,
392
+ }
393
+ if price_info:
394
+ order_data["priceInfo"] = price_info
395
+ if cabin.get("subCabinCode"):
396
+ order_data["flightInfo"]["subCabinCode"] = cabin["subCabinCode"]
397
+ if flight_detail.get("isShareFlight") and flight_detail.get("realFlightNo"):
398
+ order_data["flightInfo"]["realFlightNo"] = flight_detail["realFlightNo"]
399
+ return _post("/order/create", order_data)
400
+
401
+
402
+ # ---------- 支付前校验 ----------
403
+
404
+ def pay_validate(order_no: str) -> dict:
405
+ return _post("/order/pay-validate", {"orderNo": order_no})
406
+
407
+
408
+ # ---------- 申请出票 ----------
409
+
410
+ def order_issue(
411
+ order_no: str,
412
+ payment_total: float = 0,
413
+ payments: list[dict] | None = None,
414
+ ) -> dict:
415
+ issue_data: dict = {"orderNo": order_no}
416
+ issue_data["paymentTotalAmount"] = payment_total
417
+ if payments:
418
+ issue_data["paymentInfos"] = payments
419
+ return _post("/order/issue", issue_data)
420
+
421
+
422
+ # ---------- 订单详情 ----------
423
+
424
+ def order_detail(order_no: str) -> dict:
425
+ return _get(f"/order/{order_no}")
426
+
427
+
428
+ def order_cancel(order_no: str) -> dict:
429
+ return _post(f"/order/{order_no}/cancel")
430
+
431
+
432
+ def order_change(
433
+ original_order_no: str,
434
+ flight_detail: dict,
435
+ cabin: dict,
436
+ from_city: str,
437
+ to_city: str,
438
+ from_date: str,
439
+ shopping_code: str,
440
+ price_info: str | None,
441
+ passenger: dict,
442
+ estimated_total: float,
443
+ ) -> dict:
444
+ to_encrypt: dict[str, str] = {}
445
+ if passenger.get("credentialNo"):
446
+ to_encrypt["credentialNo"] = passenger["credentialNo"]
447
+ if passenger.get("mobile"):
448
+ to_encrypt["mobile"] = passenger["mobile"]
449
+ if passenger.get("email"):
450
+ to_encrypt["email"] = passenger["email"]
451
+ encrypted = encrypt_fields(to_encrypt) if to_encrypt else {}
452
+ encrypted_passenger = dict(passenger)
453
+ for k, v in encrypted.items():
454
+ encrypted_passenger[k] = v
455
+ change_data: dict = {
456
+ "orderNo": original_order_no,
457
+ "changeType": 2,
458
+ "passengerInfo": encrypted_passenger,
459
+ "voluntary": True,
460
+ "flightInfo": {
461
+ "fromCityCode": from_city,
462
+ "toCityCode": to_city,
463
+ "fromDate": from_date,
464
+ "flightNo": flight_detail.get("flightNo"),
465
+ "cabinCode": cabin.get("code"),
466
+ },
467
+ "shoppingCode": shopping_code or "",
468
+ "travelBusiness": True,
469
+ "estimatedTotal": 1,
470
+ }
471
+ if price_info:
472
+ change_data["priceInfo"] = price_info
473
+ if cabin.get("subCabinCode"):
474
+ change_data["flightInfo"]["subCabinCode"] = cabin["subCabinCode"]
475
+ if flight_detail.get("isShareFlight") and flight_detail.get("realFlightNo"):
476
+ change_data["flightInfo"]["realFlightNo"] = flight_detail["realFlightNo"]
477
+ return _post("/order/change", change_data)
478
+
479
+
480
+ def order_refund(order_no: str, passengers: list[dict]) -> dict:
481
+ refund_passengers = []
482
+ for p in passengers:
483
+ encrypted = encrypt_fields({"credentialNo": p["credentialNo"]})
484
+ refund_passengers.append({
485
+ "name": p["name"],
486
+ "credentialNo": encrypted.get("credentialNo", p["credentialNo"]),
487
+ "credentialType": p.get("credentialType", "IDENTITY"),
488
+ })
489
+
490
+ return _post(f"/order/{order_no}/refund", {
491
+ "reason": "行程变更",
492
+ "voluntary": True,
493
+ "passengers": refund_passengers,
494
+ "customerRefundOrderNo": f"REF{int(time.time() * 1000)}",
495
+ "channelRefundOrderNo": f"REF{int(time.time() * 1000)}",
496
+ "callbackUrl": "",
497
+ })
498
+
499
+
500
+ # ---------- CLI ----------
501
+
502
+ def _birth_day_from_id_card(credential_no: str) -> str | None:
503
+ """从 18 位身份证号中解析出生日期,返回 YYYY-MM-DD;非 18 位或非身份证则返回 None"""
504
+ if not credential_no or len(credential_no) != 18:
505
+ return None
506
+ s = credential_no.strip()
507
+ if not s[:17].isdigit():
508
+ return None
509
+ # 第 7–14 位为出生日期 YYYYMMDD
510
+ ymd = s[6:14]
511
+ if len(ymd) != 8 or not ymd.isdigit():
512
+ return None
513
+ return f"{ymd[:4]}-{ymd[4:6]}-{ymd[6:8]}"
514
+
515
+
516
+ # 订单状态中文展示
517
+ ORDER_SHOW_STATUS_ZH = {
518
+ "WAIT_PAYMENT": "待支付",
519
+ "WAIT_ISSUE": "待出票",
520
+ "ISSUED": "已出票",
521
+ "CANCELLED": "已取消",
522
+ "REFUNDED": "已退票",
523
+ }
524
+ ORDER_STATUS_ZH = {"ORDERED": "已下单", "ISSUED": "已出票", "CANCELLED": "已取消", "REFUNDED": "已退票"}
525
+ PAYMENT_STATUS_ZH = {"WAIT_PAYMENT": "待支付", "PAID": "已支付", "REFUNDED": "已退款"}
526
+ PASSENGER_STATUS_ZH = {"NOT_ISSUE": "待出票", "ISSUED": "已出票", "REFUNDED": "已退票"}
527
+
528
+
529
+ def _print_order_summary_zh(data: dict) -> None:
530
+ """打印订单关键信息的中文摘要"""
531
+ if not data:
532
+ return
533
+ order_no = data.get("orderNo")
534
+ total = data.get("totalAmount")
535
+ show_status = data.get("orderShowStatus", "")
536
+ order_status = data.get("orderStatus", "")
537
+ pay_status = data.get("paymentStatus", "")
538
+ cabin_zh = {"Y": "经济舱", "C": "公务舱"}
539
+ print("---------- 订单关键信息 ----------")
540
+ print(f" 订单号:{order_no}")
541
+ print(f" 订单状态:{ORDER_SHOW_STATUS_ZH.get(show_status, show_status)}")
542
+ print(f" 处理状态:{ORDER_STATUS_ZH.get(order_status, order_status)}")
543
+ print(f" 支付状态:{PAYMENT_STATUS_ZH.get(pay_status, pay_status)}")
544
+ if total is not None:
545
+ print(f" 订单总额:¥{total}")
546
+ flights = data.get("orderFlights") or []
547
+ for i, f in enumerate(flights, 1):
548
+ cabin = cabin_zh.get(f.get("cabinClass"), f.get("cabinClass") or "")
549
+ from_t = f.get("fromTerminal") or ""
550
+ to_t = f.get("toTerminal") or ""
551
+ seg = f"{f.get('fromCityName','')}{f.get('fromAirportName','')}{from_t} {f.get('fromDate','')} {f.get('fromTime','')} → {f.get('toCityName','')}{f.get('toAirportName','')}{to_t} {f.get('toTime','')}"
552
+ print(f" 航程{i}:{f.get('flightNo','')} {seg} {cabin}")
553
+ rcd = f.get("refundChangeDetail") or {}
554
+ baggage = (rcd.get("baggage") or "").strip()
555
+ if baggage:
556
+ print(f" 行李额:{baggage}")
557
+ refund_headers = rcd.get("refundHeaders") or []
558
+ refund_amounts = rcd.get("refundAmountList") or []
559
+ if refund_headers and refund_amounts:
560
+ pairs = [f"{h} ¥{a}" for h, a in zip(refund_headers, refund_amounts)]
561
+ print(f" 退票手续费:{' / '.join(pairs)}")
562
+ change_headers = rcd.get("changeHeaders") or []
563
+ change_amounts = rcd.get("changeAmountList") or []
564
+ if change_headers and change_amounts:
565
+ pairs = [f"{h} ¥{a}" for h, a in zip(change_headers, change_amounts)]
566
+ print(f" 改签手续费:{' / '.join(pairs)}")
567
+ passengers = data.get("passengers") or []
568
+ for p in passengers:
569
+ st = p.get("status", "")
570
+ ticket = p.get("ticketNo") or "—"
571
+ print(f" 乘客:{p.get('name','')} 状态:{PASSENGER_STATUS_ZH.get(st, st)} 票号:{ticket}")
572
+ print("----------------------------------")
573
+
574
+
575
+ def _format_refund_change_rules(order_data: dict) -> dict:
576
+ """从订单详情中提取退改签规则,返回结构化 dict 供 Agent 展示给用户。"""
577
+ flights = order_data.get("orderFlights") or []
578
+ rules = []
579
+ for f in flights:
580
+ rcd = f.get("refundChangeDetail") or {}
581
+ segment = (
582
+ f"{f.get('fromCityName', '')} → {f.get('toCityName', '')} "
583
+ f"{f.get('flightNo', '')} {f.get('fromDate', '')} {f.get('fromTime', '')}"
584
+ )
585
+ refund_table = list(zip(
586
+ rcd.get("refundHeaders", []),
587
+ rcd.get("refundAmountList", []),
588
+ ))
589
+ change_table = list(zip(
590
+ rcd.get("changeHeaders", []),
591
+ rcd.get("changeAmountList", []),
592
+ ))
593
+ rules.append({
594
+ "segment": segment,
595
+ "facePrice": f.get("facePrice"),
596
+ "refund_rules": [{"period": h, "fee": a} for h, a in refund_table],
597
+ "change_rules": [{"period": h, "fee": a} for h, a in change_table],
598
+ "baggage": rcd.get("baggage", ""),
599
+ "endorseRule": rcd.get("endorseRule", ""),
600
+ "remark": rcd.get("remark", ""),
601
+ })
602
+ return {"refundChangeRules": rules}
603
+
604
+
605
+ # ---------- 订单缓存 ----------
606
+
607
+ def _load_orders_cache() -> dict:
608
+ """读取本地订单缓存文件,返回 {orderNo: summary} 字典。"""
609
+ if os.path.isfile(ORDERS_CACHE_FILE):
610
+ try:
611
+ with open(ORDERS_CACHE_FILE, "r", encoding="utf-8") as f:
612
+ return json.load(f)
613
+ except (OSError, json.JSONDecodeError):
614
+ pass
615
+ return {}
616
+
617
+
618
+ def _save_order_to_cache(order_no: str, order_data: dict) -> None:
619
+ """将订单详情的关键摘要追加/更新到缓存文件。"""
620
+ cache = _load_orders_cache()
621
+ cache[str(order_no)] = {
622
+ "orderNo": str(order_no),
623
+ "orderShowStatus": order_data.get("orderShowStatus"),
624
+ "orderStatus": order_data.get("orderStatus"),
625
+ "paymentStatus": order_data.get("paymentStatus"),
626
+ "totalAmount": order_data.get("totalAmount"),
627
+ "flights": [
628
+ {
629
+ "flightNo": fl.get("flightNo"),
630
+ "fromCityName": fl.get("fromCityName"),
631
+ "toCityName": fl.get("toCityName"),
632
+ "fromDate": fl.get("fromDate"),
633
+ "fromTime": fl.get("fromTime"),
634
+ }
635
+ for fl in (order_data.get("orderFlights") or [])
636
+ ],
637
+ "passengers": [
638
+ {
639
+ "name": p.get("name"),
640
+ "status": p.get("status"),
641
+ "ticketNo": p.get("ticketNo"),
642
+ }
643
+ for p in (order_data.get("passengers") or [])
644
+ ],
645
+ "refundOrders": [
646
+ {
647
+ "refundOrderNo": str(r.get("refundOrderNo", "")),
648
+ "refundOrderShowStatus": r.get("refundOrderShowStatus"),
649
+ "refundableTotalAmount": r.get("refundableTotalAmount"),
650
+ "refundTotalAmount": r.get("refundTotalAmount"),
651
+ }
652
+ for r in (order_data.get("refundOrders") or [])
653
+ ],
654
+ "changeOrders": [
655
+ {
656
+ "changeOrderNo": str(c.get("orderNo", "")),
657
+ "changeOrderShowStatus": c.get("orderShowStatus"),
658
+ }
659
+ for c in (order_data.get("changeOrders") or [])
660
+ ],
661
+ "updatedAt": time.strftime("%Y-%m-%d %H:%M:%S"),
662
+ }
663
+ try:
664
+ with open(ORDERS_CACHE_FILE, "w", encoding="utf-8") as f:
665
+ json.dump(cache, f, ensure_ascii=False, indent=2)
666
+ except OSError:
667
+ pass
668
+
669
+
670
+ # ---------- 乘客身份管理 ----------
671
+
672
+ def _load_passengers() -> list[dict]:
673
+ """读取已保存的乘客身份列表。"""
674
+ if os.path.isfile(PASSENGERS_FILE):
675
+ try:
676
+ with open(PASSENGERS_FILE, "r", encoding="utf-8") as f:
677
+ return json.load(f)
678
+ except (OSError, json.JSONDecodeError):
679
+ pass
680
+ return []
681
+
682
+
683
+ def _save_passenger(name: str, mobile: str, credential_no: str, gender: str) -> dict:
684
+ """保存乘客身份到文件,以证件号去重。返回保存后的乘客记录。"""
685
+ passengers = _load_passengers()
686
+ record = {
687
+ "name": name,
688
+ "mobile": mobile,
689
+ "credentialNo": credential_no,
690
+ "gender": gender,
691
+ "savedAt": time.strftime("%Y-%m-%d %H:%M:%S"),
692
+ }
693
+ for i, p in enumerate(passengers):
694
+ if p.get("credentialNo") == credential_no:
695
+ passengers[i] = record
696
+ break
697
+ else:
698
+ passengers.append(record)
699
+ try:
700
+ with open(PASSENGERS_FILE, "w", encoding="utf-8") as f:
701
+ json.dump(passengers, f, ensure_ascii=False, indent=2)
702
+ except OSError:
703
+ pass
704
+ return record
705
+
706
+
707
+ def cmd_search(args: argparse.Namespace) -> int:
708
+ from_code = resolve_city(args.from_city) or args.from_city.strip()
709
+ to_code = resolve_city(args.to_city) or args.to_city.strip()
710
+ if not from_code:
711
+ print(json.dumps({"ok": False, "error": f"无法识别出发城市: {args.from_city},请填写三字码或支持的城市名"}, ensure_ascii=False))
712
+ return 1
713
+ if not to_code:
714
+ print(json.dumps({"ok": False, "error": f"无法识别到达城市: {args.to_city},请填写三字码或支持的城市名"}, ensure_ascii=False))
715
+ return 1
716
+ res = flight_search(from_code, to_code, args.from_date, flight_no=getattr(args, "flight_no", None))
717
+ if res.get("code") != "0":
718
+ print(json.dumps({"ok": False, "code": res.get("code"), "msg": res.get("msg")}, ensure_ascii=False))
719
+ return 1
720
+ # 保存完整查询结果,供 create-order 仅传航班号时使用
721
+ state = {
722
+ "from_city": from_code,
723
+ "to_city": to_code,
724
+ "from_date": args.from_date,
725
+ "result": res,
726
+ }
727
+ try:
728
+ with open(LAST_SEARCH_FILE, "w", encoding="utf-8") as f:
729
+ json.dump(state, f, ensure_ascii=False, indent=2)
730
+ except OSError:
731
+ pass # 忽略写入失败,仅影响后续 create-order 需传全参
732
+ rows = list_flights_simple(res, top_n=None)
733
+ _print_flights_table(rows)
734
+ print()
735
+ print(f"共 {len(rows)} 条航班(已按经济舱价格升序展示全部,未截断)。")
736
+ print("已保存最近一次查询结果。下单请使用: create-order --flight-no <航班号> [--cabin-grade Y|C]")
737
+ return 0
738
+
739
+
740
+ def cmd_create_order(args: argparse.Namespace) -> int:
741
+ passenger_name = os.environ.get(ENV_PASSENGER_NAME, "").strip()
742
+ passenger_mobile = os.environ.get(ENV_PASSENGER_MOBILE, "").strip()
743
+ passenger_credential_no = os.environ.get(ENV_PASSENGER_CREDENTIAL_NO, "").strip()
744
+ passenger_gender = os.environ.get(ENV_PASSENGER_GENDER, "").strip().upper()
745
+
746
+ missing = []
747
+ if not passenger_name:
748
+ missing.append(ENV_PASSENGER_NAME)
749
+ if not passenger_mobile:
750
+ missing.append(ENV_PASSENGER_MOBILE)
751
+ if not passenger_credential_no:
752
+ missing.append(ENV_PASSENGER_CREDENTIAL_NO)
753
+ if passenger_gender not in ("M", "F"):
754
+ missing.append(f"{ENV_PASSENGER_GENDER} (需为 M 或 F)")
755
+ if missing:
756
+ print(json.dumps({
757
+ "ok": False,
758
+ "error": "创建订单前请设置以下环境变量",
759
+ "missing": missing,
760
+ }, ensure_ascii=False))
761
+ return 1
762
+
763
+ passenger = {
764
+ "name": passenger_name,
765
+ "gender": passenger_gender,
766
+ "credentialNo": passenger_credential_no,
767
+ "credentialType": getattr(args, "credential_type", "IDENTITY"),
768
+ "mobile": passenger_mobile,
769
+ "passengerType": getattr(args, "passenger_type", "ADU"),
770
+ }
771
+ birth_day = getattr(args, "passenger_birth_day", None)
772
+ if not birth_day and passenger_credential_no:
773
+ birth_day = _birth_day_from_id_card(passenger_credential_no)
774
+ if birth_day:
775
+ passenger["birthDay"] = birth_day
776
+
777
+ # 从最近一次查询结果读取行程与航班详情
778
+ from_city = to_city = from_date = None
779
+ search_res = None
780
+ if os.path.isfile(LAST_SEARCH_FILE):
781
+ try:
782
+ with open(LAST_SEARCH_FILE, "r", encoding="utf-8") as f:
783
+ state = json.load(f)
784
+ from_city = state.get("from_city")
785
+ to_city = state.get("to_city")
786
+ from_date = state.get("from_date")
787
+ search_res = state.get("result")
788
+ except (OSError, json.JSONDecodeError):
789
+ pass
790
+ if not search_res or not from_city or not to_city or not from_date:
791
+ print(json.dumps({
792
+ "ok": False,
793
+ "error": "未找到最近一次航班查询结果,请先执行查询航班后再下单",
794
+ }, ensure_ascii=False))
795
+ return 1
796
+ try:
797
+ flight, cabin = pick_flight_and_cabin(search_res, args.flight_no, args.cabin_grade)
798
+ except ValueError as e:
799
+ print(json.dumps({"ok": False, "error": str(e)}, ensure_ascii=False))
800
+ return 1
801
+
802
+ verify_res = check_price(flight, cabin, from_city, to_city)
803
+ shopping_code = ""
804
+ price_info = None
805
+ if verify_res.get("code") == "0":
806
+ vdata = verify_res.get("data", {})
807
+ shopping_code = vdata.get("shoppingCode", cabin.get("shoppingCode", ""))
808
+ price_info = vdata.get("priceInfo")
809
+ else:
810
+ shopping_code = cabin.get("shoppingCode", "")
811
+
812
+ estimated_total = float(cabin.get("salePrice", 0)) + float(flight.get("departureTax", 0)) + float(flight.get("fuelTax", 0))
813
+ order_res = create_order(
814
+ flight_detail=flight,
815
+ cabin=cabin,
816
+ from_city=from_city,
817
+ to_city=to_city,
818
+ from_date=from_date,
819
+ shopping_code=shopping_code,
820
+ price_info=price_info,
821
+ passenger=passenger,
822
+ estimated_total=estimated_total,
823
+ )
824
+ if order_res.get("code") != "0":
825
+ print(json.dumps({
826
+ "ok": False,
827
+ "code": order_res.get("code"),
828
+ "msg": order_res.get("msg"),
829
+ }, ensure_ascii=False))
830
+ return 1
831
+ order_data = order_res.get("data", {})
832
+ print(json.dumps({
833
+ "ok": True,
834
+ "orderNo": str(order_data.get("orderNo", "")),
835
+ "pnrCode": order_data.get("pnrCode"),
836
+ "expireTime": order_data.get("expireTime"),
837
+ "paymentTotalAmount": estimated_total,
838
+ }, ensure_ascii=False, indent=2))
839
+ return 0
840
+
841
+
842
+ def cmd_pay_issue(args: argparse.Namespace) -> int:
843
+ order_no = args.order_no
844
+ # 从订单详情获取应付金额,无需用户传入
845
+ detail_res = order_detail(order_no=order_no)
846
+ if detail_res.get("code") != "0":
847
+ print(json.dumps({"ok": False, "step": "order_detail", "code": detail_res.get("code"), "msg": detail_res.get("msg")}, ensure_ascii=False))
848
+ return 1
849
+ data = detail_res.get("data", {})
850
+ _save_order_to_cache(order_no, data)
851
+ order_status = data.get("orderStatus", "")
852
+ if order_status == "WAIT_CHECK":
853
+ print("---------- 订单状态异常 ----------")
854
+ print(f" 订单号:{order_no}")
855
+ print(f" 当前状态:{order_status}(等待审核)")
856
+ print(" 订单审核还未通过,暂时无法出票,请等待审核完成后再操作。")
857
+ print("----------------------------------")
858
+ print(json.dumps({"ok": False, "error": "订单审核未通过(WAIT_CHECK),无法出票", "orderStatus": order_status}, ensure_ascii=False))
859
+ return 1
860
+ total = data.get("totalAmount")
861
+ if total is None:
862
+ print(json.dumps({"ok": False, "error": "订单详情中无 totalAmount,无法申请出票"}, ensure_ascii=False))
863
+ return 1
864
+ total = float(total)
865
+ pay_res = pay_validate(order_no)
866
+ if pay_res.get("code") != "0":
867
+ print(json.dumps({"ok": False, "step": "pay_validate", "code": pay_res.get("code"), "msg": pay_res.get("msg")}, ensure_ascii=False))
868
+ return 1
869
+ issue_res = order_issue(
870
+ order_no=order_no,
871
+ payment_total=total,
872
+ payments=[{
873
+ "paymentMethod": "BP_ACCOUNT",
874
+ "payAmount": total,
875
+ "paymentTradeId": f"PAY{int(time.time() * 1000)}",
876
+ }],
877
+ )
878
+ if issue_res.get("code") != "0":
879
+ print(json.dumps({
880
+ "ok": False,
881
+ "step": "order_issue",
882
+ "code": issue_res.get("code"),
883
+ "msg": issue_res.get("msg"),
884
+ }, ensure_ascii=False))
885
+ return 1
886
+ print("---------- 出票申请 ----------")
887
+ print(f" 订单号:{order_no}")
888
+ print(f" 支付金额:¥{total}")
889
+ print(" 出票申请已提交(异步处理),请等待结果通知。")
890
+ print("------------------------------")
891
+ print(json.dumps({"ok": True, "message": "出票申请已提交(异步处理)"}, ensure_ascii=False, indent=2))
892
+ return 0
893
+
894
+
895
+ def cmd_order_status(args: argparse.Namespace) -> int:
896
+ order_no = getattr(args, "order_no", None) or ""
897
+ if not order_no:
898
+ print(json.dumps({"ok": False, "error": "请提供 --order-no"}, ensure_ascii=False))
899
+ return 1
900
+ res = order_detail(order_no=order_no)
901
+ if res.get("code") != "0":
902
+ print(json.dumps({"ok": False, "code": res.get("code"), "msg": res.get("msg")}, ensure_ascii=False))
903
+ return 1
904
+ data = res.get("data", {})
905
+ _save_order_to_cache(order_no, data)
906
+ _print_order_summary_zh(data)
907
+ print(json.dumps({"ok": True, "data": data}, ensure_ascii=False, indent=2))
908
+ return 0
909
+
910
+
911
+ def cmd_cancel_order(args: argparse.Namespace) -> int:
912
+ order_no = (getattr(args, "order_no", None) or "").strip()
913
+ if not order_no:
914
+ print(json.dumps({"ok": False, "error": "请提供 --order-no"}, ensure_ascii=False))
915
+ return 1
916
+
917
+
918
+ res = order_cancel(order_no)
919
+ if res.get("code") != "0":
920
+ print(json.dumps({"ok": False, "code": res.get("code"), "msg": res.get("msg")}, ensure_ascii=False))
921
+ return 1
922
+ print(json.dumps({"ok": True, "message": f"订单 {order_no} 已取消"}, ensure_ascii=False, indent=2))
923
+ return 0
924
+
925
+
926
+ def cmd_refund_order(args: argparse.Namespace) -> int:
927
+ order_no = (getattr(args, "order_no", None) or "").strip()
928
+ if not order_no:
929
+ print(json.dumps({"ok": False, "error": "请提供 --order-no"}, ensure_ascii=False))
930
+ return 1
931
+
932
+ dry_run = getattr(args, "dry_run", False)
933
+
934
+ detail_res = order_detail(order_no=order_no)
935
+ if detail_res.get("code") != "0":
936
+ print(json.dumps({"ok": False, "step": "order_detail", "code": detail_res.get("code"), "msg": detail_res.get("msg")}, ensure_ascii=False))
937
+ return 1
938
+ data = detail_res.get("data", {})
939
+ _save_order_to_cache(order_no, data)
940
+
941
+ show_status = data.get("orderShowStatus", "")
942
+ if show_status != "ISSUED":
943
+ print(json.dumps({
944
+ "ok": False,
945
+ "error": f"订单当前状态为 {ORDER_SHOW_STATUS_ZH.get(show_status, show_status)},仅已出票(ISSUED)订单可退票",
946
+ "orderShowStatus": show_status,
947
+ }, ensure_ascii=False))
948
+ return 1
949
+
950
+ rules = _format_refund_change_rules(data)
951
+
952
+ if dry_run:
953
+ _print_order_summary_zh(data)
954
+ print(json.dumps({"ok": True, "dryRun": True, **rules}, ensure_ascii=False, indent=2))
955
+ return 0
956
+
957
+ passenger_name = os.environ.get(ENV_PASSENGER_NAME, "").strip()
958
+ passenger_credential_no = os.environ.get(ENV_PASSENGER_CREDENTIAL_NO, "").strip()
959
+ if not passenger_name or not passenger_credential_no:
960
+ print(json.dumps({
961
+ "ok": False,
962
+ "error": "退票需要乘客信息,请设置环境变量",
963
+ "missing": [k for k, v in [
964
+ (ENV_PASSENGER_NAME, passenger_name),
965
+ (ENV_PASSENGER_CREDENTIAL_NO, passenger_credential_no),
966
+ ] if not v],
967
+ }, ensure_ascii=False))
968
+ return 1
969
+
970
+ passengers = [{
971
+ "name": passenger_name,
972
+ "credentialNo": passenger_credential_no,
973
+ "credentialType": "IDENTITY",
974
+ }]
975
+
976
+ res = order_refund(order_no, passengers)
977
+ if res.get("code") != "0":
978
+ print(json.dumps({"ok": False, "code": res.get("code"), "msg": res.get("msg")}, ensure_ascii=False))
979
+ return 1
980
+ print(json.dumps({"ok": True, "message": f"订单 {order_no} 退票申请已提交", **rules}, ensure_ascii=False, indent=2))
981
+ return 0
982
+
983
+
984
+ def cmd_change_order(args: argparse.Namespace) -> int:
985
+ original_order_no = (getattr(args, "order_no", None) or "").strip()
986
+ if not original_order_no:
987
+ print(json.dumps({"ok": False, "error": "请提供 --order-no"}, ensure_ascii=False))
988
+ return 1
989
+
990
+ detail_res = order_detail(order_no=original_order_no)
991
+ if detail_res.get("code") != "0":
992
+ print(json.dumps({"ok": False, "step": "order_detail", "code": detail_res.get("code"), "msg": detail_res.get("msg")}, ensure_ascii=False))
993
+ return 1
994
+ detail_data = detail_res.get("data", {})
995
+ _save_order_to_cache(original_order_no, detail_data)
996
+
997
+ show_status = detail_data.get("orderShowStatus", "")
998
+ if show_status != "ISSUED":
999
+ print(json.dumps({
1000
+ "ok": False,
1001
+ "error": f"订单当前状态为 {ORDER_SHOW_STATUS_ZH.get(show_status, show_status)},仅已出票(ISSUED)订单可改签",
1002
+ "orderShowStatus": show_status,
1003
+ }, ensure_ascii=False))
1004
+ return 1
1005
+
1006
+ rules = _format_refund_change_rules(detail_data)
1007
+
1008
+ passenger_name = os.environ.get(ENV_PASSENGER_NAME, "").strip()
1009
+ passenger_mobile = os.environ.get(ENV_PASSENGER_MOBILE, "").strip()
1010
+ passenger_credential_no = os.environ.get(ENV_PASSENGER_CREDENTIAL_NO, "").strip()
1011
+ passenger_gender = os.environ.get(ENV_PASSENGER_GENDER, "").strip().upper()
1012
+
1013
+ missing = []
1014
+ if not passenger_name:
1015
+ missing.append(ENV_PASSENGER_NAME)
1016
+ if not passenger_mobile:
1017
+ missing.append(ENV_PASSENGER_MOBILE)
1018
+ if not passenger_credential_no:
1019
+ missing.append(ENV_PASSENGER_CREDENTIAL_NO)
1020
+ if passenger_gender not in ("M", "F"):
1021
+ missing.append(f"{ENV_PASSENGER_GENDER} (需为 M 或 F)")
1022
+ if missing:
1023
+ print(json.dumps({
1024
+ "ok": False,
1025
+ "error": "改签前请设置以下环境变量",
1026
+ "missing": missing,
1027
+ }, ensure_ascii=False))
1028
+ return 1
1029
+
1030
+ passenger = {
1031
+ "name": passenger_name,
1032
+ "gender": passenger_gender,
1033
+ "credentialNo": passenger_credential_no,
1034
+ "credentialType": getattr(args, "credential_type", "IDENTITY"),
1035
+ "mobile": passenger_mobile,
1036
+ "passengerType": getattr(args, "passenger_type", "ADU"),
1037
+ }
1038
+ birth_day = getattr(args, "passenger_birth_day", None)
1039
+ if not birth_day and passenger_credential_no:
1040
+ birth_day = _birth_day_from_id_card(passenger_credential_no)
1041
+ if birth_day:
1042
+ passenger["birthDay"] = birth_day
1043
+
1044
+ from_city = to_city = from_date = None
1045
+ search_res = None
1046
+ if os.path.isfile(LAST_SEARCH_FILE):
1047
+ try:
1048
+ with open(LAST_SEARCH_FILE, "r", encoding="utf-8") as f:
1049
+ state = json.load(f)
1050
+ from_city = state.get("from_city")
1051
+ to_city = state.get("to_city")
1052
+ from_date = state.get("from_date")
1053
+ search_res = state.get("result")
1054
+ except (OSError, json.JSONDecodeError):
1055
+ pass
1056
+ if not search_res or not from_city or not to_city or not from_date:
1057
+ print(json.dumps({
1058
+ "ok": False,
1059
+ "error": "未找到最近一次航班查询结果,请先执行查询航班后再改签",
1060
+ }, ensure_ascii=False))
1061
+ return 1
1062
+
1063
+ try:
1064
+ flight, cabin = pick_flight_and_cabin(search_res, args.flight_no, args.cabin_grade)
1065
+ except ValueError as e:
1066
+ print(json.dumps({"ok": False, "error": str(e)}, ensure_ascii=False))
1067
+ return 1
1068
+
1069
+ verify_res = check_price(flight, cabin, from_city, to_city)
1070
+ shopping_code = ""
1071
+ price_info = None
1072
+ if verify_res.get("code") == "0":
1073
+ vdata = verify_res.get("data", {})
1074
+ shopping_code = vdata.get("shoppingCode", cabin.get("shoppingCode", ""))
1075
+ price_info = vdata.get("priceInfo")
1076
+ else:
1077
+ shopping_code = cabin.get("shoppingCode", "")
1078
+
1079
+ estimated_total = float(cabin.get("salePrice", 0)) + float(flight.get("departureTax", 0)) + float(flight.get("fuelTax", 0))
1080
+ change_res = order_change(
1081
+ original_order_no=original_order_no,
1082
+ flight_detail=flight,
1083
+ cabin=cabin,
1084
+ from_city=from_city,
1085
+ to_city=to_city,
1086
+ from_date=from_date,
1087
+ shopping_code=shopping_code,
1088
+ price_info=price_info,
1089
+ passenger=passenger,
1090
+ estimated_total=estimated_total,
1091
+ )
1092
+ if change_res.get("code") != "0":
1093
+ print(json.dumps({
1094
+ "ok": False,
1095
+ "code": change_res.get("code"),
1096
+ "msg": change_res.get("msg"),
1097
+ }, ensure_ascii=False))
1098
+ return 1
1099
+ change_data = change_res.get("data", {})
1100
+ print(json.dumps({
1101
+ "ok": True,
1102
+ "changeOrderNo": str(change_data.get("orderNo", "")),
1103
+ "pnrCode": change_data.get("pnrCode"),
1104
+ "expireTime": change_data.get("expireTime"),
1105
+ "paymentTotalAmount": estimated_total,
1106
+ **rules,
1107
+ }, ensure_ascii=False, indent=2))
1108
+ return 0
1109
+
1110
+
1111
+ def cmd_list_orders(args: argparse.Namespace) -> int:
1112
+ cache = _load_orders_cache()
1113
+ if not cache:
1114
+ print(json.dumps({"ok": True, "orders": [], "message": "暂无缓存的订单记录"}, ensure_ascii=False, indent=2))
1115
+ return 0
1116
+ orders = sorted(cache.values(), key=lambda o: o.get("updatedAt", ""), reverse=True)
1117
+ print(json.dumps({"ok": True, "orders": orders}, ensure_ascii=False, indent=2))
1118
+ return 0
1119
+
1120
+
1121
+ def cmd_save_passenger(args: argparse.Namespace) -> int:
1122
+ name = os.environ.get(ENV_PASSENGER_NAME, "").strip()
1123
+ mobile = os.environ.get(ENV_PASSENGER_MOBILE, "").strip()
1124
+ credential_no = os.environ.get(ENV_PASSENGER_CREDENTIAL_NO, "").strip()
1125
+ gender = os.environ.get(ENV_PASSENGER_GENDER, "").strip().upper()
1126
+ missing = []
1127
+ if not name:
1128
+ missing.append(ENV_PASSENGER_NAME)
1129
+ if not mobile:
1130
+ missing.append(ENV_PASSENGER_MOBILE)
1131
+ if not credential_no:
1132
+ missing.append(ENV_PASSENGER_CREDENTIAL_NO)
1133
+ if gender not in ("M", "F"):
1134
+ missing.append(f"{ENV_PASSENGER_GENDER} (需为 M 或 F)")
1135
+ if missing:
1136
+ print(json.dumps({
1137
+ "ok": False,
1138
+ "error": "保存乘客身份前请设置以下环境变量",
1139
+ "missing": missing,
1140
+ }, ensure_ascii=False))
1141
+ return 1
1142
+ record = _save_passenger(name, mobile, credential_no, gender)
1143
+ print(json.dumps({"ok": True, "message": f"乘客 {name} 的身份信息已保存", "passenger": record}, ensure_ascii=False, indent=2))
1144
+ return 0
1145
+
1146
+
1147
+ def cmd_list_passengers(args: argparse.Namespace) -> int:
1148
+ passengers = _load_passengers()
1149
+ if not passengers:
1150
+ print(json.dumps({"ok": True, "passengers": [], "message": "暂无已保存的乘客身份"}, ensure_ascii=False, indent=2))
1151
+ return 0
1152
+ display = []
1153
+ for i, p in enumerate(passengers):
1154
+ cred = p.get("credentialNo", "")
1155
+ masked = cred[:6] + "****" + cred[-4:] if len(cred) >= 10 else cred
1156
+ display.append({
1157
+ "index": i + 1,
1158
+ "name": p.get("name"),
1159
+ "gender": p.get("gender"),
1160
+ "credentialNo_masked": masked,
1161
+ "mobile": p.get("mobile", "")[:3] + "****" + p.get("mobile", "")[-4:] if len(p.get("mobile", "")) >= 7 else p.get("mobile", ""),
1162
+ "savedAt": p.get("savedAt"),
1163
+ })
1164
+ print(json.dumps({"ok": True, "passengers": display, "_raw": passengers}, ensure_ascii=False, indent=2))
1165
+ return 0
1166
+
1167
+
1168
+ def main() -> int:
1169
+ parser = argparse.ArgumentParser(description="机票查询与下单")
1170
+ sub = parser.add_subparsers(dest="command", required=True)
1171
+
1172
+ # search:支持三字码或城市名(如 杭州、北京)
1173
+ p_search = sub.add_parser("search", help="航班查询,返回当日全部航班(按经济舱价格排序,不截断)")
1174
+ p_search.add_argument("--from-city", required=True, help="出发城市:三字码(如 HGH)或城市名(如 杭州)")
1175
+ p_search.add_argument("--to-city", required=True, help="到达城市:三字码(如 BJS)或城市名(如 北京)")
1176
+ p_search.add_argument("--from-date", required=True, help="出发日期 YYYY-MM-DD")
1177
+ p_search.add_argument("--flight-no", default=None, help="可选,指定航班号只查该航班(如 CA1723)")
1178
+ p_search.set_defaults(func=cmd_search)
1179
+
1180
+ # create-order:仅需航班号,行程与舱位从最近一次 search 结果读取
1181
+ p_order = sub.add_parser("create-order", help="创建订单(需先执行 search;行程与舱位从上次查询结果读取)")
1182
+ p_order.add_argument("--flight-no", required=True, help="航班号(从 search 结果中选择)")
1183
+ p_order.add_argument("--cabin-grade", default="Y", choices=["Y", "C"], help="Y=经济舱 C=公务舱,默认 Y")
1184
+ p_order.add_argument("--passenger-birth-day", default=None, help="乘机人生日 YYYY-MM-DD(可选,默认从身份证号解析)")
1185
+ p_order.add_argument("--passenger-type", default="ADU", help="ADU/CHD/INF")
1186
+ p_order.add_argument("--credential-type", default="IDENTITY")
1187
+ p_order.set_defaults(func=cmd_create_order)
1188
+
1189
+ # pay-issue:仅需订单号,支付金额从订单详情接口自动获取
1190
+ p_issue = sub.add_parser("pay-issue", help="支付前校验并申请出票(金额从订单详情自动获取)")
1191
+ p_issue.add_argument("--order-no", required=True, help="订单号")
1192
+ p_issue.set_defaults(func=cmd_pay_issue)
1193
+
1194
+ # order-status:仅需订单号
1195
+ p_status = sub.add_parser("order-status", help="查询订单状态")
1196
+ p_status.add_argument("--order-no", required=True, help="订单号")
1197
+ p_status.set_defaults(func=cmd_order_status)
1198
+
1199
+ # cancel-order:取消订单
1200
+ p_cancel = sub.add_parser("cancel-order", help="取消订单(待支付等可取消状态)")
1201
+ p_cancel.add_argument("--order-no", required=True, help="订单号")
1202
+ p_cancel.set_defaults(func=cmd_cancel_order)
1203
+
1204
+ # refund-order:退票
1205
+ p_refund = sub.add_parser("refund-order", help="申请退票(已出票订单)")
1206
+ p_refund.add_argument("--order-no", required=True, help="订单号")
1207
+ p_refund.add_argument("--dry-run", action="store_true", default=False, help="仅查询退改签规则,不实际执行退票")
1208
+ p_refund.set_defaults(func=cmd_refund_order)
1209
+
1210
+ # change-order:改签
1211
+ p_change = sub.add_parser("change-order", help="改签(需先执行 search;从上次查询结果中选取改签航班)")
1212
+ p_change.add_argument("--order-no", required=True, help="原订单号")
1213
+ p_change.add_argument("--flight-no", required=True, help="改签目标航班号(从 search 结果中选择)")
1214
+ p_change.add_argument("--cabin-grade", default="Y", choices=["Y", "C"], help="Y=经济舱 C=公务舱,默认 Y")
1215
+ p_change.add_argument("--passenger-birth-day", default=None, help="乘机人生日 YYYY-MM-DD(可选,默认从身份证号解析)")
1216
+ p_change.add_argument("--passenger-type", default="ADU", help="ADU/CHD/INF")
1217
+ p_change.add_argument("--credential-type", default="IDENTITY")
1218
+ p_change.set_defaults(func=cmd_change_order)
1219
+
1220
+ # list-orders:列出缓存的订单
1221
+ p_list = sub.add_parser("list-orders", help="列出本地缓存的历史订单摘要")
1222
+ p_list.set_defaults(func=cmd_list_orders)
1223
+
1224
+ # save-passenger:保存当前乘客身份到文件
1225
+ p_save_pax = sub.add_parser("save-passenger", help="将当前环境变量中的乘客身份保存到本地文件")
1226
+ p_save_pax.set_defaults(func=cmd_save_passenger)
1227
+
1228
+ # list-passengers:列出已保存的乘客身份
1229
+ p_list_pax = sub.add_parser("list-passengers", help="列出已保存的所有乘客身份")
1230
+ p_list_pax.set_defaults(func=cmd_list_passengers)
1231
+
1232
+ args = parser.parse_args()
1233
+ return args.func(args)
1234
+
1235
+
1236
+ if __name__ == "__main__":
1237
+ sys.exit(main())