yee88 0.6.2__tar.gz → 0.7.0__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.
- {yee88-0.6.2 → yee88-0.7.0}/PKG-INFO +1 -1
- {yee88-0.6.2 → yee88-0.7.0}/pyproject.toml +1 -1
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/__init__.py +2 -0
- yee88-0.7.0/src/yee88/cli/handoff.py +302 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cron/manager.py +54 -17
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/skills/yee88/SKILL.md +31 -6
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/model.py +42 -1
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/loop.py +2 -1
- yee88-0.7.0/tests/test_cron_manager.py +422 -0
- yee88-0.7.0/tests/test_cron_scheduler.py +143 -0
- {yee88-0.6.2 → yee88-0.7.0}/uv.lock +1 -1
- yee88-0.6.2/.sisyphus/plans/yee88-architecture-review.md +0 -581
- {yee88-0.6.2 → yee88-0.7.0}/.codex/AGENTS.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/.github/workflows/ci.yml +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/.github/workflows/release.yml +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/.gitignore +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/Justfile +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/LICENSE +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/README.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/changelog.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/assets/favicon.svg +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/assets/logo.svg +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/assets/og-image.jpg +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/assets/takopi.svg +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/developing.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/explanation/architecture.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/explanation/index.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/explanation/module-map.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/explanation/plugin-system.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/explanation/routing-and-sessions.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/add-a-runner.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/chat-sessions.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/dev-setup.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/file-transfer.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/index.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/projects.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/route-by-chat.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/schedule-tasks.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/switch-engines.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/topics.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/troubleshooting.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/voice-notes.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/worktrees.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/write-a-plugin.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/index.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/javascripts/hero-chat.js +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/overrides/.icons/takopi/takopi.svg +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/overrides/main.html +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/plugins.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/agents/index.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/agents/invariants.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/agents/repo-map.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/commands-and-directives.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/config.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/context-resolution.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/env-vars.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/index.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/plugin-api.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/plugins.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/claude/runner.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/claude/stream-json-cheatsheet.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/claude/takopi-events.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/codex/exec-json-cheatsheet.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/codex/takopi-events.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/index.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/opencode/runner.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/opencode/stream-json-cheatsheet.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/opencode/takopi-events.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/pi/runner.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/pi/stream-json-cheatsheet.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/pi/takopi-events.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/specification.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/reference/transports/telegram.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/stylesheets/admonitions.css +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/stylesheets/hero-chat.css +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/stylesheets/workflow-preview.css +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/tutorials/conversation-modes.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/tutorials/first-run.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/tutorials/index.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/tutorials/install.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/tutorials/multi-engine.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/tutorials/projects-and-branches.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/user-guide-zh.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/docs/user-guide.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/readme.md +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/scripts/commit_notify.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/scripts/docs_build_cf.sh +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/scripts/docs_prebuild.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/scripts/onboarding_preview.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/scripts/release_notify.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/__init__.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/api.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/backends.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/backends_helpers.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/config.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/cron.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/doctor.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/init.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/onboarding_cmd.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/plugins.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/reload.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/run.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/topic.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/commands.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/config.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/config_migrations.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/config_watch.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/context.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cron/__init__.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cron/models.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cron/scheduler.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/directives.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/engines.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/events.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/ids.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/lockfile.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/logging.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/markdown.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/model.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/plugins.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/presenter.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/progress.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/router.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runner.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runner_bridge.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/__init__.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/claude.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/codex.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/mock.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/opencode.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/pi.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/run_options.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/tool_actions.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runtime_loader.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/scheduler.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/schemas/__init__.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/schemas/claude.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/schemas/codex.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/schemas/opencode.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/schemas/pi.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/settings.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/__init__.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/api_models.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/api_schemas.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/backend.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/bridge.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/chat_prefs.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/chat_sessions.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/client.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/client_api.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/__init__.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/agent.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/cancel.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/dispatch.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/executor.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/file_transfer.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/handlers.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/media.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/menu.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/overrides.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/parse.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/plan.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/reasoning.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/reply.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/topics.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/trigger.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/context.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/engine_defaults.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/engine_overrides.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/files.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/onboarding.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/outbox.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/parsing.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/render.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/state_store.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/topic_state.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/topics.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/trigger_mode.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/types.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/voice.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/transport.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/transport_runtime.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/transports.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/utils/__init__.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/utils/git.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/utils/json_state.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/utils/paths.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/utils/streams.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/utils/subprocess.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/src/yee88/worktrees.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/__init__.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/conftest.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/factories.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/claude_stream_json_session.jsonl +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/codex_exec_json_all_formats.jsonl +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/codex_exec_json_all_formats.txt +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/opencode_run_json.jsonl +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/opencode_stream_error.jsonl +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/opencode_stream_success.jsonl +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/opencode_stream_success_no_reason.jsonl +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/pi_print_mode_events.jsonl +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/pi_stream_error.jsonl +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/pi_stream_success.jsonl +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/plugin_fixtures.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/telegram_fakes.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_api_exports.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_auto_router.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_claude_runner.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_claude_schema.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_cli_auto_router.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_cli_chat_id.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_cli_commands.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_cli_config.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_cli_doctor.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_cli_helpers.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_codex_runner_helpers.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_codex_schema.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_codex_tool_result_summary.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_command_registry.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_config_store.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_config_watch.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_engine_discovery.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_exec_bridge.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_exec_render.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_exec_runner.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_git_utils.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_lockfile.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_onboarding.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_onboarding_helpers.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_onboarding_interactive.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_opencode_runner.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_opencode_schema.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_paths.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_pi_runner.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_pi_schema.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_plugins.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_projects_config.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_rendering.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_runner_contract.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_runner_run_options.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_runner_utils.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_runtime_loader.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_settings.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_settings_contract.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_subprocess.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_agent_trigger_commands.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_backend.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_bridge.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_chat_prefs.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_chat_sessions.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_client.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_client_api.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_context_helpers.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_engine_defaults.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_engine_overrides.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_file_transfer_helpers.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_files.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_incoming.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_media_command.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_polling.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_queue.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_topic_state.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_topics_command.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_topics_helpers.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_trigger_mode.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_voice.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_tool_actions.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_transport.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_transport_registry.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_transport_runtime.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/tests/test_worktrees.py +0 -0
- {yee88-0.6.2 → yee88-0.7.0}/zensical.toml +0 -0
|
@@ -92,6 +92,7 @@ from .config import (
|
|
|
92
92
|
config_unset,
|
|
93
93
|
)
|
|
94
94
|
from .cron import app as cron_app
|
|
95
|
+
from .handoff import app as handoff_app
|
|
95
96
|
from .reload import reload_command
|
|
96
97
|
|
|
97
98
|
|
|
@@ -215,6 +216,7 @@ def create_app() -> typer.Typer:
|
|
|
215
216
|
app.command(name="plugins")(plugins_cmd)
|
|
216
217
|
app.add_typer(config_app, name="config")
|
|
217
218
|
app.add_typer(cron_app, name="cron")
|
|
219
|
+
app.add_typer(handoff_app, name="handoff")
|
|
218
220
|
app.command(name="reload")(reload_command)
|
|
219
221
|
app.callback()(app_main)
|
|
220
222
|
for engine_id in _engine_ids_for_cli():
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import anyio
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from ..context import RunContext
|
|
13
|
+
from ..model import ResumeToken
|
|
14
|
+
from ..settings import load_settings_if_exists
|
|
15
|
+
from ..telegram.client import TelegramClient
|
|
16
|
+
from ..telegram.topic_state import TopicStateStore, resolve_state_path
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="Handoff session context to Telegram")
|
|
19
|
+
|
|
20
|
+
OPENCODE_STORAGE = Path.home() / ".local" / "share" / "opencode" / "storage"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SessionInfo:
|
|
25
|
+
id: str
|
|
26
|
+
directory: str
|
|
27
|
+
updated: float
|
|
28
|
+
title: str
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def project_name(self) -> str:
|
|
32
|
+
return Path(self.directory).name if self.directory else "unknown"
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def updated_str(self) -> str:
|
|
36
|
+
return datetime.fromtimestamp(self.updated / 1000).strftime("%m-%d %H:%M")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_recent_sessions(limit: int = 10) -> list[SessionInfo]:
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
["opencode", "session", "list", "--format", "json", "-n", str(limit)],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
check=True,
|
|
46
|
+
)
|
|
47
|
+
data = json.loads(result.stdout)
|
|
48
|
+
return [
|
|
49
|
+
SessionInfo(
|
|
50
|
+
id=s.get("id", ""),
|
|
51
|
+
directory=s.get("directory", ""),
|
|
52
|
+
updated=s.get("updated", 0),
|
|
53
|
+
title=s.get("title", ""),
|
|
54
|
+
)
|
|
55
|
+
for s in data
|
|
56
|
+
]
|
|
57
|
+
except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError):
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_session_messages(session_id: str, limit: int = 5) -> list[dict]:
|
|
62
|
+
message_dir = OPENCODE_STORAGE / "message" / session_id
|
|
63
|
+
if not message_dir.exists():
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
messages: list[tuple[int, str, str]] = []
|
|
67
|
+
for msg_file in message_dir.glob("msg_*.json"):
|
|
68
|
+
try:
|
|
69
|
+
data = json.loads(msg_file.read_text())
|
|
70
|
+
created = data.get("time", {}).get("created", 0)
|
|
71
|
+
role = data.get("role", "unknown")
|
|
72
|
+
msg_id = data.get("id", "")
|
|
73
|
+
messages.append((created, role, msg_id))
|
|
74
|
+
except (json.JSONDecodeError, OSError):
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
messages.sort(key=lambda x: x[0], reverse=True)
|
|
78
|
+
messages = messages[:limit]
|
|
79
|
+
messages.reverse()
|
|
80
|
+
|
|
81
|
+
result = []
|
|
82
|
+
for _, role, msg_id in messages:
|
|
83
|
+
part_dir = OPENCODE_STORAGE / "part" / msg_id
|
|
84
|
+
if not part_dir.exists():
|
|
85
|
+
continue
|
|
86
|
+
for part_file in part_dir.glob("prt_*.json"):
|
|
87
|
+
try:
|
|
88
|
+
part_data = json.loads(part_file.read_text())
|
|
89
|
+
if part_data.get("type") == "text":
|
|
90
|
+
text = part_data.get("text", "")
|
|
91
|
+
if text.startswith('"') and text.endswith('"\n'):
|
|
92
|
+
text = json.loads(text.rstrip('\n'))
|
|
93
|
+
result.append({"role": role, "text": text})
|
|
94
|
+
break
|
|
95
|
+
except (json.JSONDecodeError, OSError):
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _format_handoff_message(
|
|
102
|
+
session_id: str,
|
|
103
|
+
messages: list[dict],
|
|
104
|
+
project: str | None = None,
|
|
105
|
+
) -> str:
|
|
106
|
+
lines = ["📱 **会话接力**", ""]
|
|
107
|
+
|
|
108
|
+
if project:
|
|
109
|
+
lines.append(f"📁 项目: `{project}`")
|
|
110
|
+
lines.append(f"🔗 Session: `{session_id}`")
|
|
111
|
+
lines.append("")
|
|
112
|
+
lines.append("---")
|
|
113
|
+
lines.append("")
|
|
114
|
+
|
|
115
|
+
for msg in messages:
|
|
116
|
+
role = msg.get("role", "unknown")
|
|
117
|
+
text = msg.get("text", "")
|
|
118
|
+
role_label = "👤" if role == "user" else "🤖"
|
|
119
|
+
if len(text) > 500:
|
|
120
|
+
text = text[:500] + "..."
|
|
121
|
+
lines.append(f"{role_label} **{role}**:")
|
|
122
|
+
lines.append(text)
|
|
123
|
+
lines.append("")
|
|
124
|
+
|
|
125
|
+
total_len = sum(len(line) for line in lines)
|
|
126
|
+
if total_len > 3500:
|
|
127
|
+
lines = lines[:20]
|
|
128
|
+
lines.append("... (truncated)")
|
|
129
|
+
|
|
130
|
+
lines.append("---")
|
|
131
|
+
lines.append("")
|
|
132
|
+
lines.append("💡 直接在此 Topic 发消息即可继续对话")
|
|
133
|
+
|
|
134
|
+
return "\n".join(lines)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def _create_handoff_topic(
|
|
138
|
+
token: str,
|
|
139
|
+
chat_id: int,
|
|
140
|
+
session_id: str,
|
|
141
|
+
project: str,
|
|
142
|
+
config_path: Path,
|
|
143
|
+
) -> int | None:
|
|
144
|
+
title = f"📱 {project} handoff"
|
|
145
|
+
|
|
146
|
+
client = TelegramClient(token)
|
|
147
|
+
try:
|
|
148
|
+
result = await client.create_forum_topic(chat_id, title)
|
|
149
|
+
if result is None:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
thread_id = result.message_thread_id
|
|
153
|
+
|
|
154
|
+
state_path = resolve_state_path(config_path)
|
|
155
|
+
store = TopicStateStore(state_path)
|
|
156
|
+
|
|
157
|
+
context = RunContext(project=project.lower(), branch=None)
|
|
158
|
+
await store.set_context(chat_id, thread_id, context, topic_title=title)
|
|
159
|
+
|
|
160
|
+
resume_token = ResumeToken(engine="opencode", value=session_id)
|
|
161
|
+
await store.set_session_resume(chat_id, thread_id, resume_token)
|
|
162
|
+
|
|
163
|
+
return thread_id
|
|
164
|
+
finally:
|
|
165
|
+
await client.close()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def _send_to_telegram(
|
|
169
|
+
token: str,
|
|
170
|
+
chat_id: int,
|
|
171
|
+
message: str,
|
|
172
|
+
thread_id: int | None = None,
|
|
173
|
+
) -> bool:
|
|
174
|
+
client = TelegramClient(token)
|
|
175
|
+
try:
|
|
176
|
+
result = await client.send_message(
|
|
177
|
+
chat_id=chat_id,
|
|
178
|
+
text=message,
|
|
179
|
+
message_thread_id=thread_id,
|
|
180
|
+
parse_mode="Markdown",
|
|
181
|
+
)
|
|
182
|
+
return result is not None
|
|
183
|
+
finally:
|
|
184
|
+
await client.close()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.command()
|
|
188
|
+
def send(
|
|
189
|
+
session: str | None = typer.Option(
|
|
190
|
+
None, "--session", "-s", help="Session ID (defaults to latest)"
|
|
191
|
+
),
|
|
192
|
+
limit: int = typer.Option(
|
|
193
|
+
3, "--limit", "-n", help="Number of messages to include"
|
|
194
|
+
),
|
|
195
|
+
project: str | None = typer.Option(
|
|
196
|
+
None, "--project", "-p", help="Project name for context"
|
|
197
|
+
),
|
|
198
|
+
) -> None:
|
|
199
|
+
result = load_settings_if_exists()
|
|
200
|
+
if result is None:
|
|
201
|
+
typer.echo("❌ 未找到 yee88 配置文件", err=True)
|
|
202
|
+
raise typer.Exit(1)
|
|
203
|
+
|
|
204
|
+
settings, config_path = result
|
|
205
|
+
telegram_cfg = settings.transports.telegram
|
|
206
|
+
|
|
207
|
+
token = telegram_cfg.bot_token
|
|
208
|
+
chat_id = telegram_cfg.chat_id
|
|
209
|
+
|
|
210
|
+
if not token or not chat_id:
|
|
211
|
+
typer.echo("❌ Telegram 配置不完整 (需要 bot_token 和 chat_id)", err=True)
|
|
212
|
+
raise typer.Exit(1)
|
|
213
|
+
|
|
214
|
+
if not telegram_cfg.topics.enabled:
|
|
215
|
+
typer.echo("❌ Topics 未启用,请先运行: yee88 config set transports.telegram.topics.enabled true", err=True)
|
|
216
|
+
raise typer.Exit(1)
|
|
217
|
+
|
|
218
|
+
session_id = session
|
|
219
|
+
session_project = project
|
|
220
|
+
if session_id is None:
|
|
221
|
+
sessions = _get_recent_sessions(limit=10)
|
|
222
|
+
if not sessions:
|
|
223
|
+
typer.echo("❌ 未找到 OpenCode 会话", err=True)
|
|
224
|
+
raise typer.Exit(1)
|
|
225
|
+
|
|
226
|
+
typer.echo("\n📋 最近的会话:\n")
|
|
227
|
+
for i, s in enumerate(sessions[:10], 1):
|
|
228
|
+
title_display = s.title[:40] if s.title else s.project_name
|
|
229
|
+
typer.echo(f" [{i}] {s.updated_str} {title_display}")
|
|
230
|
+
typer.echo("")
|
|
231
|
+
|
|
232
|
+
choice = typer.prompt("选择会话 (1-10)", default="1")
|
|
233
|
+
try:
|
|
234
|
+
idx = int(choice) - 1
|
|
235
|
+
if idx < 0 or idx >= len(sessions):
|
|
236
|
+
typer.echo("❌ 无效选择", err=True)
|
|
237
|
+
raise typer.Exit(1)
|
|
238
|
+
except ValueError:
|
|
239
|
+
typer.echo("❌ 请输入数字", err=True)
|
|
240
|
+
raise typer.Exit(1)
|
|
241
|
+
|
|
242
|
+
selected = sessions[idx]
|
|
243
|
+
session_id = selected.id
|
|
244
|
+
if session_project is None:
|
|
245
|
+
session_project = selected.project_name
|
|
246
|
+
|
|
247
|
+
if not session_id:
|
|
248
|
+
typer.echo("❌ 会话 ID 为空", err=True)
|
|
249
|
+
raise typer.Exit(1)
|
|
250
|
+
|
|
251
|
+
typer.echo(f"📖 读取会话 {session_id[:20]}...")
|
|
252
|
+
|
|
253
|
+
messages = _get_session_messages(session_id, limit=limit)
|
|
254
|
+
if not messages:
|
|
255
|
+
typer.echo("❌ 无法读取会话消息", err=True)
|
|
256
|
+
raise typer.Exit(1)
|
|
257
|
+
|
|
258
|
+
typer.echo("🆕 创建新 Topic...")
|
|
259
|
+
|
|
260
|
+
async def do_handoff() -> tuple[bool, int | None]:
|
|
261
|
+
thread_id = await _create_handoff_topic(
|
|
262
|
+
token=token,
|
|
263
|
+
chat_id=chat_id,
|
|
264
|
+
session_id=session_id,
|
|
265
|
+
project=session_project or "unknown",
|
|
266
|
+
config_path=config_path,
|
|
267
|
+
)
|
|
268
|
+
if thread_id is None:
|
|
269
|
+
return False, None
|
|
270
|
+
|
|
271
|
+
handoff_msg = _format_handoff_message(
|
|
272
|
+
session_id=session_id,
|
|
273
|
+
messages=messages,
|
|
274
|
+
project=session_project,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
success = await _send_to_telegram(
|
|
278
|
+
token=token,
|
|
279
|
+
chat_id=chat_id,
|
|
280
|
+
message=handoff_msg,
|
|
281
|
+
thread_id=thread_id,
|
|
282
|
+
)
|
|
283
|
+
return success, thread_id
|
|
284
|
+
|
|
285
|
+
success, thread_id = anyio.run(do_handoff)
|
|
286
|
+
|
|
287
|
+
if success:
|
|
288
|
+
typer.echo("✅ 已发送到 Telegram!")
|
|
289
|
+
typer.echo(f" Session: {session_id}")
|
|
290
|
+
typer.echo(f" Project: {session_project}")
|
|
291
|
+
typer.echo(f" Topic ID: {thread_id}")
|
|
292
|
+
typer.echo(f" 消息数: {limit}")
|
|
293
|
+
else:
|
|
294
|
+
typer.echo("❌ 发送失败", err=True)
|
|
295
|
+
raise typer.Exit(1)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@app.callback(invoke_without_command=True)
|
|
299
|
+
def main(ctx: typer.Context) -> None:
|
|
300
|
+
"""Handoff session context to Telegram for mobile continuation."""
|
|
301
|
+
if ctx.invoked_subcommand is None:
|
|
302
|
+
ctx.invoke(send, session=None, limit=3, project=None)
|
|
@@ -2,25 +2,54 @@ import tomllib
|
|
|
2
2
|
import tomli_w
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import List, Optional
|
|
5
|
+
from zoneinfo import ZoneInfo
|
|
5
6
|
from croniter import croniter
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from .models import CronJob
|
|
8
9
|
|
|
10
|
+
BEIJING_TZ = ZoneInfo("Asia/Shanghai")
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
class CronManager:
|
|
11
|
-
def __init__(self, config_dir: Path):
|
|
14
|
+
def __init__(self, config_dir: Path, timezone: str = "Asia/Shanghai"):
|
|
12
15
|
self.file = config_dir / "cron.toml"
|
|
13
16
|
self.jobs: List[CronJob] = []
|
|
17
|
+
self.timezone = ZoneInfo(timezone)
|
|
14
18
|
|
|
15
19
|
def _validate_project(self, project: str) -> None:
|
|
16
20
|
if not project:
|
|
17
21
|
return
|
|
22
|
+
|
|
18
23
|
path = Path(project).expanduser().resolve()
|
|
19
24
|
if path.exists() and path.is_dir():
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
from ..settings import load_settings_if_exists
|
|
28
|
+
from ..engines import list_backend_ids
|
|
29
|
+
|
|
30
|
+
result = load_settings_if_exists()
|
|
31
|
+
if result is None:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
settings, config_path = result
|
|
35
|
+
engine_ids = list_backend_ids()
|
|
36
|
+
projects_config = settings.to_projects_config(
|
|
37
|
+
config_path=config_path,
|
|
38
|
+
engine_ids=engine_ids
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if project.lower() in projects_config.projects:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
available = list(projects_config.projects.keys())
|
|
45
|
+
if available:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"未知项目: {project}。可用项目: {', '.join(available)}"
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"未知项目: {project}。请先使用 'yee88 init' 注册项目"
|
|
52
|
+
)
|
|
24
53
|
|
|
25
54
|
def load(self):
|
|
26
55
|
if not self.file.exists():
|
|
@@ -99,7 +128,7 @@ class CronManager:
|
|
|
99
128
|
return False
|
|
100
129
|
|
|
101
130
|
def get_due_jobs(self) -> List[CronJob]:
|
|
102
|
-
now = datetime.now()
|
|
131
|
+
now = datetime.now(self.timezone)
|
|
103
132
|
due = []
|
|
104
133
|
one_time_completed = []
|
|
105
134
|
|
|
@@ -107,30 +136,38 @@ class CronManager:
|
|
|
107
136
|
if not job.enabled:
|
|
108
137
|
continue
|
|
109
138
|
|
|
110
|
-
# 一次性任务处理
|
|
111
139
|
if job.one_time:
|
|
112
140
|
try:
|
|
113
141
|
exec_time = datetime.fromisoformat(job.schedule)
|
|
142
|
+
if exec_time.tzinfo is None:
|
|
143
|
+
exec_time = exec_time.replace(tzinfo=self.timezone)
|
|
114
144
|
if exec_time <= now:
|
|
115
145
|
due.append(job)
|
|
116
146
|
one_time_completed.append(job.id)
|
|
117
147
|
except Exception:
|
|
118
148
|
continue
|
|
119
149
|
else:
|
|
120
|
-
# 周期性任务处理
|
|
121
150
|
try:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
151
|
+
if job.last_run:
|
|
152
|
+
base = datetime.fromisoformat(job.last_run)
|
|
153
|
+
if base.tzinfo is None:
|
|
154
|
+
base = base.replace(tzinfo=self.timezone)
|
|
155
|
+
itr = croniter(job.schedule, base)
|
|
156
|
+
next_run = itr.get_next(datetime)
|
|
157
|
+
if next_run <= now:
|
|
158
|
+
due.append(job)
|
|
159
|
+
job.last_run = now.isoformat()
|
|
160
|
+
job.next_run = itr.get_next(datetime).isoformat()
|
|
161
|
+
else:
|
|
162
|
+
itr = croniter(job.schedule, now)
|
|
163
|
+
prev_run = itr.get_prev(datetime)
|
|
164
|
+
if prev_run.date() == now.date():
|
|
165
|
+
due.append(job)
|
|
166
|
+
job.last_run = now.isoformat()
|
|
167
|
+
job.next_run = itr.get_next(datetime).isoformat()
|
|
130
168
|
except Exception:
|
|
131
169
|
continue
|
|
132
170
|
|
|
133
|
-
# 删除已完成的一次性任务
|
|
134
171
|
if one_time_completed:
|
|
135
172
|
self.jobs = [j for j in self.jobs if j.id not in one_time_completed]
|
|
136
173
|
|
|
@@ -36,13 +36,38 @@ description: 当用户说"提醒我"、"X分钟/小时后"、"定时"、"每天/
|
|
|
36
36
|
|
|
37
37
|
## ⛔ 默认不传 --project!除非用户明确要求
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
### 🧠 COT:判断是否需要 --project
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
**收到定时任务请求时,先在脑中过一遍这个决策流程:**
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
1. 用户有没有提到具体项目名?
|
|
45
|
+
- "在 takopi 项目下..." → 需要 --project
|
|
46
|
+
- "帮我在 work 项目..." → 需要 --project
|
|
47
|
+
- "提醒我喝水" → 不需要 --project
|
|
48
|
+
- "每天9点提醒站会" → 不需要 --project
|
|
49
|
+
|
|
50
|
+
2. 用户有没有说"在某个项目下运行"?
|
|
51
|
+
- "在 xxx 下执行..." → 需要 --project
|
|
52
|
+
- "切到 xxx 项目..." → 需要 --project
|
|
53
|
+
- 没有提到项目上下文 → 不需要 --project
|
|
54
|
+
|
|
55
|
+
3. 这个任务是通用提醒还是项目相关?
|
|
56
|
+
- 喝水、休息、开会 → 通用,不需要 --project
|
|
57
|
+
- 代码审查、部署、PR → 可能需要,但除非用户说了项目名,否则不传
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**结论:99% 的情况不需要 --project。只有用户明确说了项目名才传。**
|
|
61
|
+
|
|
62
|
+
### 决策表
|
|
63
|
+
|
|
64
|
+
| 用户说的话 | 需要 --project? | 命令 |
|
|
65
|
+
|-----------|----------------|------|
|
|
66
|
+
| "1分钟后提醒我喝水" | ❌ 不需要 | `yee88 cron add reminder "+1m" "喝水" -o` |
|
|
67
|
+
| "每天9点提醒我站会" | ❌ 不需要 | `yee88 cron add standup "0 9 * * *" "站会时间"` |
|
|
68
|
+
| "30分钟后提醒我休息" | ❌ 不需要 | `yee88 cron add break "+30m" "休息一下" -o` |
|
|
69
|
+
| "在 takopi 项目下每天9点跑测试" | ✅ 需要 | `yee88 cron add test "0 9 * * *" "跑测试" --project takopi` |
|
|
70
|
+
| "帮我在 work 项目设个提醒" | ✅ 需要 | `yee88 cron add reminder "+1h" "..." --project work -o` |
|
|
46
71
|
|
|
47
72
|
**⚠️ --project 只接受项目别名,不是路径!**
|
|
48
73
|
|
|
@@ -29,7 +29,7 @@ MODEL_SELECT_CALLBACK_PREFIX = "yee88:model_select:"
|
|
|
29
29
|
|
|
30
30
|
MODEL_USAGE = (
|
|
31
31
|
"usage: `/model`, `/model status`, `/model set <model>`, "
|
|
32
|
-
"`/model set <engine> <model>`,
|
|
32
|
+
"`/model set <engine> <model>`, `/model clear [engine]`, or `/model reset`"
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
|
@@ -339,4 +339,45 @@ async def _handle_model_command(
|
|
|
339
339
|
await reply(text="chat model override cleared.")
|
|
340
340
|
return
|
|
341
341
|
|
|
342
|
+
if action == "reset":
|
|
343
|
+
if len(tokens) > 1:
|
|
344
|
+
await reply(text=MODEL_USAGE)
|
|
345
|
+
return
|
|
346
|
+
if not await require_admin_or_private(
|
|
347
|
+
cfg,
|
|
348
|
+
msg,
|
|
349
|
+
missing_sender="cannot verify sender for model overrides.",
|
|
350
|
+
failed_member="failed to verify model override permissions.",
|
|
351
|
+
denied="changing model overrides is restricted to group admins.",
|
|
352
|
+
):
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
cleared_engines = []
|
|
356
|
+
for engine in engine_ids:
|
|
357
|
+
scope = await apply_engine_override(
|
|
358
|
+
reply=reply,
|
|
359
|
+
tkey=tkey,
|
|
360
|
+
topic_store=topic_store,
|
|
361
|
+
chat_prefs=chat_prefs,
|
|
362
|
+
chat_id=msg.chat_id,
|
|
363
|
+
engine=engine,
|
|
364
|
+
update=lambda current: EngineOverrides(
|
|
365
|
+
model=None,
|
|
366
|
+
reasoning=current.reasoning if current is not None else None,
|
|
367
|
+
),
|
|
368
|
+
topic_unavailable="topic model overrides are unavailable.",
|
|
369
|
+
chat_unavailable="chat model overrides are unavailable (no config path).",
|
|
370
|
+
)
|
|
371
|
+
if scope is not None:
|
|
372
|
+
cleared_engines.append(engine)
|
|
373
|
+
|
|
374
|
+
if cleared_engines:
|
|
375
|
+
engines_list = ", ".join(cleared_engines)
|
|
376
|
+
await reply(
|
|
377
|
+
text=f"all model overrides reset to default for engines: {engines_list}"
|
|
378
|
+
)
|
|
379
|
+
else:
|
|
380
|
+
await reply(text="no model overrides to reset.")
|
|
381
|
+
return
|
|
382
|
+
|
|
342
383
|
await reply(text=MODEL_USAGE)
|
|
@@ -1077,7 +1077,8 @@ async def run_main_loop(
|
|
|
1077
1077
|
tg.start_soon(run_config_watch)
|
|
1078
1078
|
|
|
1079
1079
|
if config_path is not None:
|
|
1080
|
-
cron_manager = CronManager(config_path.parent)
|
|
1080
|
+
cron_manager = CronManager(config_path.parent, timezone="Asia/Shanghai")
|
|
1081
|
+
logger.info("cron.manager.initialized", timezone="Asia/Shanghai")
|
|
1081
1082
|
|
|
1082
1083
|
async def _execute_cron_job(job: CronJob) -> None:
|
|
1083
1084
|
try:
|