hap-cli 0.8.27__tar.gz → 0.8.29__tar.gz

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 (294) hide show
  1. {hap_cli-0.8.27 → hap_cli-0.8.29}/PKG-INFO +1 -1
  2. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/__init__.py +1 -1
  3. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/chat_cmd.py +80 -4
  4. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/post_cmd.py +69 -9
  5. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/worksheet_cmd.py +122 -3
  6. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/chat.py +153 -0
  7. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/post.py +23 -7
  8. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/worksheet.py +229 -6
  9. hap_cli-0.8.29/hap_cli/guide/en/post.md +101 -0
  10. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/index.json +8 -0
  11. hap_cli-0.8.29/hap_cli/guide/zh-Hans/post.md +89 -0
  12. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/i18n.py +12 -0
  13. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/locale/messages.json +66 -14
  14. hap_cli-0.8.29/hap_cli/tests/test_chat_recall_cli.py +159 -0
  15. hap_cli-0.8.29/hap_cli/tests/test_post_scope_cli.py +203 -0
  16. hap_cli-0.8.29/hap_cli/tests/test_worksheet_switch.py +174 -0
  17. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli.egg-info/PKG-INFO +1 -1
  18. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli.egg-info/SOURCES.txt +5 -0
  19. {hap_cli-0.8.27 → hap_cli-0.8.29}/MANIFEST.in +0 -0
  20. {hap_cli-0.8.27 → hap_cli-0.8.29}/README.md +0 -0
  21. {hap_cli-0.8.27 → hap_cli-0.8.29}/app_live_tests/__init__.py +0 -0
  22. {hap_cli-0.8.27 → hap_cli-0.8.29}/app_live_tests/__main__.py +0 -0
  23. {hap_cli-0.8.27 → hap_cli-0.8.29}/app_live_tests/config.py +0 -0
  24. {hap_cli-0.8.27 → hap_cli-0.8.29}/app_live_tests/harness.py +0 -0
  25. {hap_cli-0.8.27 → hap_cli-0.8.29}/app_live_tests/smoke.py +0 -0
  26. {hap_cli-0.8.27 → hap_cli-0.8.29}/app_live_tests/state.py +0 -0
  27. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/README.md +0 -0
  28. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/README_CN.md +0 -0
  29. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/__main__.py +0 -0
  30. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/__init__.py +0 -0
  31. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/app_cmd.py +0 -0
  32. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/app_creator_cmd.py +0 -0
  33. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/app_editor_cmd.py +0 -0
  34. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/attachment_cmd.py +0 -0
  35. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/auth_cmd.py +0 -0
  36. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/calendar_cmd.py +0 -0
  37. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/chart_cmd.py +0 -0
  38. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/chatbot_cmd.py +0 -0
  39. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/config_cmd.py +0 -0
  40. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/contact_cmd.py +0 -0
  41. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/department_cmd.py +0 -0
  42. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/group_cmd.py +0 -0
  43. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/guide_cmd.py +0 -0
  44. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/icon_cmd.py +0 -0
  45. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/instance_cmd.py +0 -0
  46. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/node_cmd.py +0 -0
  47. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/optionset_cmd.py +0 -0
  48. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/page_cmd.py +0 -0
  49. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/record_cmd.py +0 -0
  50. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/region_cmd.py +0 -0
  51. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/role_cmd.py +0 -0
  52. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/update_cmd.py +0 -0
  53. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/upload_cmd.py +0 -0
  54. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/v3_registry.py +0 -0
  55. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/commands/workflow_cmd.py +0 -0
  56. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/context.py +0 -0
  57. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/__init__.py +0 -0
  58. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/action_spec_adapter.py +0 -0
  59. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app.py +0 -0
  60. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/__init__.py +0 -0
  61. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/_hapmeta/__init__.py +0 -0
  62. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/_hapmeta/control_type_codes.py +0 -0
  63. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/_hapmeta/worksheet_templates.py +0 -0
  64. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/charts.py +0 -0
  65. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/cleanup.py +0 -0
  66. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/compiler.py +0 -0
  67. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/config.py +0 -0
  68. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/design/design.schema.json +0 -0
  69. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/errors.py +0 -0
  70. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/examples/minimal.design.json +0 -0
  71. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/executor.py +0 -0
  72. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/fields.py +0 -0
  73. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/hap.py +0 -0
  74. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/merge.py +0 -0
  75. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/recording/__init__.py +0 -0
  76. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/recording/console.py +0 -0
  77. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/recording/jsonl.py +0 -0
  78. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/recording/mirror.py +0 -0
  79. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/recording/report.py +0 -0
  80. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/schema.py +0 -0
  81. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/seed/INSTRUCTIONS.md +0 -0
  82. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/seed/__init__.py +0 -0
  83. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/seed/cli.py +0 -0
  84. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/seed/executor.py +0 -0
  85. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/seed/resources/attachments.json +0 -0
  86. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/seed/resources/sample.docx +0 -0
  87. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/seed/resources/sample.pdf +0 -0
  88. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/seed/resources/sample_images.json +0 -0
  89. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/seed/template.py +0 -0
  90. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/selftest.py +0 -0
  91. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/smoke.py +0 -0
  92. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/steps.py +0 -0
  93. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/store.py +0 -0
  94. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/validate.py +0 -0
  95. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/app_creator/workflow_dsl.py +0 -0
  96. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/attachment.py +0 -0
  97. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/auth.py +0 -0
  98. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/calendar_mod.py +0 -0
  99. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/chart.py +0 -0
  100. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/chatbot.py +0 -0
  101. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/cli_access.py +0 -0
  102. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/contact.py +0 -0
  103. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/control_type_codes.py +0 -0
  104. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/department.py +0 -0
  105. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/__init__.py +0 -0
  106. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
  107. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
  108. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/apply.py +0 -0
  109. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/componentlower.py +0 -0
  110. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/editspec/component.schema.json +0 -0
  111. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/editspec/custom-action.schema.json +0 -0
  112. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/editspec/envelope.schema.json +0 -0
  113. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/editspec/field.schema.json +0 -0
  114. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/editspec.py +0 -0
  115. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/errors.py +0 -0
  116. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/fieldlower.py +0 -0
  117. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/jsonschema_mini.py +0 -0
  118. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/models.py +0 -0
  119. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/ops.py +0 -0
  120. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/planner.py +0 -0
  121. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/editor/reader.py +0 -0
  122. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/field_normalizer.py +0 -0
  123. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/field_spec_adapter.py +0 -0
  124. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/filter_translator.py +0 -0
  125. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/flow_node.py +0 -0
  126. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/global_meta.py +0 -0
  127. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/group.py +0 -0
  128. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/guide.py +0 -0
  129. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/icon_index.py +0 -0
  130. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/instance.py +0 -0
  131. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/logger.py +0 -0
  132. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/optionset.py +0 -0
  133. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/page.py +0 -0
  134. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/record.py +0 -0
  135. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/response_crypto.py +0 -0
  136. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/role.py +0 -0
  137. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/role_perm_builder.py +0 -0
  138. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/session.py +0 -0
  139. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/token_crypto.py +0 -0
  140. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/update.py +0 -0
  141. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/upload.py +0 -0
  142. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/v3/__init__.py +0 -0
  143. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/v3/dispatcher.py +0 -0
  144. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/v3/render.py +0 -0
  145. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/v3/schema.py +0 -0
  146. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/view_spec_adapter.py +0 -0
  147. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/workflow.py +0 -0
  148. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/workflow_node_dsl.py +0 -0
  149. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/core/worksheet_templates.py +0 -0
  150. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/en/approval.md +0 -0
  151. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/en/chart.md +0 -0
  152. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/en/messages.md +0 -0
  153. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/en/overview.md +0 -0
  154. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/en/record.md +0 -0
  155. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/en/setup.md +0 -0
  156. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/en/skills.md +0 -0
  157. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/zh-Hans/approval.md +0 -0
  158. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/zh-Hans/chart.md +0 -0
  159. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/zh-Hans/messages.md +0 -0
  160. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/zh-Hans/overview.md +0 -0
  161. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/zh-Hans/record.md +0 -0
  162. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/zh-Hans/setup.md +0 -0
  163. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/guide/zh-Hans/skills.md +0 -0
  164. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/hap_cli.py +0 -0
  165. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/locale/__init__.py +0 -0
  166. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/locale/messages.schema.json +0 -0
  167. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/__init__.py +0 -0
  168. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/conftest.py +0 -0
  169. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_app_creator.py +0 -0
  170. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_app_editor.py +0 -0
  171. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_app_export_usage.py +0 -0
  172. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_approval_submit_fill_cli.py +0 -0
  173. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_attachment_share.py +0 -0
  174. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_auth_prerelease.py +0 -0
  175. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_bug_036430.py +0 -0
  176. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_bug_036750.py +0 -0
  177. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_chart.py +0 -0
  178. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_chat_attachment_send.py +0 -0
  179. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_cli_access.py +0 -0
  180. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_config_cmd.py +0 -0
  181. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_content_language.py +0 -0
  182. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_control_lookup_guard.py +0 -0
  183. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_core.py +0 -0
  184. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
  185. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_full_e2e.py +0 -0
  186. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_global_meta.py +0 -0
  187. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_guide.py +0 -0
  188. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_i18n.py +0 -0
  189. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_instance_history_cli.py +0 -0
  190. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_integration.py +0 -0
  191. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_integration_approval.py +0 -0
  192. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_integration_approval_actions.py +0 -0
  193. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_integration_calendar.py +0 -0
  194. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_integration_destructive.py +0 -0
  195. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_integration_misc.py +0 -0
  196. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_integration_post.py +0 -0
  197. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_integration_social.py +0 -0
  198. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_integration_v3.py +0 -0
  199. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_integration_workflow.py +0 -0
  200. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
  201. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_member_relation_array_wrap.py +0 -0
  202. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_multi_profile.py +0 -0
  203. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_no_workflow_unsupported.py +0 -0
  204. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
  205. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_org_id_cli.py +0 -0
  206. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_org_id_docs.py +0 -0
  207. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_parameter_conventions.py +0 -0
  208. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
  209. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
  210. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_record_field_alias.py +0 -0
  211. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
  212. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_record_write_fixes.py +0 -0
  213. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_select_add_option.py +0 -0
  214. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_select_rejects_relation_value.py +0 -0
  215. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_semantic_types_registry.py +0 -0
  216. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_update.py +0 -0
  217. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_update_mode.py +0 -0
  218. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_upload_host_resolution.py +0 -0
  219. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
  220. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_v3_dispatcher.py +0 -0
  221. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
  222. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_v3_session.py +0 -0
  223. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
  224. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_workflow_envelope_errors.py +0 -0
  225. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_workflow_list_cli.py +0 -0
  226. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_workflow_read_write_keys.py +0 -0
  227. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
  228. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_worksheet_create_section.py +0 -0
  229. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
  230. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
  231. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/utils/__init__.py +0 -0
  232. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/utils/formatting.py +0 -0
  233. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/utils/options.py +0 -0
  234. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli/utils/parameter_mapping.py +0 -0
  235. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli.egg-info/dependency_links.txt +0 -0
  236. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli.egg-info/entry_points.txt +0 -0
  237. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli.egg-info/requires.txt +0 -0
  238. {hap_cli-0.8.27 → hap_cli-0.8.29}/hap_cli.egg-info/top_level.txt +0 -0
  239. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/__init__.py +0 -0
  240. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/__main__.py +0 -0
  241. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/_wftest.py +0 -0
  242. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/charts.py +0 -0
  243. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/cleanup.py +0 -0
  244. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/compiler.py +0 -0
  245. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/config.py +0 -0
  246. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/errors.py +0 -0
  247. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/executor.py +0 -0
  248. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/fields.py +0 -0
  249. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/hap.py +0 -0
  250. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/recording/__init__.py +0 -0
  251. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/recording/console.py +0 -0
  252. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/recording/jsonl.py +0 -0
  253. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/recording/mirror.py +0 -0
  254. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/recording/report.py +0 -0
  255. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/schema.py +0 -0
  256. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/seed/__init__.py +0 -0
  257. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/seed/cli.py +0 -0
  258. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/seed/executor.py +0 -0
  259. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/seed/template.py +0 -0
  260. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/selftest.py +0 -0
  261. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/smoke.py +0 -0
  262. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/step.py +0 -0
  263. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/steps.py +0 -0
  264. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/store.py +0 -0
  265. {hap_cli-0.8.27 → hap_cli-0.8.29}/live_tests/workflow_dsl.py +0 -0
  266. {hap_cli-0.8.27 → hap_cli-0.8.29}/setup.cfg +0 -0
  267. {hap_cli-0.8.27 → hap_cli-0.8.29}/setup.py +0 -0
  268. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/INDEX.json +0 -0
  269. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
  270. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
  271. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/create_optionset.yaml +0 -0
  272. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/create_role.yaml +0 -0
  273. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/delete_optionset.yaml +0 -0
  274. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/delete_record.yaml +0 -0
  275. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/delete_role.yaml +0 -0
  276. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
  277. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_app_info.yaml +0 -0
  278. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
  279. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
  280. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
  281. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_record_details.yaml +0 -0
  282. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
  283. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_record_list.yaml +0 -0
  284. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_record_logs.yaml +0 -0
  285. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
  286. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_record_relations.yaml +0 -0
  287. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
  288. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_regions.yaml +0 -0
  289. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_role_details.yaml +0 -0
  290. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/get_role_list.yaml +0 -0
  291. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/knowledge_search.yaml +0 -0
  292. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
  293. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
  294. {hap_cli-0.8.27 → hap_cli-0.8.29}/sources/v3-api-schema/update_optionset.yaml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hap-cli
