netizen-cli 0.12.1__tar.gz → 0.12.2__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 (295) hide show
  1. {netizen_cli-0.12.1/netizen_cli.egg-info → netizen_cli-0.12.2}/PKG-INFO +2 -2
  2. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/__init__.py +1 -1
  3. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/queries.py +42 -37
  4. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/static/admin.js +114 -40
  5. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/static/index.html +11 -4
  6. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/web.py +25 -16
  7. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/bindings.py +89 -9
  8. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/callbacks.py +1 -0
  9. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/controls.py +144 -85
  10. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/reply.py +10 -4
  11. netizen_cli-0.12.2/netizen_cli/channel/reply_presenter.py +867 -0
  12. netizen_cli-0.12.2/netizen_cli/channel/session_pages.py +102 -0
  13. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/channel_app.py +458 -1383
  14. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/codex_runtime.py +497 -100
  15. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/domain.py +2 -0
  16. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/main.py +2 -0
  17. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/management/__init__.py +2 -2
  18. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/management/service.py +125 -364
  19. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/runtime/contracts.py +16 -0
  20. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/sdk_gap_adapter.py +58 -5
  21. netizen_cli-0.12.2/netizen_cli/session_inventory.py +142 -0
  22. netizen_cli-0.12.2/netizen_cli/sessions/delegation.py +29 -0
  23. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/sessions/presentation.py +27 -9
  24. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/sessions/schema.py +3 -3
  25. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/sessions/service.py +152 -76
  26. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/terminal_cleanup.py +2 -2
  27. netizen_cli-0.12.2/netizen_cli/thread_history.py +142 -0
  28. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/turn_plan_observer.py +2 -2
  29. {netizen_cli-0.12.1 → netizen_cli-0.12.2/netizen_cli.egg-info}/PKG-INFO +2 -2
  30. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli.egg-info/SOURCES.txt +9 -0
  31. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli.egg-info/requires.txt +1 -1
  32. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/pyproject.toml +2 -2
  33. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/references/admin.md +18 -12
  34. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/references/faq.md +2 -2
  35. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/references/feedback-and-files.md +13 -14
  36. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/references/fork-side-goal.md +18 -14
  37. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/references/messages.md +2 -1
  38. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/references/sessions.md +15 -6
  39. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_bindings.py +152 -0
  40. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_cards.py +32 -43
  41. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_channel_app.py +184 -2040
  42. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_codex_runtime.py +1299 -54
  43. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_codex_sdk_capabilities.py +1 -1
  44. netizen_cli-0.12.2/tests/test_codex_sdk_upgrade_0163.py +78 -0
  45. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_completion_mention_cards.py +43 -70
  46. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_completion_mentions.py +149 -196
  47. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_deployment.py +1 -1
  48. netizen_cli-0.12.2/tests/test_goal_reply_separation.py +268 -0
  49. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_main.py +3 -0
  50. netizen_cli-0.12.2/tests/test_partial_answer_delivery.py +716 -0
  51. netizen_cli-0.12.2/tests/test_probe_sdk_response_recovery.py +75 -0
  52. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_question_channel.py +5 -7
  53. netizen_cli-0.12.2/tests/test_resume_channel.py +177 -0
  54. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_scheduled_channel.py +60 -4
  55. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_scheduler_presenter.py +46 -15
  56. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_sdk_gap_adapter.py +73 -1
  57. netizen_cli-0.12.2/tests/test_session_card_pages.py +182 -0
  58. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_session_collaboration_app.py +35 -10
  59. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_session_id_cards.py +87 -6
  60. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_session_tools.py +213 -15
  61. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_terminal_cleanup.py +2 -2
  62. netizen_cli-0.12.2/tests/test_thread_history.py +186 -0
  63. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_turn_plan_observer.py +1 -1
  64. netizen_cli-0.12.1/netizen_cli/channel/reply_presenter.py +0 -1655
  65. netizen_cli-0.12.1/tests/test_partial_answer_delivery.py +0 -487
  66. netizen_cli-0.12.1/tests/test_resume_channel.py +0 -91
  67. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/MANIFEST.in +0 -0
  68. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/README.md +0 -0
  69. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/_build.py +0 -0
  70. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/config.example.yaml +0 -0
  71. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/__main__.py +0 -0
  72. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/account_rate_limits.py +0 -0
  73. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/__init__.py +0 -0
  74. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/auth.py +0 -0
  75. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/credentials.py +0 -0
  76. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/errors.py +0 -0
  77. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/feishu.py +0 -0
  78. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/login.py +0 -0
  79. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/port_config.py +0 -0
  80. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/presentation.py +0 -0
  81. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/static/admin.css +0 -0
  82. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/static/chat-picker.css +0 -0
  83. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/static/chat-picker.js +0 -0
  84. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/static/identity.js +0 -0
  85. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/static/login.html +0 -0
  86. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/static/login.js +0 -0
  87. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/static/session-access.js +0 -0
  88. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/transport.py +0 -0
  89. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/admin/users.py +0 -0
  90. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/builtin_skills.py +0 -0
  91. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/__init__.py +0 -0
  92. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/chat_target.py +0 -0
  93. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/defaults.py +0 -0
  94. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/fork.py +0 -0
  95. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/model_info.py +0 -0
  96. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/pagination.py +0 -0
  97. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/questions.py +0 -0
  98. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/scheduled.py +0 -0
  99. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/session_ids.py +0 -0
  100. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/status.py +0 -0
  101. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cards/usage.py +0 -0
  102. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/channel/__init__.py +0 -0
  103. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/channel/completion_mentions.py +0 -0
  104. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/channel/input_preparation.py +0 -0
  105. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/channel/messages.py +0 -0
  106. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/channel/ports.py +0 -0
  107. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/channel/question_inputs.py +0 -0
  108. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/channel/reactions.py +0 -0
  109. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/channel/topics.py +0 -0
  110. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/chat_avatars.py +0 -0
  111. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/chat_targets.py +0 -0
  112. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cli.py +0 -0
  113. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cli_data.py +0 -0
  114. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cli_packages.py +0 -0
  115. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cli_services.py +0 -0
  116. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cli_setup.py +0 -0
  117. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cli_update.py +0 -0
  118. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cli_update_restore.py +0 -0
  119. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/cli_update_worker.py +0 -0
  120. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/completion_mention.py +0 -0
  121. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/database_migrations.py +0 -0
  122. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/defaults/__init__.py +0 -0
  123. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/defaults/models.py +0 -0
  124. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/defaults/service.py +0 -0
  125. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/defaults/store.py +0 -0
  126. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/deployment/__init__.py +0 -0
  127. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/deployment/restart_worker.py +0 -0
  128. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/deployment/update_executor.py +0 -0
  129. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/deployment/update_protocol.py +0 -0
  130. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/error_messages.py +0 -0
  131. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/experience.py +0 -0
  132. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/feishu_app_onboarding.py +0 -0
  133. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/feishu_app_permissions.py +0 -0
  134. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/feishu_links.py +0 -0
  135. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/git_status.py +0 -0
  136. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/image_inputs.py +0 -0
  137. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/instance.py +0 -0
  138. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/lark_app.py +0 -0
  139. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/management/blocking_io.py +0 -0
  140. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/management/chat_directory.py +0 -0
  141. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/management/chat_labels.py +0 -0
  142. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/management/coordination.py +0 -0
  143. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/management/updates.py +0 -0
  144. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/markdown_images.py +0 -0
  145. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/mcp_transport.py +0 -0
  146. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/message_content.py +0 -0
  147. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/message_history.py +0 -0
  148. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/message_preparation.py +0 -0
  149. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/message_projection.py +0 -0
  150. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/migrations/__init__.py +0 -0
  151. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/migrations/schema.py +0 -0
  152. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/migrations/v14.py +0 -0
  153. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/model_settings.py +0 -0
  154. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/package_resources.py +0 -0
  155. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/pagination.py +0 -0
  156. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/partial_answers.py +0 -0
  157. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/projects.py +0 -0
  158. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/prompt_projection.py +0 -0
  159. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/quoted_context.py +0 -0
  160. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/result_images.py +0 -0
  161. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/runtime/__init__.py +0 -0
  162. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/runtime/name_writes.py +0 -0
  163. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/runtime/thread_naming.py +0 -0
  164. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/schedules/__init__.py +0 -0
  165. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/schedules/mcp.py +0 -0
  166. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/schedules/models.py +0 -0
  167. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/schedules/scheduler.py +0 -0
  168. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/schedules/service.py +0 -0
  169. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/schedules/store.py +0 -0
  170. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/service_launcher.py +0 -0
  171. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/session_settings.py +0 -0
  172. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/sessions/__init__.py +0 -0
  173. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/sessions/errors.py +0 -0
  174. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/sessions/mcp.py +0 -0
  175. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/sessions/policy.py +0 -0
  176. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/settings.py +0 -0
  177. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/skill_references.py +0 -0
  178. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/task_text.py +0 -0
  179. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/turn_activity.py +0 -0
  180. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/turn_files.py +0 -0
  181. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/turn_patch_children.py +0 -0
  182. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli/user_questions.py +0 -0
  183. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli.egg-info/dependency_links.txt +0 -0
  184. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli.egg-info/entry_points.txt +0 -0
  185. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/netizen_cli.egg-info/top_level.txt +0 -0
  186. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/setup.cfg +0 -0
  187. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-lark/SKILL.md +0 -0
  188. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/SKILL.md +0 -0
  189. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/references/commands.md +0 -0
  190. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/references/concepts.md +0 -0
  191. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/references/native-capabilities.md +0 -0
  192. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/references/scheduled-tasks.md +0 -0
  193. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/references/session-collaboration.md +0 -0
  194. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/references/status-and-control.md +0 -0
  195. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/skills/netizen-user-guide/references/user-guide.md +0 -0
  196. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_account_rate_limits.py +0 -0
  197. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_admin_update_executor_probe.py +0 -0
  198. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_binding_scheduled_channel.py +0 -0
  199. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_channel_fixtures.py +0 -0
  200. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_channel_input_preparation.py +0 -0
  201. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_chat_avatars.py +0 -0
  202. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_chat_target_cards.py +0 -0
  203. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_check_sdk.py +0 -0
  204. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_cli.py +0 -0
  205. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_cli_data.py +0 -0
  206. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_cli_distribution.py +0 -0
  207. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_cli_integration.py +0 -0
  208. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_cli_packages.py +0 -0
  209. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_cli_packaging.py +0 -0
  210. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_cli_services.py +0 -0
  211. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_cli_setup.py +0 -0
  212. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_cli_update.py +0 -0
  213. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_codex_sdk_upgrade_0161.py +0 -0
  214. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_completion_topic_delivery.py +0 -0
  215. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_cron_chat_targets.py +0 -0
  216. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_database_migrations.py +0 -0
  217. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_defaults_cards.py +0 -0
  218. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_defaults_channel.py +0 -0
  219. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_defaults_runtime.py +0 -0
  220. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_documentation_links.py +0 -0
  221. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_domain_experience.py +0 -0
  222. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_error_messages.py +0 -0
  223. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_feishu_app_onboarding.py +0 -0
  224. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_feishu_app_permissions.py +0 -0
  225. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_file_pagination.py +0 -0
  226. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_fork_cards.py +0 -0
  227. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_git_status.py +0 -0
  228. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_image_inputs.py +0 -0
  229. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_lark_app.py +0 -0
  230. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_lark_sdk_contract.py +0 -0
  231. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_markdown_images.py +0 -0
  232. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_message_content.py +0 -0
  233. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_message_history.py +0 -0
  234. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_message_material_inputs.py +0 -0
  235. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_message_preparation.py +0 -0
  236. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_migration_schema_validation.py +0 -0
  237. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_model_info_cards.py +0 -0
  238. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_model_settings.py +0 -0
  239. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_name_writes.py +0 -0
  240. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_pagination.py +0 -0
  241. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_partial_answers.py +0 -0
  242. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_persistent_fork_bindings.py +0 -0
  243. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_persistent_fork_channel.py +0 -0
  244. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_persistent_fork_runtime.py +0 -0
  245. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_probe_child_files.py +0 -0
  246. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_probe_feishu_completion_mention.py +0 -0
  247. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_probe_feishu_partial_answers.py +0 -0
  248. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_probe_feishu_turn_file_card.py +0 -0
  249. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_probe_persistent_fork.py +0 -0
  250. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_probe_python_sdk.py +0 -0
  251. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_probe_scheduled_tasks.py +0 -0
  252. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_probe_sdk_completion_race.py +0 -0
  253. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_probe_skill_execution.py +0 -0
  254. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_probe_thread_naming.py +0 -0
  255. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_project_delete_channel.py +0 -0
  256. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_project_delete_runtime.py +0 -0
  257. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_project_deletion_storage.py +0 -0
  258. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_prompt_projection.py +0 -0
  259. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_question_cards.py +0 -0
  260. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_question_target_parity.py +0 -0
  261. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_quoted_context.py +0 -0
  262. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_release_script.py +0 -0
  263. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_repository_hygiene.py +0 -0
  264. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_result_images.py +0 -0
  265. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_retired_deployment.py +0 -0
  266. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_runtime_resume_activation.py +0 -0
  267. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_runtime_thread_naming.py +0 -0
  268. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_schedule_card_retry.py +0 -0
  269. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_schedule_cards.py +0 -0
  270. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_schedule_mcp.py +0 -0
  271. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_schedule_run_now.py +0 -0
  272. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_schedule_scope_matrix.py +0 -0
  273. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_schedule_service.py +0 -0
  274. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_scheduled_runtime.py +0 -0
  275. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_scheduler.py +0 -0
  276. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_schedules_storage.py +0 -0
  277. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_service_launcher.py +0 -0
  278. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_session_defaults.py +0 -0
  279. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_session_external_runtime.py +0 -0
  280. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_session_policy.py +0 -0
  281. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_session_settings.py +0 -0
  282. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_settings_projects.py +0 -0
  283. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_status_cards.py +0 -0
  284. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_status_commands.py +0 -0
  285. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_task_text.py +0 -0
  286. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_thread_naming.py +0 -0
  287. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_turn_activity.py +0 -0
  288. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_turn_files.py +0 -0
  289. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_turn_patch_children.py +0 -0
  290. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_turn_patch_statistics.py +0 -0
  291. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_update_executor.py +0 -0
  292. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_usage_card.py +0 -0
  293. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_usage_control.py +0 -0
  294. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_user_guide_skill.py +0 -0
  295. {netizen_cli-0.12.1 → netizen_cli-0.12.2}/tests/test_user_questions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netizen-cli