3
- Version: 0.8.27
3
+ Version: 0.8.29
4
4
  Summary: CLI harness for Mingdao HAP - Enterprise no-code platform
5
5
  Author: hap-cli
6
6
  License: Apache-2.0
@@ -1,3 +1,3 @@
1
1
  """CLI harness for Mingdao HAP (hap-cli)."""
2
2
 
3
- __version__ = "0.8.27"
3
+ __version__ = "0.8.29"
@@ -274,8 +274,11 @@ def chat_send_to_one(ctx, to, message, files, dry_run):
274
274
  \b
275
275
  hap chat send-to-one -t <accountId> -m "spec attached" -f ./spec.pdf
276
276
 
277
- Text and each file arrive as separate messages, in that order. You
278
- cannot message yourself — your own account is skipped.
277
+ Text and each file arrive as separate messages, in that order.
278
+
279
+ To message yourself, send to the file-transfer assistant with
280
+ -t file-transfer. Your own account id is not a valid target and is
281
+ skipped.
279
282
  """
280
283
  if not to:
281
284
  ctx.handle_error(click.UsageError(
@@ -302,9 +305,20 @@ def chat_send_to_one(ctx, to, message, files, dry_run):
302
305
  return
303
306
  try:
304
307
  session = ctx.get_session()
308
+ # The file-transfer assistant is a conversation id, not an account
309
+ # id, so the account-addressed endpoint drops it silently. It goes
310
+ # over the socket path the chat panel uses instead.
311
+ socket_targets = [t for t in to if t == chat_mod.FILE_TRANSFER_ID]
312
+ account_targets = [t for t in to if t != chat_mod.FILE_TRANSFER_ID]
305
313
  if not sources:
306
- data = chat_mod.send_message(session, list(to), message)
307
- ctx.output(data, lambda d: click.echo("Message sent."))
314
+ results = {}
315
+ if account_targets:
316
+ results["accounts"] = chat_mod.send_message(
317
+ session, account_targets, message)
318
+ for target in socket_targets:
319
+ results[target] = chat_mod.send_user_message(
320
+ session, target, message)
321
+ ctx.output(results, lambda d: click.echo("Message sent."))
308
322
  return
309
323
  descriptors = chat_mod.upload_chat_files(session, sources)
310
324
  acks = chat_mod.send_files(session, list(to), descriptors, message)
@@ -430,3 +444,65 @@ def chat_card_detail(ctx, task, post, calendar, kcfile, worksheet, worksheetrow)
430
444
  except Exception as e:
431
445
  ctx.handle_error(e)
432
446
 
447
+
448
+
449
+ @chat.command("recall")
450
+ @click.argument("message_id")
451
+ @click.option("--group-id", "group_id", default="", help="Group id the message is in")
452
+ @click.option("--with-user", "with_user", default="",
453
+ help="Peer accountId for a 1-on-1 message")
454
+ @click.option("--time", "msg_time", default="",
455
+ help="Send time of the message. Looked up from the conversation "
456
+ "when omitted.")
457
+ @click.option("--as-admin", is_flag=True,
458
+ help="Recall as a group admin. Admins may recall another "
459
+ "member's message; ordinary recall only covers your own.")
460
+ @click.option("--dry-run", is_flag=True,
461
+ help="Show which message would be recalled, without recalling it.")
462
+ @pass_context
463
+ def chat_recall(ctx, message_id, group_id, with_user, msg_time, as_admin, dry_run):
464
+ """Recall a message you sent.
465
+
466
+ You can recall your own message within 5 minutes of sending it; a group
467
+ admin can recall another member's message with no time limit. A recall
468
+ cannot be undone.
469
+ """
470
+ if bool(group_id) == bool(with_user):
471
+ raise click.UsageError("Pass exactly one of --group-id or --with-user.")
472
+ try:
473
+ session = ctx.get_session()
474
+ found = None
475
+ if not msg_time:
476
+ # The recall payload carries the message's send time; look it
477
+ # up so the caller only needs the message id.
478
+ history = (
479
+ chat_mod.get_group_messages(session, group_id, num=50)
480
+ if group_id else
481
+ chat_mod.get_user_messages(session, with_user, num=50)
482
+ ) or []
483
+ for m in history:
484
+ if str(m.get("id")) == str(message_id):
485
+ found = m
486
+ msg_time = m.get("time", "")
487
+ break
488
+ if dry_run:
489
+ ctx.output(
490
+ {"messageId": message_id,
491
+ "groupId": group_id, "withUser": with_user,
492
+ "time": msg_time,
493
+ "preview": ((found or {}).get("msg") or {}).get("con"),
494
+ "recalled": False},
495
+ lambda d: click.echo(
496
+ f"Would recall {d['messageId']}"
497
+ + (f" ({d['preview']!r})" if d.get("preview") else "")),
498
+ )
499
+ return
500
+ admin_id = session.account_id if as_admin else ""
501
+ data = chat_mod.withdraw_message(
502
+ session, message_id,
503
+ group_id=group_id, to_user=with_user,
504
+ time=msg_time, admin_account_id=admin_id,
505
+ )
506
+ ctx.output(data, lambda d: click.echo("Message recalled."))
507
+ except Exception as e:
508
+ ctx.handle_error(e)
@@ -644,25 +644,74 @@ def post_get(ctx, post_id):
644
644
  help="Post type: 0=text, 3=link, 5=vote, 7=file",
645
645
  )
646
646
  @click.option(
647
- "--share-group",
647
+ "--to-group",
648
648
  "share_group_ids",
649
649
  multiple=True,
650
- help="Also share to this group id (repeatable)",
650
+ help="Audience: members of this group can see the post (repeatable). "
651
+ "Naming a group does not also publish to the organization.",
651
652
  )
652
653
  @click.option(
653
- "--share-org",
654
+ "--to-org",
654
655
  "share_org_ids",
655
656
  multiple=True,
656
- help=(
657
- "Share to this organization id (repeatable). If omitted, the post "
658
- "goes only to --org-id."
659
- ),
657
+ help="Audience: everyone in this organization can see the post "
658
+ "(repeatable). This is the company-wide feed.",
660
659
  )
660
+ @click.option("--share-group", "legacy_group_ids", multiple=True, hidden=True)
661
+ @click.option("--share-org", "legacy_org_ids", multiple=True, hidden=True)
662
+ @click.option("--card-to-group", "card_group_ids", multiple=True,
663
+ help="After publishing, also send a card linking to the post "
664
+ "into this group chat (repeatable). This is a chat "
665
+ "message and does not change who can see the post.")
666
+ @click.option("--dry-run", is_flag=True,
667
+ help="Show the audience and what would be sent, without publishing.")
661
668
  @pass_context
662
- def post_create(ctx, org_id, message, post_type, share_group_ids, share_org_ids):
663
- """Create a new post in the given organization."""
669
+ def post_create(ctx, org_id, message, post_type, share_group_ids, share_org_ids,
670
+ legacy_group_ids, legacy_org_ids, card_group_ids, dry_run):
671
+ """Create a new post in the given organization.
672
+
673
+ Audience is set with --to-group / --to-org and defaults to everyone in
674
+ --org-id when neither is given. Who can see a post, and sending a card
675
+ about it to a group chat, are covered in `hap guide post`.
676
+ """
664
677
  try:
678
+ share_group_ids = tuple(share_group_ids) + tuple(legacy_group_ids)
679
+ share_org_ids = tuple(share_org_ids) + tuple(legacy_org_ids)
680
+ if dry_run:
681
+ audience = (
682
+ [f"group:{g}" for g in share_group_ids]
683
+ + [f"org:{o}" for o in share_org_ids]
684
+ ) or [f"org:{org_id or '<current>'} (everyone, default)"]
685
+ ctx.output(
686
+ {"audience": audience, "cardToGroups": list(card_group_ids),
687
+ "message": message, "published": False},
688
+ lambda d: click.echo(
689
+ "Would publish to: " + ", ".join(d["audience"])
690
+ + (f"; card to groups: {', '.join(d['cardToGroups'])}"
691
+ if d["cardToGroups"] else "")),
692
+ )
693
+ return
665
694
  session = ctx.get_session()
695
+ # Only a group with the feed turned on can be a post audience. The
696
+ # server rejects the rest with "选择了没有加入的群组" (you picked a
697
+ # group you have not joined), which is misleading when you are in
698
+ # the group — it is a plain chat group with no feed. Check first so
699
+ # the message names the real reason.
700
+ if share_group_ids:
701
+ from hap_cli.core import group as group_mod
702
+ no_feed = []
703
+ for gid in share_group_ids:
704
+ info = group_mod.get_group_info(session, gid) or {}
705
+ if isinstance(info.get("data"), dict):
706
+ info = info["data"]
707
+ if not info.get("isPost"):
708
+ no_feed.append(info.get("name") or gid)
709
+ if no_feed:
710
+ raise click.ClickException(
711
+ "These groups cannot receive a post because they are chat "
712
+ "groups with no feed enabled: " + ", ".join(no_feed)
713
+ + ". Pick a group whose feed is on, or send a card to the "
714
+ "chat instead with --card-to-group.")
666
715
  pid = require_api_project_id(session, org_id)
667
716
  raw = post_mod.add_post(
668
717
  session,
@@ -674,10 +723,21 @@ def post_create(ctx, org_id, message, post_type, share_group_ids, share_org_ids)
674
723
  )
675
724
  post_obj = raw.get("post") if isinstance(raw, dict) else None
676
725
  summary = _simplify_post(post_obj) if post_obj else {"raw": raw}
726
+ if card_group_ids and summary.get("id"):
727
+ from hap_cli.core import chat as chat_mod
728
+ sent = []
729
+ for gid in card_group_ids:
730
+ chat_mod.send_group_post_card(
731
+ session, gid, summary["id"], message,
732
+ post_type=post_type)
733
+ sent.append(gid)
734
+ summary["cardSentToGroups"] = sent
677
735
  def _render(d):
678
736
  click.echo(f"Post created: {d.get('id')}")
679
737
  if d.get("text"):
680
738
  click.echo(f" {d['text']}")
739
+ if d.get("cardSentToGroups"):
740
+ click.echo(f" Card sent to: {', '.join(d['cardSentToGroups'])}")
681
741
  ctx.output(summary, _render)
682
742
  except Exception as e:
683
743
  ctx.handle_error(e)
@@ -919,24 +919,143 @@ def worksheet_delete_button(ctx, worksheet_id, btn_id, app_id, view_id, yes):
919
919
 
920
920
  @worksheet.command("switches")
921
921
  @click.argument("worksheet_id")
922
+ @click.option("--raw", is_flag=True,
923
+ help="Return the server's switch list unannotated.")
922
924
  @pass_context
923
- def worksheet_switches(ctx, worksheet_id):
924
- """Get feature switches for a worksheet."""
925
+ def worksheet_switches(ctx, worksheet_id, raw):
926
+ """Get feature switches for a worksheet.
927
+
928
+ Each switch is listed with the name and group it has on the feature
929
+ switch page, because several numbers share a name and only the group
930
+ tells them apart.
931
+ """
925
932
  try:
926
933
  session = ctx.get_session()
927
934
  data = ws_mod.get_switches(session, worksheet_id)
935
+ if not raw:
936
+ data = ws_mod.annotate_switches(data)
928
937
  ctx.output(data, lambda d: output_json(d))
929
938
  except Exception as e:
930
939
  ctx.handle_error(e)
931
940
 
932
941
 
942
+ @worksheet.command("switch-types")
943
+ @pass_context
944
+ def worksheet_switch_types(ctx):
945
+ """Show the feature switch numbers with their names and groups."""
946
+ rows = [ws_mod.describe_switch(t) for t in sorted(ws_mod.SWITCH_TYPES)]
947
+ ctx.output(
948
+ rows,
949
+ lambda d: output_table(
950
+ d, ["type", "group", "name", "path"],
951
+ ["Type", "Group", "Name", "Path"],
952
+ ),
953
+ )
954
+
955
+
956
+ def _parse_switch_changes(pairs):
957
+ changes = {}
958
+ for raw in pairs:
959
+ if "=" not in raw:
960
+ raise click.UsageError(
961
+ f"Invalid --set value {raw!r}. Use --set TYPE=true|false.")
962
+ key, value = raw.split("=", 1)
963
+ try:
964
+ stype = int(key.strip())
965
+ except ValueError:
966
+ raise click.UsageError(
967
+ f"Invalid switch number {key!r}. Run `hap worksheet "
968
+ "switch-types` for the list.")
969
+ val = value.strip().lower()
970
+ if val not in ("true", "false", "1", "0", "on", "off"):
971
+ raise click.UsageError(
972
+ f"Invalid state {value!r} for switch {stype}. Use true or false.")
973
+ changes[stype] = val in ("true", "1", "on")
974
+ return changes
975
+
976
+
977
+ @worksheet.command("batch-edit-switch")
978
+ @click.argument("worksheet_ids", nargs=-1, required=True)
979
+ @click.option("--set", "pairs", multiple=True, required=True,
980
+ help="Switch to change as TYPE=true|false (repeatable). "
981
+ "Numbers come from `hap worksheet switch-types`.")
982
+ @click.option("--dry-run", is_flag=True,
983
+ help="Show what would change on each worksheet, without saving.")
984
+ @pass_context
985
+ def worksheet_batch_edit_switch(ctx, worksheet_ids, pairs, dry_run):
986
+ """Change several feature switches on one or more worksheets.
987
+
988
+ The switches you do not name keep their current values. Turning the
989
+ 批量操作 or 统计 parent on or off carries its children with it. Each
990
+ worksheet is read back after saving, and any switch that did not take
991
+ is reported rather than counted as success.
992
+ """
993
+ try:
994
+ changes = _parse_switch_changes(pairs)
995
+ session = ctx.get_session()
996
+ if dry_run:
997
+ preview = [
998
+ {**ws_mod.describe_switch(t), "state": v}
999
+ for t, v in changes.items()
1000
+ ]
1001
+ ctx.output(
1002
+ {"worksheetIds": list(worksheet_ids), "changes": preview,
1003
+ "saved": False},
1004
+ lambda d: click.echo(
1005
+ f"Would set {len(preview)} switch(es) on "
1006
+ f"{len(worksheet_ids)} worksheet(s): "
1007
+ + ", ".join(f"{c['path'] or c['type']}={c['state']}"
1008
+ for c in preview)),
1009
+ )
1010
+ return
1011
+ results, failed_sheets = [], []
1012
+ for wid in worksheet_ids:
1013
+ try:
1014
+ res = ws_mod.batch_edit_switch(session, wid, changes)
1015
+ except Exception as exc: # keep going across worksheets
1016
+ results.append({"worksheetId": wid, "error": str(exc)})
1017
+ failed_sheets.append(wid)
1018
+ continue
1019
+ results.append(res)
1020
+ if res.get("failed"):
1021
+ failed_sheets.append(wid)
1022
+ payload = {"results": results,
1023
+ "worksheetsChanged": len(worksheet_ids) - len(failed_sheets),
1024
+ "worksheetsFailed": failed_sheets}
1025
+
1026
+ def _render(d):
1027
+ click.echo(f"Changed {d['worksheetsChanged']} of "
1028
+ f"{len(worksheet_ids)} worksheet(s).")
1029
+ for r in d["results"]:
1030
+ if r.get("error"):
1031
+ click.echo(f" {r['worksheetId']}: {r['error']}")
1032
+ elif r.get("failed"):
1033
+ for t, info in r["failed"].items():
1034
+ click.echo(f" {r['worksheetId']}: switch {t} is "
1035
+ f"{info['actual']!r}, wanted "
1036
+ f"{info['expected']!r}")
1037
+ ctx.output(payload, _render)
1038
+ if failed_sheets:
1039
+ raise click.ClickException(
1040
+ f"{len(failed_sheets)} worksheet(s) did not end up in the "
1041
+ "requested state: " + ", ".join(failed_sheets))
1042
+ except Exception as e:
1043
+ ctx.handle_error(e)
1044
+
1045
+
933
1046
  @worksheet.command("edit-switch")
934
1047
  @click.argument("worksheet_id")
935
1048
  @click.argument("switch_id", type=int)
936
1049
  @click.argument("value", type=bool)
937
1050
  @pass_context
938
1051
  def worksheet_edit_switch(ctx, worksheet_id, switch_id, value):
939
- """Edit a single feature switch."""
1052
+ """Turn one feature switch on or off.
1053
+
1054
+ SWITCH_ID is a switch number from `hap worksheet switch-types`. The
1055
+ new state is read back, so a change that did not take is reported as
1056
+ an error instead of success. To change several switches, or the same
1057
+ ones across many worksheets, use `worksheet batch-edit-switch`.
1058
+ """
940
1059
  try:
941
1060
  session = ctx.get_session()
942
1061
  data = ws_mod.edit_switch(session, worksheet_id, switch_id, value)
@@ -438,6 +438,159 @@ def send_group_message(
438
438
  return ack_data
439
439
 
440
440
 
441
+ # The file-transfer assistant (文件传输助手) is a real 1-on-1 conversation
442
+ # with a fixed pseudo-account id, not a shortcut for your own account
443
+ # (pd-openweb src/pages/chat/utils/constant.js `FILE_TRANSFER.id`). It is
444
+ # how the web client lets you message yourself.
445
+ FILE_TRANSFER_ID = "file-transfer"
446
+
447
+
448
+ def send_user_message(
449
+ session: Session,
450
+ to_user: str,
451
+ message: str,
452
+ ) -> Any:
453
+ """Send a 1-on-1 chat message over the socket, as the chat panel does.
454
+
455
+ ``send_message`` posts to the main-site ``Message.SendMessageToAccountIds``,
456
+ which addresses real *account ids* and silently drops anything else —
457
+ the caller's own id, and the file-transfer assistant, both come back
458
+ as ``successCount: 0`` with nothing stored.
459
+
460
+ The chat panel never uses that endpoint: it emits the socket.io event
461
+ ``'send message'`` with ``touser`` set to the conversation id
462
+ (``src/pages/chat/utils/socket.js`` ``Message.send`` USER branch;
463
+ ``containers/ChatPanelSession/index.js:152,334,357`` set
464
+ ``sendMsg.touser = session.id``). Because the target is a
465
+ conversation id rather than an account id, this path also reaches
466
+ :data:`FILE_TRANSFER_ID`, which is what "send to myself" means in the
467
+ web client.
468
+ """
469
+ import uuid
470
+
471
+ if not to_user:
472
+ raise ValueError("to_user is required")
473
+ if not message:
474
+ raise ValueError("message must not be empty")
475
+ payload = {
476
+ "type": MSGTYPE_TEXT,
477
+ "msg": message,
478
+ "touser": to_user,
479
+ "waitingid": str(uuid.uuid4()),
480
+ }
481
+ ack = _socket_emit(session, "send message", payload, strict=True)
482
+ if isinstance(ack, tuple) and len(ack) >= 2:
483
+ return ack[1]
484
+ return ack
485
+
486
+
487
+ def withdraw_message(
488
+ session: Session,
489
+ message_id: str,
490
+ *,
491
+ group_id: str = "",
492
+ to_user: str = "",
493
+ time: str = "",
494
+ admin_account_id: str = "",
495
+ ) -> Any:
496
+ """Recall (撤回) a message you sent, in a group or a 1-on-1 chat.
497
+
498
+ Recall has no REST endpoint — pd-openweb emits a socket.io event
499
+ (``src/pages/chat/utils/socket.js`` ``Message.sendWithdrawMessgae``):
500
+ ``'withdraw group message'`` for a group, ``'withdraw user message'``
501
+ for a 1-on-1. The parameter object is built by
502
+ ``containers/Message/index.js::handleWithdrawMessage`` as
503
+ ``{messageId, groupid | touser, time}`` — note the lowercase
504
+ ``groupid`` / ``touser`` keys — plus ``adminid`` when a group admin
505
+ recalls somebody else's message.
506
+
507
+ Server-side limits, surfaced by the UI rather than by us: an author
508
+ can only recall within 5 minutes of sending
509
+ (``components/Message/MessageToolbar/index.js:285``), while a group
510
+ admin may recall another member's message with no time limit
511
+ (``:275-276``). A recall past the window is refused by the server.
512
+
513
+ Exactly one of ``group_id`` / ``to_user`` identifies the conversation.
514
+ """
515
+ if not message_id:
516
+ raise ValueError("message_id is required")
517
+ if bool(group_id) == bool(to_user):
518
+ raise ValueError("pass exactly one of group_id or to_user")
519
+ param: dict[str, Any] = {"messageId": message_id, "time": time or ""}
520
+ if group_id:
521
+ param["groupid"] = group_id
522
+ event = "withdraw group message"
523
+ else:
524
+ param["touser"] = to_user
525
+ event = "withdraw user message"
526
+ if admin_account_id:
527
+ param["adminid"] = admin_account_id
528
+ ack = _socket_emit(session, event, param, strict=True)
529
+ if isinstance(ack, tuple) and len(ack) >= 2:
530
+ return ack[1]
531
+ return ack
532
+
533
+
534
+ def send_group_post_card(
535
+ session: Session,
536
+ group_id: str,
537
+ post_id: str,
538
+ title: str,
539
+ *,
540
+ post_type: int = 0,
541
+ server_url: str = "",
542
+ ) -> Any:
543
+ """Send a card linking to a post into a group chat.
544
+
545
+ This is a chat message about a post; it does not change who can see
546
+ the post itself (that is the post's own audience — see
547
+ ``hap_cli.core.post.add_post``).
548
+
549
+ Wire shape mirrors ``pd-openweb/src/pages/chat/utils/cardSender.js``
550
+ (``_initPost``): message ``type`` 5 (``Constant.MSGTYPE_CARD``) with a
551
+ ``card`` of ``{md, entityid, url, title, text}``, where ``md`` is
552
+ ``vote`` for a vote post (postType 7) and ``post`` otherwise, and the
553
+ url is ``/feeddetail?itemID=<postId>``. The title is capped at 300
554
+ characters, as the frontend does.
555
+
556
+ The ``msg`` line is the conversation-list summary, built the way
557
+ ``ChatPanelSession/index.js:277-278`` builds it: ``[<kind>] <title>``,
558
+ where the kind comes from the *localized* card name
559
+ (``cardDisposeName`` → ``_l('动态')`` / ``_l('投票')``). It is read by
560
+ other people in the chat, so it goes through the content-default
561
+ catalog rather than being hardcoded in one language.
562
+ """
563
+ import uuid
564
+
565
+ from hap_cli.i18n import content_default
566
+
567
+ if not group_id:
568
+ raise ValueError("group_id is required")
569
+ if not post_id:
570
+ raise ValueError("post_id is required")
571
+ card_title = (title or "")[:300]
572
+ is_vote = str(post_type) == "7"
573
+ kind = content_default("chat.vote" if is_vote else "chat.post")
574
+ base = (server_url or getattr(session, "server_url", "") or "").rstrip("/")
575
+ payload = {
576
+ "type": 5,
577
+ "msg": f"{kind} {card_title}",
578
+ "card": {
579
+ "md": "vote" if is_vote else "post",
580
+ "entityid": post_id,
581
+ "url": f"{base}/feeddetail?itemID={post_id}",
582
+ "title": card_title,
583
+ "text": "",
584
+ },
585
+ "togroup": group_id,
586
+ "waitingid": str(uuid.uuid4()),
587
+ }
588
+ ack = _socket_emit(session, "send group message", payload, strict=True)
589
+ if isinstance(ack, tuple) and len(ack) >= 2:
590
+ return ack[1]
591
+ return ack
592
+
593
+
441
594
  def send_group_files(
442
595
  session: Session,
443
596
  group_id: str,
@@ -207,19 +207,35 @@ def add_post(
207
207
  ) -> dict[str, Any]:
208
208
  """Create a post via ``Post.AddPost``.