3
- Version: 0.12.1
3
+ Version: 0.12.2
4
4
  Summary: Use native Codex threads through a Feishu Channel.
5
5
  Requires-Python: <3.15,>=3.11
6
6
  Requires-Dist: h11==0.16.0
@@ -11,7 +11,7 @@ Requires-Dist: markdown-it-py==4.0.0
11
11
  Requires-Dist: mdformat==1.0.0
12
12
  Requires-Dist: mdformat-gfm==1.0.0
13
13
  Requires-Dist: mcp==2.2.0
14
- Requires-Dist: openai-codex==0.162.1
14
+ Requires-Dist: openai-codex==0.163.0
15
15
  Requires-Dist: pydantic==2.13.4
16
16
  Requires-Dist: PyYAML==6.0.3
17
17
  Requires-Dist: qrcode==8.2
@@ -1,3 +1,3 @@
1
1
  """Netizen: a thin Feishu Channel for native Codex Threads."""
2
2
 
3
- __version__ = "0.12.1"
3
+ __version__ = "0.12.2"
@@ -12,7 +12,7 @@ from datetime import UTC, datetime
12
12
  from .errors import AdminWebError
13
13
  from ..bindings import BindingCursor, SideTopicCursor
14
14
  from ..domain import ScopeKind
15
- from ..management import SessionInventoryState, SessionUpdatedCursor
15
+ from ..management import SessionInventoryState, SessionNativeCursor
16
16
 
17
17
 
18
18
  _MAX_TEXT_BYTES = 4_096
@@ -113,22 +113,34 @@ def _text_set_query(
113
113
  def _created_range_query(
114
114
  values: Mapping[str, list[str]],
115
115
  ) -> tuple[str | None, str | None]:
116
- created_from = _optional_created_time_query(values, "createdFrom")
117
- created_before = _optional_created_time_query(values, "createdBefore")
116
+ return _time_range_query(values, "created", "创建")
117
+
118
+
119
+ def _updated_range_query(
120
+ values: Mapping[str, list[str]],
121
+ ) -> tuple[str | None, str | None]:
122
+ return _time_range_query(values, "updated", "更新")
123
+
124
+
125
+ def _time_range_query(
126
+ values: Mapping[str, list[str]], prefix: str, label: str,
127
+ ) -> tuple[str | None, str | None]:
128
+ lower = _optional_time_query(values, f"{prefix}From")
129
+ upper = _optional_time_query(values, f"{prefix}Before")
118
130
  if (
119
- created_from is not None
120
- and created_before is not None
121
- and created_from >= created_before
131
+ lower is not None
132
+ and upper is not None
133
+ and lower >= upper
122
134
  ):
123
135
  raise AdminWebError(
124
136
  400,
125
137
  "invalid_time_range",
126
- "创建时间的开始时间必须早于结束时间。",
138
+ f"{label}时间的开始时间必须早于结束时间。",
127
139
  )
128
- return created_from, created_before
140
+ return lower, upper
129
141
 
130
142
 
131
- def _optional_created_time_query(
143
+ def _optional_time_query(
132
144
  values: Mapping[str, list[str]],
133
145
  name: str,
134
146
  ) -> str | None:
@@ -190,27 +202,17 @@ def _current_query(values: Mapping[str, list[str]]) -> bool | None:
190
202
  return raw[0] == "true" if len(raw) == 1 else None
191
203
 
192
204
 
193
- def _session_inventory_states(
205
+ def _session_inventory_state(
194
206
  values: Mapping[str, list[str]],
195
- ) -> tuple[SessionInventoryState, ...] | None:
196
- raw = _text_set_query(values, "inventoryState")
197
- if raw is None:
198
- return (
199
- SessionInventoryState.ACTIVE,
200
- SessionInventoryState.LAZY,
201
- SessionInventoryState.UNKNOWN,
202
- )
203
- if raw == ("all",):
204
- return None
205
- try:
206
- states = tuple(SessionInventoryState(value) for value in raw)
207
- except ValueError:
207
+ ) -> SessionInventoryState:
208
+ raw = _optional_one(values, "inventoryState") or "active"
209
+ if raw not in {"active", "archived", "lazy"}:
208
210
  raise AdminWebError(
209
211
  400,
210
212
  "invalid_inventory_state",
211
213
  "会话状态无效。",
212
- ) from None
213
- return None if set(states) == set(SessionInventoryState) else states
214
+ )
215
+ return SessionInventoryState(raw)
214
216
 
215
217
 
216
218
  def _id_query(values: Mapping[str, list[str]], name: str) -> tuple[str, ...]:
@@ -308,13 +310,14 @@ def _decode_binding_cursor(
308
310
 
309
311
 
310
312
  def _encode_session_cursor(
311
- cursor: BindingCursor | SessionUpdatedCursor | None,
313
+ cursor: BindingCursor | SessionNativeCursor | None,
312
314
  fingerprint: str,
313
315
  ) -> str | None:
314
- if not isinstance(cursor, SessionUpdatedCursor):
316
+ if not isinstance(cursor, SessionNativeCursor):
315
317
  return _encode_binding_cursor(cursor, fingerprint)
318
+ marker = "start" if cursor.cursor is None else "next"
316
319
  return _encode_cursor(
317
- "session-updated", (str(cursor.updated_at) if cursor.updated_at is not None else "unknown", cursor.binding_id), fingerprint,
320
+ "session-native", (marker, cursor.cursor if cursor.cursor is not None else "start", str(cursor.offset)), fingerprint,
318
321
  )
319
322
 
320
323
 
@@ -322,19 +325,21 @@ def _decode_session_cursor(
322
325
  encoded: str | None,
323
326
  fingerprint: str,
324
327
  *,
325
- sort_by: str,
326
- ) -> BindingCursor | SessionUpdatedCursor | None:
327
- if sort_by == "created_at":
328
+ inventory_state: SessionInventoryState,
329
+ ) -> BindingCursor | SessionNativeCursor | None:
330
+ if inventory_state is SessionInventoryState.LAZY:
328
331
  return _decode_binding_cursor(encoded, fingerprint)
329
- values = _decode_cursor(encoded, kind="session-updated", length=2, fingerprint=fingerprint)
332
+ values = _decode_cursor(encoded, kind="session-native", length=3, fingerprint=fingerprint)
330
333
  if values is None:
331
334
  return None
332
- raw_time, binding_id = values
333
- if raw_time == "unknown":
334
- return SessionUpdatedCursor(None, binding_id)
335
- if not re.fullmatch(r"0|[1-9][0-9]{0,15}", raw_time):
335
+ marker, raw_cursor, raw_offset = values
336
+ if (
337
+ marker not in {"start", "next"}
338
+ or marker == "start" and raw_cursor != "start"
339
+ or re.fullmatch(r"0|[1-9][0-9]?", raw_offset) is None
340
+ ):
336
341
  raise AdminWebError(400, "invalid_cursor", "分页游标无效。")
337
- return SessionUpdatedCursor(int(raw_time), binding_id)
342
+ return SessionNativeCursor(None if marker == "start" else raw_cursor, int(raw_offset))
338
343
 
339
344
 
340
345
  def _encode_side_cursor(
@@ -541,11 +541,6 @@ async function loadProjects(cursor = undefined) {
541
541
  ));
542
542
  row.append(status);
543
543
  cell(row, `${project.bindingCount}(Lazy ${project.lazyBindingCount})`);
544
- cell(row, project.archivedBindingCount == null
545
- ? "未确认"
546
- : project.unconfirmedBindingCount > 0
547
- ? `已确认 ${project.archivedBindingCount};另有 ${project.unconfirmedBindingCount} 个会话状态未确认`
548
- : project.archivedBindingCount);
549
544
  cell(row, project.lastActivatedAt);
550
545
  const actions = actionsCell(row);
551
546
  if (project.actions.setEnabled) {
@@ -578,7 +573,6 @@ let openFilter = null;
578
573
  const filterOptions = {
579
574
  project: [],
580
575
  scopeKind: [["direct", "单聊"], ["group", "群聊"], ["topic", "话题"]],
581
- inventoryState: [["active", "Active"], ["lazy", "Lazy"], ["unknown", "状态未确认"], ["archived", "Archived"], ["missing", "Missing"]],
582
576
  current: [["true", "当前"], ["false", "非当前"]],
583
577
  state: [["creating", "创建中"], ["open", "存活"], ["closed", "已结束"], ["expired", "已失效"], ["failed", "失败"]],
584
578
  enabled: [["true", "已启用"], ["false", "已暂停"]],
@@ -796,8 +790,7 @@ function initializeFilter(root) {
796
790
  const single = Boolean(root.dataset.scheduleFilter);
797
791
  const prefix = single ? "schedule" : root.dataset.sideMulti ? "side" : "session";
798
792
  const valueField = single ? document.querySelector(`#schedule-filter-${name}`) : null;
799
- const defaults = name === "inventoryState" ? ["active", "lazy", "unknown"]
800
- : name === "state" ? ["creating", "open"] : name === "ended" ? ["false"] : [];
793
+ const defaults = name === "state" ? ["creating", "open"] : name === "ended" ? ["false"] : [];
801
794
  const legend = root.querySelector("legend");
802
795
  legend.id = `${prefix}-${name}-label`;
803
796
  const trigger = document.createElement("button");
@@ -1034,7 +1027,17 @@ function formQuery(form, defaultPageSize = "25", refreshRelativeTime = true) {
1034
1027
  const filters = root.dataset.sideMulti ? sideMultiFilters : sessionMultiFilters;
1035
1028
  const values = filters.get(name).values();
1036
1029
  for (const value of values) query.append(name, value);
1037
- if (["inventoryState", "state"].includes(name) && values.length === 0) query.set(name, "all");
1030
+ if (name === "state" && values.length === 0) query.set(name, "all");
1031
+ }
1032
+ if (form.id === "session-filter") {
1033
+ // Exact links remain targeted while the user changes state or date filters.
1034
+ const identity = new URLSearchParams(state.sessionPage?.query || "").get("identity");
1035
+ if (identity) query.set("identity", identity);
1036
+ if (query.get("inventoryState") === "lazy") {
1037
+ for (const key of ["createdFrom", "createdBefore", "updatedFrom", "updatedBefore"]) query.delete(key);
1038
+ } else if (!query.has("updatedFrom") && !query.has("updatedBefore")) {
1039
+ query.set("updatedRange", "all"); // URL intent only; never sent to the API.
1040
+ }
1038
1041
  }
1039
1042
  if (!query.has("pageSize")) query.set("pageSize", defaultPageSize);
1040
1043
  return query;
@@ -1054,6 +1057,7 @@ const timeRangePresetLabels = {
1054
1057
  "last-24-hours": "最近 24 小时",
1055
1058
  "last-7-days": "最近 7 天",
1056
1059
  "last-30-days": "最近 30 天",
1060
+ "last-3-months": "最近 3 个月",
1057
1061
  };
1058
1062
 
1059
1063
  function twoDigits(value) {
@@ -1178,7 +1182,7 @@ function allTimeRange() {
1178
1182
  };
1179
1183
  }
1180
1184
 
1181
- function presetTimeRange(kind, now = new Date()) {
1185
+ function presetTimeRange(kind, now = new Date(), openEnded = false) {
1182
1186
  if (kind === "all") return allTimeRange();
1183
1187
  let from;
1184
1188
  let before;
@@ -1188,6 +1192,14 @@ function presetTimeRange(kind, now = new Date()) {
1188
1192
  } else if (kind === "yesterday") {
1189
1193
  from = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
1190
1194
  before = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1195
+ } else if (kind === "last-3-months") {
1196
+ before = new Date(now.getTime());
1197
+ from = new Date(now.getTime());
1198
+ const day = from.getDate();
1199
+ from.setDate(1);
1200
+ from.setMonth(from.getMonth() - 3);
1201
+ const lastDay = new Date(from.getFullYear(), from.getMonth() + 1, 0).getDate();
1202
+ from.setDate(Math.min(day, lastDay));
1191
1203
  } else {
1192
1204
  const durationDays = {
1193
1205
  "last-24-hours": 1,
@@ -1199,13 +1211,14 @@ function presetTimeRange(kind, now = new Date()) {
1199
1211
  from = new Date(now.getTime() - durationDays * 24 * 60 * 60 * 1000);
1200
1212
  }
1201
1213
  const label = timeRangePresetLabels[kind];
1202
- const summary = kind.startsWith("last-")
1214
+ const rolling = kind.startsWith("last-");
1215
+ const summary = rolling && !openEnded
1203
1216
  ? `${label} · 至 ${compactLocalMinute(before)} · ${utcOffsetLabel(before)}`
1204
1217
  : `${label} · ${utcOffsetLabel(from)}`;
1205
1218
  return {
1206
1219
  kind,
1207
1220
  from: canonicalUtc(from),
1208
- before: canonicalUtc(before),
1221
+ before: openEnded && rolling ? "" : canonicalUtc(before),
1209
1222
  startValue: localMinuteValue(from),
1210
1223
  endValue: localMinuteValue(new Date(before.getTime() - 60 * 1000)),
1211
1224
  summary,
@@ -1252,6 +1265,11 @@ function initializeTimeRangeFilter(root) {
1252
1265
  }
1253
1266
  root.append(timeRangeTemplate.content.cloneNode(true));
1254
1267
  const identity = root.dataset.timeRangeId;
1268
+ const field = root.dataset.timeRangeField || "created";
1269
+ const label = root.querySelector("legend").textContent;
1270
+ const defaultKind = root.dataset.timeRangeDefault || "all";
1271
+ // Rolling update filters include later updates when this applied page is refreshed.
1272
+ const preset = (kind) => presetTimeRange(kind, new Date(), field === "updated");
1255
1273
  const fromNode = root.querySelector("[data-time-range-from]");
1256
1274
  const beforeNode = root.querySelector("[data-time-range-before]");
1257
1275
  const trigger = root.querySelector("[data-time-range-trigger]");
@@ -1270,6 +1288,11 @@ function initializeTimeRangeFilter(root) {
1270
1288
  const clear = root.querySelector("[data-time-range-clear]");
1271
1289
  const presetButtons = [...root.querySelectorAll("[data-time-range-preset]")];
1272
1290
 
1291
+ fromNode.name = `${field}From`;
1292
+ beforeNode.name = `${field}Before`;
1293
+ title.textContent = label;
1294
+ quickClear.setAttribute("aria-label", `清除${label}筛选`);
1295
+ quickClear.title = `清除${label}筛选`;
1273
1296
  popover.id = `${identity}-popover`;
1274
1297
  title.id = `${identity}-title`;
1275
1298
  help.id = `${identity}-help`;
@@ -1282,8 +1305,8 @@ function initializeTimeRangeFilter(root) {
1282
1305
  start.setAttribute("aria-describedby", `${help.id} ${zone.id} ${error.id}`);
1283
1306
  end.setAttribute("aria-describedby", `${help.id} ${zone.id} ${error.id}`);
1284
1307
 
1285
- let applied = allTimeRange();
1286
- let draft = allTimeRange();
1308
+ let applied = preset(defaultKind);
1309
+ let draft = { ...applied };
1287
1310
 
1288
1311
  function setInputValidity(invalid) {
1289
1312
  for (const [node, name] of [[start, "start"], [end, "end"]]) {
@@ -1297,7 +1320,7 @@ function initializeTimeRangeFilter(root) {
1297
1320
  beforeNode.value = applied.before;
1298
1321
  summary.textContent = applied.summary;
1299
1322
  trigger.classList.toggle("has-value", applied.kind !== "all");
1300
- trigger.setAttribute("aria-label", `创建时间:${applied.summary}`);
1323
+ trigger.setAttribute("aria-label", `${label}:${applied.summary}`);
1301
1324
  quickClear.hidden = applied.kind === "all";
1302
1325
  }
1303
1326
 
@@ -1383,7 +1406,7 @@ function initializeTimeRangeFilter(root) {
1383
1406
 
1384
1407
  function prepareQuery() {
1385
1408
  if (applied.kind === "all" || applied.kind === "custom") return;
1386
- applied = presetTimeRange(applied.kind);
1409
+ applied = preset(applied.kind);
1387
1410
  renderApplied();
1388
1411
  }
1389
1412
 
@@ -1408,7 +1431,7 @@ function initializeTimeRangeFilter(root) {
1408
1431
  cancel: cancelDraft,
1409
1432
  prepareQuery,
1410
1433
  setBounds(from, before) {
1411
- if (!from && !before) { this.reset(); return; }
1434
+ if (!from && !before) { this.reset("all"); return; }
1412
1435
  const startDate = new Date(from);
1413
1436
  const beforeDate = new Date(before);
1414
1437
  const endDate = new Date(beforeDate.getTime() - 60000);
@@ -1420,9 +1443,9 @@ function initializeTimeRangeFilter(root) {
1420
1443
  closePopover({ restoreFocus: false });
1421
1444
  renderApplied();
1422
1445
  },
1423
- reset() {
1424
- applied = allTimeRange();
1425
- draft = allTimeRange();
1446
+ reset(kind = defaultKind) {
1447
+ applied = preset(kind);
1448
+ draft = { ...applied };
1426
1449
  closePopover({ restoreFocus: false });
1427
1450
  renderApplied();
1428
1451
  },
@@ -1441,7 +1464,7 @@ function initializeTimeRangeFilter(root) {
1441
1464
  });
1442
1465
  for (const button of presetButtons) {
1443
1466
  button.addEventListener("click", () => {
1444
- draft = presetTimeRange(button.dataset.timeRangePreset);
1467
+ draft = preset(button.dataset.timeRangePreset);
1445
1468
  renderDraft();
1446
1469
  });
1447
1470
  }
@@ -1453,7 +1476,10 @@ function initializeTimeRangeFilter(root) {
1453
1476
  });
1454
1477
  cancel.addEventListener("click", () => cancelDraft());
1455
1478
  done.addEventListener("click", () => {
1456
- if (draft.kind === "custom" && !validateDraft()) {
1479
+ // A restored URL may have only one bound. Confirming it unchanged must
1480
+ // preserve that valid range; edited custom input still requires both dates.
1481
+ const unchanged = start.value === applied.startValue && end.value === applied.endValue;
1482
+ if (draft.kind === "custom" && !unchanged && !validateDraft()) {
1457
1483
  (start.getAttribute("aria-invalid") === "true" ? start : end).focus();
1458
1484
  return;
1459
1485
  }
@@ -1645,13 +1671,16 @@ function sessionIdentityCell(row, session) {
1645
1671
  row.append(td);
1646
1672
  }
1647
1673
 
1648
- function sessionUpdatedAtLabel(session) {
1649
- if (session.catalogState === "lazy") return "尚未开始";
1650
- if (!Number.isSafeInteger(session.updatedAt) || session.updatedAt < 0) return "—";
1651
- const date = new Date(session.updatedAt * 1000);
1674
+ function nativeSessionDateLabel(value) {
1675
+ if (!Number.isSafeInteger(value) || value < 0) return "—";
1676
+ const date = new Date(value * 1000);
1652
1677
  return Number.isNaN(date.getTime()) ? "—" : date.toLocaleString("zh-CN", { hour12: false });
1653
1678
  }
1654
1679
 
1680
+ function sessionUpdatedAtLabel(session) {
1681
+ return session.catalogState === "lazy" ? "尚未开始" : nativeSessionDateLabel(session.updatedAt);
1682
+ }
1683
+
1655
1684
  function sessionDateLabel(value) {
1656
1685
  if (typeof value !== "string" || !value) return "—";
1657
1686
  const date = new Date(value);
@@ -1706,18 +1735,26 @@ function sessionSort(kind) {
1706
1735
  function sessionFilterPage(kind = "session", { resetSort = false } = {}) {
1707
1736
  const query = formQuery(document.querySelector(`#${kind}-filter`), "20");
1708
1737
  const sort = resetSort ? { by: "updatedAt", direction: "desc" } : sessionSort(kind);
1709
- query.set("sortBy", sort.by);
1738
+ query.set("sortBy", kind === "session" && query.get("inventoryState") === "lazy" ? "createdAt" : sort.by);
1710
1739
  query.set("sortDirection", sort.direction);
1711
1740
  return firstSessionPage(query.toString());
1712
1741
  }
1713
1742
 
1714
1743
  function renderSessionSort(kind) {
1715
1744
  const sort = sessionSort(kind);
1745
+ const applied = new URLSearchParams(state[`${kind}Page`]?.query || "");
1746
+ const lazy = kind === "session" && applied.get("inventoryState") === "lazy";
1716
1747
  for (const button of document.querySelectorAll("[data-session-sort]")) {
1717
1748
  if (button.dataset.sessionSort !== kind) continue;
1718
1749
  const selected = button.dataset.sortBy === sort.by;
1719
1750
  const descending = selected && sort.direction === "desc";
1720
- const label = button.dataset.sortBy === "createdAt" ? "创建时间" : "最近更新";
1751
+ const label = button.dataset.sortBy === "createdAt" ? lazy ? "登记时间" : "创建时间" : "最近更新";
1752
+ button.disabled = lazy && button.dataset.sortBy === "updatedAt";
1753
+ const labelNode = button.querySelector(".sort-label");
1754
+ if (labelNode) {
1755
+ labelNode.textContent = label;
1756
+ button.title = lazy ? "Netizen 登记时间;此时尚无 Codex 创建时间" : "Codex 原生会话创建时间,与创建时间筛选口径一致;按浏览器本地时区显示";
1757
+ }
1721
1758
  button.parentElement.setAttribute("aria-sort", selected ? descending ? "descending" : "ascending" : "none");
1722
1759
  button.setAttribute("aria-label", `按${label}${descending ? "由远及近" : "由近及远"}排序`
1723
1760
  + (selected ? `(当前${descending ? "由近及远" : "由远及近"})` : ""));
@@ -1725,6 +1762,14 @@ function renderSessionSort(kind) {
1725
1762
  }
1726
1763
  }
1727
1764
 
1765
+ function updateSessionDateFilters() {
1766
+ const lazy = document.querySelector("#session-inventory-state").value === "lazy";
1767
+ for (const root of document.querySelector("#session-filter").querySelectorAll("[data-time-range]")) {
1768
+ if (lazy) timeRangeControllers.get(root)?.cancel({ restoreFocus: false });
1769
+ root.disabled = lazy;
1770
+ }
1771
+ }
1772
+
1728
1773
  function renderSessionFreshness() {
1729
1774
  const kind = state.sessionTab === "temporary" ? "side" : "session";
1730
1775
  const updatedAt = state[`${kind}RefreshedAt`];
@@ -1741,6 +1786,7 @@ async function sortSessionPage(kind, by) {
1741
1786
  const page = state[`${kind}Page`];
1742
1787
  // Sorting operates on the displayed result, not unsubmitted filter drafts.
1743
1788
  const query = new URLSearchParams(page.query ?? sessionFilterPage(kind).query);
1789
+ if (kind === "session" && query.get("inventoryState") === "lazy" && by === "updatedAt") return false;
1744
1790
  const previous = sessionSort(kind);
1745
1791
  query.set("sortBy", by);
1746
1792
  query.set("sortDirection", previous.by === by && previous.direction === "desc" ? "asc" : "desc");
@@ -1758,13 +1804,18 @@ function renderSessionPagination(kind = "session") {
1758
1804
  function resetSessionFilters(kind = "session") {
1759
1805
  const form = document.querySelector(`#${kind}-filter`);
1760
1806
  form.reset();
1807
+ if (kind === "session") updateSessionDateFilters();
1761
1808
  (kind === "side" ? sideChatPicker : sessionChatPicker)?.reset();
1762
1809
  const filters = kind === "side" ? sideMultiFilters : sessionMultiFilters;
1763
1810
  for (const controller of filters.values()) controller.reset();
1764
1811
  for (const root of form.querySelectorAll("[data-time-range]")) {
1765
1812
  timeRangeControllers.get(root)?.reset();
1766
1813
  }
1767
- return refresh("sessions", sessionFilterPage(kind, { resetSort: true }));
1814
+ const page = sessionFilterPage(kind, { resetSort: true });
1815
+ const query = new URLSearchParams(page.query);
1816
+ query.delete("identity");
1817
+ page.query = query.toString();
1818
+ return refresh("sessions", page);
1768
1819
  }
1769
1820
 
1770
1821
  function invalidateSessionListRequest() {
@@ -1780,11 +1831,12 @@ async function loadSessionPage(kind, page, render) {
1780
1831
  const query = page.query == null
1781
1832
  ? formQuery(document.querySelector(`#${kind}-filter`), "20")
1782
1833
  : new URLSearchParams(page.query);
1783
- if (!query.has("sortBy")) query.set("sortBy", "updatedAt");
1834
+ if (!query.has("sortBy")) query.set("sortBy", kind === "session" && query.get("inventoryState") === "lazy" ? "createdAt" : "updatedAt");
1784
1835
  if (!query.has("sortDirection")) query.set("sortDirection", "desc");
1785
1836
  const candidate = { ...page, query: query.toString(), previousCursors: [...page.previousCursors] };
1786
1837
  await loadSessionProjectOptions(kind === "side" ? sideMultiFilters : sessionMultiFilters, isCurrent);
1787
1838
  if (!isCurrent()) return false;
1839
+ query.delete("updatedRange");
1788
1840
  if (page.cursor) query.set("cursor", page.cursor);
1789
1841
  const data = await api(`/api/v1/${kind === "side" ? "side-topics" : "sessions"}?${query}`);
1790
1842
  if (!isCurrent()) return false;
@@ -1812,8 +1864,9 @@ async function loadSessions(page = state.sessionPage) {
1812
1864
  }
1813
1865
 
1814
1866
  function renderSessions(data) {
1815
- document.querySelector("#sessions-catalog-notice").hidden = data.catalogAvailable !== false
1816
- && !data.items.some((session) => session.catalogState === "unknown");
1867
+ const query = new URLSearchParams(state.sessionPage.query || "");
1868
+ document.querySelector("#sessions-lazy-notice").hidden = query.get("inventoryState") !== "lazy";
1869
+ document.querySelector("#sessions-identity-notice").hidden = !query.has("identity");
1817
1870
  const body = document.querySelector("#sessions-body");
1818
1871
  body.replaceChildren();
1819
1872
  for (const session of data.items) {
@@ -1844,7 +1897,7 @@ function renderSessions(data) {
1844
1897
  const turnSettings = settings.turn_settings;
1845
1898
  const model = turnSettings ? `${turnSettings.model_id} / ${turnSettings.effort_id} / ${turnSettings.service_tier_id}` : "继承 Codex";
1846
1899
  cell(row, `${settings.message_context_mode} · ${model}`);
1847
- cell(row, sessionDateLabel(session.createdAt), "session-time");
1900
+ cell(row, session.catalogState === "lazy" ? sessionDateLabel(session.registeredAt) : nativeSessionDateLabel(session.createdAt), "session-time");
1848
1901
  cell(row, sessionUpdatedAtLabel(session), "session-time");
1849
1902
  const actions = actionsCell(row);
1850
1903
  wireSessionActions(actions, session);
@@ -1931,7 +1984,7 @@ function renderSides(data) {
1931
1984
  const link = document.createElement("a");
1932
1985
  link.className = "action";
1933
1986
  link.textContent = "查看来源会话";
1934
- link.href = `/sessions/ordinary?identity=${encodeURIComponent(parent.bindingId)}&inventoryState=all`;
1987
+ link.href = `/sessions/ordinary?identity=${encodeURIComponent(parent.bindingId)}&updatedRange=all`;
1935
1988
  link.target = "_blank";
1936
1989
  link.rel = "noopener noreferrer";
1937
1990
  actions.append(link);
@@ -3708,7 +3761,7 @@ async function loadScheduleRuns(page = scheduleRunsPage) {
3708
3761
  }
3709
3762
  if (run.binding_id && !run.binding_removed) {
3710
3763
  const link = document.createElement("a");
3711
- link.href = `/sessions/ordinary?${new URLSearchParams({ identity: run.binding_id, inventoryState: "all" })}`;
3764
+ link.href = `/sessions/ordinary?${new URLSearchParams({ identity: run.binding_id, updatedRange: "all" })}`;
3712
3765
  link.textContent = "打开 Session";
3713
3766
  links.append(link);
3714
3767
  }
@@ -3751,7 +3804,7 @@ async function moveScheduleRunsPage(direction) {
3751
3804
  const adminPagePaths = { projects: "/projects", schedules: "/schedules", defaults: "/defaults",
3752
3805
  updates: "/maintenance", system: "/system" };
3753
3806
  const sessionUrlKeys = ["project", "scopeKind", "inventoryState", "current", "chatId", "topicId", "identity",
3754
- "createdFrom", "createdBefore", "pageSize", "sortBy", "sortDirection"];
3807
+ "createdFrom", "createdBefore", "updatedFrom", "updatedBefore", "updatedRange", "pageSize", "sortBy", "sortDirection"];
3755
3808
  const sideUrlKeys = ["project", "state", "chatId", "createdFrom", "createdBefore", "pageSize", "sortBy", "sortDirection"];
3756
3809
  let navigationReady = false;
3757
3810
  let navigationIndex = 0;
@@ -3768,7 +3821,19 @@ function pageQuery(tab, sessionTab, raw = "") {
3768
3821
  }
3769
3822
  if (tab === "sessions") {
3770
3823
  const field = sessionTab === "temporary" ? "state" : "inventoryState";
3771
- if (!query.has(field)) for (const value of sessionTab === "temporary" ? ["creating", "open"] : ["active", "lazy", "unknown"]) query.append(field, value);
3824
+ if (!query.has(field)) for (const value of sessionTab === "temporary" ? ["creating", "open"] : ["active"]) query.append(field, value);
3825
+ if (sessionTab === "ordinary") {
3826
+ if (!["active", "archived", "lazy"].includes(query.get(field))) query.set(field, "active");
3827
+ else query.set(field, query.get(field));
3828
+ if (query.get(field) === "lazy") {
3829
+ for (const key of ["createdFrom", "createdBefore", "updatedFrom", "updatedBefore", "updatedRange"]) query.delete(key);
3830
+ query.set("sortBy", "createdAt");
3831
+ } else if (!query.has("updatedFrom") && !query.has("updatedBefore") && query.get("updatedRange") !== "all") {
3832
+ const range = presetTimeRange("last-3-months", new Date(), true);
3833
+ query.set("updatedFrom", range.from);
3834
+ }
3835
+ if (query.has("updatedFrom") || query.has("updatedBefore")) query.delete("updatedRange");
3836
+ }
3772
3837
  if (!query.has("pageSize")) query.set("pageSize", "20");
3773
3838
  if (!query.has("sortBy")) query.set("sortBy", "updatedAt");
3774
3839
  if (!query.has("sortDirection")) query.set("sortDirection", "desc");
@@ -3779,13 +3844,15 @@ function pageQuery(tab, sessionTab, raw = "") {
3779
3844
 
3780
3845
  function pageUrl(tab, sessionTab = state.sessionTab, raw = "") {
3781
3846
  const path = tab === "sessions" ? `/sessions/${sessionTab}` : adminPagePaths[tab];
3782
- const query = pageQuery(tab, sessionTab, raw);
3847
+ const intent = new URLSearchParams(raw);
3848
+ if (tab === "sessions" && sessionTab === "ordinary" && raw && !intent.has("updatedFrom") && !intent.has("updatedBefore")) intent.set("updatedRange", "all");
3849
+ const query = pageQuery(tab, sessionTab, intent);
3783
3850
  if (tab === "sessions") {
3784
3851
  for (const [key, value] of [["pageSize", "20"], ["sortBy", "updatedAt"], ["sortDirection", "desc"]]) {
3785
3852
  if (query.getAll(key).length === 1 && query.get(key) === value) query.delete(key);
3786
3853
  }
3787
3854
  const key = sessionTab === "temporary" ? "state" : "inventoryState";
3788
- const defaults = sessionTab === "temporary" ? "creating,open" : "active,lazy,unknown";
3855
+ const defaults = sessionTab === "temporary" ? "creating,open" : "active";
3789
3856
  if (query.getAll(key).sort().join(",") === defaults) query.delete(key);
3790
3857
  }
3791
3858
  if (tab === "schedules" && query.get("ended") === "false") query.delete("ended");
@@ -3829,6 +3896,7 @@ function readPageLocation() {
3829
3896
  if (path === "/" && query.get("binding_id")) {
3830
3897
  tab = "sessions";
3831
3898
  query.set("identity", query.get("binding_id"));
3899
+ if (!query.has("updatedFrom") && !query.has("updatedBefore")) query.set("updatedRange", "all");
3832
3900
  }
3833
3901
  return { tab, sessionTab, query: pageQuery(tab, sessionTab, query) };
3834
3902
  }
@@ -3846,7 +3914,12 @@ function restorePageFilters(route) {
3846
3914
  document.querySelector(`#${kind}-chat-filter`).value = chatId || "";
3847
3915
  document.querySelector(`#${kind}-page-size`).value = query.get("pageSize");
3848
3916
  for (const root of document.querySelector(`#${kind}-filter`).querySelectorAll("[data-time-range]")) {
3849
- timeRangeControllers.get(root).setBounds(query.get("createdFrom") || "", query.get("createdBefore") || "");
3917
+ const field = root.dataset.timeRangeField || "created";
3918
+ timeRangeControllers.get(root).setBounds(query.get(`${field}From`) || "", query.get(`${field}Before`) || "");
3919
+ }
3920
+ if (kind === "session") {
3921
+ document.querySelector("#session-inventory-state").value = query.get("inventoryState");
3922
+ updateSessionDateFilters();
3850
3923
  }
3851
3924
  state[`${kind}Page`] = firstSessionPage(query.toString());
3852
3925
  const name = kind === "side" ? "sides" : "sessions";
@@ -4035,6 +4108,7 @@ document.querySelector("#create-project").addEventListener("submit", async (even
4035
4108
  formElement.reset();
4036
4109
  });
4037
4110
 
4111
+ document.querySelector("#session-inventory-state").addEventListener("change", updateSessionDateFilters);
4038
4112
  document.querySelector("#session-filter").addEventListener("submit", (event) => {
4039
4113
  event.preventDefault();
4040
4114
  refresh("sessions", sessionFilterPage());
@@ -58,7 +58,7 @@
58
58
  <ul id="project-delete-remaining"></ul>
59
59
  </section>
60
60
  <div class="table-wrap"><table><thead><tr>
61
- <th>Project</th><th>路径</th><th>状态</th><th>会话</th><th>归档</th><th>最近激活</th><th>操作</th>
61
+ <th>Project</th><th>路径</th><th>状态</th><th>会话</th><th>最近激活</th><th>操作</th>
62
62
  </tr></thead><tbody id="projects-body"></tbody></table></div>
63
63
  <nav class="pagination" aria-label="Projects 分页">
64
64
  <button id="projects-previous" class="next" disabled>上一页</button>
@@ -82,18 +82,24 @@
82
82
  <div id="session-chat-picker"></div>
83
83
  <input id="session-chat-filter" name="chatId" type="hidden">
84
84
  <fieldset class="multi-filter" data-session-multi="scopeKind"><legend>Scope</legend></fieldset>
85
- <fieldset class="multi-filter" data-session-multi="inventoryState"><legend>会话状态</legend></fieldset>
85
+ <label>会话状态
86
+ <select id="session-inventory-state" name="inventoryState"><option value="active" selected>未归档</option><option value="archived">已归档</option><option value="lazy">Lazy(尚未开始)</option></select>
87
+ </label>
86
88
  <fieldset class="multi-filter" data-session-multi="current"><legend>当前指针</legend></fieldset>
87
89
  <fieldset class="time-range-filter" data-time-range data-time-range-id="session-created">
88
90
  <legend>创建时间</legend>
89
91
  </fieldset>
92
+ <fieldset class="time-range-filter" data-time-range data-time-range-id="session-updated" data-time-range-field="updated" data-time-range-default="last-3-months">
93
+ <legend>更新时间</legend>
94
+ </fieldset>
90
95
  <label>每页
91
96
  <select id="session-page-size" name="pageSize"><option value="10">10</option><option value="20" selected>20</option><option value="50">50</option><option value="100">100</option></select>
92
97
  </label>
93
98
  <button type="submit">筛选</button>
94
99
  <button type="button" id="session-filter-reset" class="refresh">重置</button>
95
100
  </form>
96
- <p id="sessions-catalog-notice" class="hint" role="status" hidden>原生目录暂时无法确认部分会话的状态,请稍后刷新。</p>
101
+ <p id="sessions-lazy-notice" class="hint" hidden>Lazy 会话尚未在 Codex 创建,按 Netizen 登记时间排序;创建时间和更新时间筛选不适用。</p>
102
+ <p id="sessions-identity-notice" class="hint" hidden>按指定会话 ID 筛选;未找到时可切换会话状态。</p>
97
103
  <div id="session-schedule-selection" class="selection-notice" hidden>
98
104
  <p>请选择执行此定时任务的会话。已填写的内容暂存在当前页面,刷新页面将丢失。</p>
99
105
  <button id="session-schedule-cancel" class="refresh" type="button">取消选择</button>
@@ -103,7 +109,7 @@
103
109
  </div>
104
110
  <div class="table-wrap"><table><thead><tr>
105
111
  <th>飞书位置</th><th>类型</th><th>Session</th><th>会话状态</th><th>运行态</th><th>Project</th><th>配置</th>
106
- <th aria-sort="none"><button type="button" class="session-sort" data-session-sort="session" data-sort-by="createdAt" aria-label="按创建时间由近及远排序" title="会话创建时间,与创建时间筛选口径一致;按浏览器本地时区显示">创建时间<span class="sort-indicator" aria-hidden="true">↕</span></button></th>
112
+ <th aria-sort="none"><button type="button" class="session-sort" data-session-sort="session" data-sort-by="createdAt" aria-label="按创建时间由近及远排序" title="Codex 原生会话创建时间,与创建时间筛选口径一致;按浏览器本地时区显示"><span class="sort-label">创建时间</span><span class="sort-indicator" aria-hidden="true">↕</span></button></th>
107
113
  <th aria-sort="descending"><button type="button" class="session-sort" data-session-sort="session" data-sort-by="updatedAt" aria-label="按最近更新由远及近排序(当前由近及远)" title="Codex 原生会话最近更新时间;按浏览器本地时区显示">最近更新<span class="sort-indicator" aria-hidden="true">↓</span></button></th><th>操作</th>
108
114
  </tr></thead><tbody id="sessions-body"></tbody></table></div>
109
115
  <nav class="pagination" aria-label="Sessions 分页">
@@ -489,6 +495,7 @@
489
495
  <button type="button" data-time-range-preset="last-24-hours">最近 24 小时</button>
490
496
  <button type="button" data-time-range-preset="last-7-days">最近 7 天</button>
491
497
  <button type="button" data-time-range-preset="last-30-days">最近 30 天</button>
498
+ <button type="button" data-time-range-preset="last-3-months">最近 3 个月</button>
492
499
  </div>
493
500
  </div>
494
501
  <div class="time-range-custom">