209
209
 
210
- ``scope`` is a ``PostShareScopeReq`` object — not an int. The
211
- frontend (``pd-openweb/src/pages/feed/components/createFeed/index.js``)
212
- posts ``{radioProjectIds, shareGroupIds, shareProjectIds}``. When
213
- the caller doesn't pass ``scope`` we default to "share with the
214
- given org only", which matches the UI's default for org-wide posts.
210
+ ``scope`` is a ``PostShareScopeReq`` object — not an int — and it is
211
+ what decides **who can see the post**. The frontend
212
+ (``pd-openweb/src/pages/feed/components/createFeed/index.js``) posts
213
+ ``{radioProjectIds, shareGroupIds, shareProjectIds}``:
214
+
215
+ * ``shareProjectIds: [<orgId>]`` — everyone in that organization.
216
+ This is the org-wide feed (index.js:267-270).
217
+ * ``shareGroupIds: [<groupId>]`` with ``shareProjectIds: []`` — only
218
+ that group's members. Posting into a group builds exactly this
219
+ (index.js:647-650).
220
+
221
+ ``project_id`` is the organization the post belongs to and is always
222
+ sent as ``projectId``; on its own it is *not* an audience. Audience
223
+ defaults to the whole organization only when the caller names no
224
+ audience at all — naming a group must never silently widen the post
225
+ to everyone, which is what an unconditional ``or [project_id]``
226
+ fallback used to do.
215
227
 
216
228
  Returns the raw ``{post, success}`` envelope.
217
229
  """
218
230
  if scope is None:
231
+ groups = list(share_group_ids or [])
232
+ orgs = list(share_project_ids or [])
233
+ if not groups and not orgs:
234
+ orgs = [project_id]
219
235
  scope = {
220
236
  "radioProjectIds": radio_project_ids,
221
- "shareGroupIds": list(share_group_ids or []),
222
- "shareProjectIds": list(share_project_ids or [project_id]),
237
+ "shareGroupIds": groups,
238
+ "shareProjectIds": orgs,
223
239
  }
224
240
  return session.api_call(
225
241
  "Post", "AddPost",