yee88 0.6.3__tar.gz → 0.7.1__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.3 → yee88-0.7.1}/PKG-INFO +1 -1
- {yee88-0.6.3 → yee88-0.7.1}/changelog.md +30 -3
- {yee88-0.6.3 → yee88-0.7.1}/docs/user-guide-zh.md +90 -1
- {yee88-0.6.3 → yee88-0.7.1}/pyproject.toml +1 -1
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cli/__init__.py +2 -0
- yee88-0.7.1/src/yee88/cli/handoff.py +304 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cron/manager.py +54 -17
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/ids.py +1 -1
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/skills/yee88/SKILL.md +57 -6
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/handlers.py +2 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/menu.py +4 -1
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/topics.py +82 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/loop.py +12 -1
- yee88-0.7.1/tests/test_cron_manager.py +422 -0
- yee88-0.7.1/tests/test_cron_scheduler.py +143 -0
- {yee88-0.6.3 → yee88-0.7.1}/uv.lock +1 -1
- yee88-0.6.3/.sisyphus/plans/yee88-architecture-review.md +0 -581
- {yee88-0.6.3 → yee88-0.7.1}/.codex/AGENTS.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/.github/workflows/ci.yml +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/.github/workflows/release.yml +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/.gitignore +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/Justfile +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/LICENSE +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/README.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/assets/favicon.svg +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/assets/logo.svg +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/assets/og-image.jpg +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/assets/takopi.svg +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/developing.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/explanation/architecture.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/explanation/index.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/explanation/module-map.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/explanation/plugin-system.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/explanation/routing-and-sessions.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/add-a-runner.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/chat-sessions.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/dev-setup.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/file-transfer.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/index.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/projects.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/route-by-chat.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/schedule-tasks.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/switch-engines.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/topics.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/troubleshooting.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/voice-notes.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/worktrees.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/how-to/write-a-plugin.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/index.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/javascripts/hero-chat.js +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/overrides/.icons/takopi/takopi.svg +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/overrides/main.html +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/plugins.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/agents/index.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/agents/invariants.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/agents/repo-map.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/commands-and-directives.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/config.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/context-resolution.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/env-vars.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/index.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/plugin-api.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/plugins.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/runners/claude/runner.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/runners/claude/stream-json-cheatsheet.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/runners/claude/takopi-events.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/runners/codex/exec-json-cheatsheet.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/runners/codex/takopi-events.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/runners/index.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/runners/opencode/runner.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/runners/opencode/stream-json-cheatsheet.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/runners/opencode/takopi-events.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/runners/pi/runner.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/runners/pi/stream-json-cheatsheet.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/runners/pi/takopi-events.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/specification.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/reference/transports/telegram.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/stylesheets/admonitions.css +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/stylesheets/hero-chat.css +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/stylesheets/workflow-preview.css +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/tutorials/conversation-modes.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/tutorials/first-run.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/tutorials/index.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/tutorials/install.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/tutorials/multi-engine.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/tutorials/projects-and-branches.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/docs/user-guide.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/readme.md +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/scripts/commit_notify.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/scripts/docs_build_cf.sh +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/scripts/docs_prebuild.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/scripts/onboarding_preview.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/scripts/release_notify.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/__init__.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/api.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/backends.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/backends_helpers.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cli/config.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cli/cron.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cli/doctor.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cli/init.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cli/onboarding_cmd.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cli/plugins.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cli/reload.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cli/run.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cli/topic.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/commands.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/config.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/config_migrations.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/config_watch.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/context.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cron/__init__.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cron/models.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/cron/scheduler.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/directives.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/engines.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/events.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/lockfile.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/logging.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/markdown.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/model.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/plugins.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/presenter.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/progress.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/router.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/runner.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/runner_bridge.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/runners/__init__.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/runners/claude.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/runners/codex.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/runners/mock.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/runners/opencode.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/runners/pi.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/runners/run_options.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/runners/tool_actions.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/runtime_loader.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/scheduler.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/schemas/__init__.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/schemas/claude.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/schemas/codex.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/schemas/opencode.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/schemas/pi.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/settings.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/__init__.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/api_models.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/api_schemas.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/backend.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/bridge.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/chat_prefs.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/chat_sessions.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/client.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/client_api.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/__init__.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/agent.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/cancel.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/dispatch.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/executor.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/file_transfer.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/media.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/model.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/overrides.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/parse.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/plan.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/reasoning.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/reply.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/commands/trigger.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/context.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/engine_defaults.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/engine_overrides.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/files.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/onboarding.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/outbox.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/parsing.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/render.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/state_store.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/topic_state.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/topics.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/trigger_mode.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/types.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/telegram/voice.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/transport.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/transport_runtime.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/transports.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/utils/__init__.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/utils/git.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/utils/json_state.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/utils/paths.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/utils/streams.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/utils/subprocess.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/src/yee88/worktrees.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/__init__.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/conftest.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/factories.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/fixtures/claude_stream_json_session.jsonl +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/fixtures/codex_exec_json_all_formats.jsonl +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/fixtures/codex_exec_json_all_formats.txt +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/fixtures/opencode_run_json.jsonl +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/fixtures/opencode_stream_error.jsonl +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/fixtures/opencode_stream_success.jsonl +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/fixtures/opencode_stream_success_no_reason.jsonl +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/fixtures/pi_print_mode_events.jsonl +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/fixtures/pi_stream_error.jsonl +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/fixtures/pi_stream_success.jsonl +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/plugin_fixtures.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/telegram_fakes.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_api_exports.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_auto_router.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_claude_runner.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_claude_schema.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_cli_auto_router.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_cli_chat_id.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_cli_commands.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_cli_config.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_cli_doctor.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_cli_helpers.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_codex_runner_helpers.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_codex_schema.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_codex_tool_result_summary.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_command_registry.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_config_store.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_config_watch.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_engine_discovery.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_exec_bridge.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_exec_render.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_exec_runner.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_git_utils.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_lockfile.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_onboarding.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_onboarding_helpers.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_onboarding_interactive.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_opencode_runner.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_opencode_schema.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_paths.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_pi_runner.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_pi_schema.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_plugins.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_projects_config.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_rendering.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_runner_contract.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_runner_run_options.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_runner_utils.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_runtime_loader.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_settings.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_settings_contract.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_subprocess.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_agent_trigger_commands.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_backend.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_bridge.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_chat_prefs.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_chat_sessions.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_client.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_client_api.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_context_helpers.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_engine_defaults.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_engine_overrides.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_file_transfer_helpers.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_files.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_incoming.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_media_command.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_polling.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_queue.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_topic_state.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_topics_command.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_topics_helpers.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_trigger_mode.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_telegram_voice.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_tool_actions.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_transport.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_transport_registry.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_transport_runtime.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/tests/test_worktrees.py +0 -0
- {yee88-0.6.3 → yee88-0.7.1}/zensical.toml +0 -0
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
# changelog
|
|
2
2
|
|
|
3
|
+
## v0.7.0 (2026-02-01)
|
|
4
|
+
|
|
5
|
+
### features
|
|
6
|
+
|
|
7
|
+
- add `/handoff` command to transfer desktop session to mobile seamlessly
|
|
8
|
+
|
|
9
|
+
### fixes
|
|
10
|
+
|
|
11
|
+
- fix scheduled task trigger issues in cron scheduler
|
|
12
|
+
|
|
13
|
+
## v0.6.3 (2026-02-01)
|
|
14
|
+
|
|
15
|
+
### features
|
|
16
|
+
|
|
17
|
+
- add `/model reset` subcommand to clear model overrides
|
|
18
|
+
|
|
3
19
|
## v0.6.2 (2026-01-31)
|
|
4
20
|
|
|
5
21
|
### changes
|
|
6
22
|
|
|
7
23
|
- optimize system_prompt handling: only prepend on first run to save tokens
|
|
8
24
|
|
|
25
|
+
## v0.6.1 (2026-01-31)
|
|
26
|
+
|
|
27
|
+
### fixes
|
|
28
|
+
|
|
29
|
+
- correct system_prompt syntax error in settings
|
|
30
|
+
|
|
9
31
|
## v0.6.0 (2026-01-31)
|
|
10
32
|
|
|
11
33
|
### features
|
|
12
34
|
|
|
13
|
-
- add one-time task execution support in cron
|
|
35
|
+
- add one-time task execution support in cron scheduler
|
|
14
36
|
|
|
15
37
|
### docs
|
|
16
38
|
|
|
@@ -20,11 +42,16 @@
|
|
|
20
42
|
|
|
21
43
|
- update system prompt wording
|
|
22
44
|
|
|
23
|
-
## v0.
|
|
45
|
+
## v0.5.0 (2026-01-31)
|
|
24
46
|
|
|
25
47
|
### changes
|
|
26
48
|
|
|
27
|
-
-
|
|
49
|
+
- fork baseline from upstream v0.21.4
|
|
50
|
+
- minor fixes and improvements
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## upstream changelog (v0.21.4 and earlier)
|
|
28
55
|
|
|
29
56
|
## v0.21.4 (2026-01-22)
|
|
30
57
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
yee88 是一个 Telegram 桥接工具,让你可以通过 Telegram 聊天界面来运行 AI 编程助手(Codex、Claude Code、OpenCode、Pi)。
|
|
4
4
|
|
|
5
|
+
**当前版本: v0.7.1**
|
|
6
|
+
|
|
5
7
|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
|
|
6
8
|
|
|
7
9
|
## 📦 一、安装与初始化(💻 电脑侧)
|
|
@@ -440,7 +442,77 @@ show_resume_line = true # 始终显示恢复行
|
|
|
440
442
|
|
|
441
443
|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
|
|
442
444
|
|
|
443
|
-
##
|
|
445
|
+
## 📲 十二、会话接力(Handoff)
|
|
446
|
+
|
|
447
|
+
将电脑端的 OpenCode 会话上下文发送到 Telegram,方便在手机上继续对话。
|
|
448
|
+
|
|
449
|
+
### 使用方法(💻 电脑侧)
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
yee88 handoff
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### 工作流程
|
|
456
|
+
|
|
457
|
+
1. 自动列出当前项目的最近会话(带话题名称)
|
|
458
|
+
2. 选择要接力的会话
|
|
459
|
+
3. 自动创建新的 Telegram Topic
|
|
460
|
+
4. 将会话上下文和最近消息发送到 Topic
|
|
461
|
+
5. 在 Telegram 中直接继续对话
|
|
462
|
+
|
|
463
|
+
### 选项
|
|
464
|
+
|
|
465
|
+
| 选项 | 说明 |
|
|
466
|
+
|------|------|
|
|
467
|
+
| `--session, -s` | 指定会话 ID(默认交互选择) |
|
|
468
|
+
| `--limit, -n` | 包含的消息数量(默认 3) |
|
|
469
|
+
| `--project, -p` | 项目名称 |
|
|
470
|
+
|
|
471
|
+
### 示例
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
# 交互式选择会话
|
|
475
|
+
yee88 handoff
|
|
476
|
+
|
|
477
|
+
# 指定会话和消息数量
|
|
478
|
+
yee88 handoff -s ses_abc123 -n 5
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### 前置条件
|
|
482
|
+
|
|
483
|
+
- 需要启用 Topics 模式:`yee88 config set transports.telegram.topics.enabled true`
|
|
484
|
+
- 需要配置 Telegram bot_token 和 chat_id
|
|
485
|
+
|
|
486
|
+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
|
|
487
|
+
|
|
488
|
+
## 🔀 十三、话题分叉(Fork)
|
|
489
|
+
|
|
490
|
+
在 Telegram Topics 模式下,将当前话题的上下文和会话状态分叉到一个新话题。
|
|
491
|
+
|
|
492
|
+
### 使用方法
|
|
493
|
+
|
|
494
|
+
在任意话题中发送:
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
/fork
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### 功能说明
|
|
501
|
+
|
|
502
|
+
- 自动创建新话题,标题格式:`原话题 (fork #1)`
|
|
503
|
+
- 支持多次分叉,自动递增编号:`原话题 (fork #2)`、`原话题 (fork #3)`...
|
|
504
|
+
- 复制原话题的上下文和会话状态到新话题
|
|
505
|
+
- 在新话题中继续对话,不影响原话题
|
|
506
|
+
|
|
507
|
+
### 使用场景
|
|
508
|
+
|
|
509
|
+
- 从当前对话中派生新的探索方向
|
|
510
|
+
- 保存当前状态,尝试不同的解决方案
|
|
511
|
+
- 多人协作时创建个人工作分支
|
|
512
|
+
|
|
513
|
+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
|
|
514
|
+
|
|
515
|
+
## 📝 十四、使用技巧
|
|
444
516
|
|
|
445
517
|
### 1. 快速切换上下文
|
|
446
518
|
|
|
@@ -479,3 +551,20 @@ codex resume <token>
|
|
|
479
551
|
/claude 优化代码结构
|
|
480
552
|
/opencode 添加测试
|
|
481
553
|
```
|
|
554
|
+
|
|
555
|
+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
|
|
556
|
+
|
|
557
|
+
## 📋 版本历史
|
|
558
|
+
|
|
559
|
+
### v0.7.1 (2025-02-01)
|
|
560
|
+
|
|
561
|
+
**新功能**
|
|
562
|
+
- ✨ **话题分叉 (`/fork`)**: 在 Topics 模式下将当前话题上下文分叉到新话题,支持多次分叉自动编号
|
|
563
|
+
- ✨ **会话接力 (`yee88 handoff`)**: 将电脑端 OpenCode 会话发送到 Telegram 继续对话
|
|
564
|
+
|
|
565
|
+
**改进**
|
|
566
|
+
- 优化 handoff 命令的输出提示信息
|
|
567
|
+
|
|
568
|
+
### v0.7.0
|
|
569
|
+
|
|
570
|
+
- 初始版本发布
|
|
@@ -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,304 @@
|
|
|
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📲 会话接力 - 将电脑端会话发送到 Telegram 继续对话")
|
|
227
|
+
typer.echo("━" * 50)
|
|
228
|
+
typer.echo("\n📋 最近的会话:\n")
|
|
229
|
+
for i, s in enumerate(sessions[:10], 1):
|
|
230
|
+
title_display = s.title[:40] if s.title else s.project_name
|
|
231
|
+
typer.echo(f" [{i}] {s.updated_str} {title_display}")
|
|
232
|
+
typer.echo("")
|
|
233
|
+
|
|
234
|
+
choice = typer.prompt("选择会话 (1-10)", default="1")
|
|
235
|
+
try:
|
|
236
|
+
idx = int(choice) - 1
|
|
237
|
+
if idx < 0 or idx >= len(sessions):
|
|
238
|
+
typer.echo("❌ 无效选择", err=True)
|
|
239
|
+
raise typer.Exit(1)
|
|
240
|
+
except ValueError:
|
|
241
|
+
typer.echo("❌ 请输入数字", err=True)
|
|
242
|
+
raise typer.Exit(1)
|
|
243
|
+
|
|
244
|
+
selected = sessions[idx]
|
|
245
|
+
session_id = selected.id
|
|
246
|
+
if session_project is None:
|
|
247
|
+
session_project = selected.project_name
|
|
248
|
+
|
|
249
|
+
if not session_id:
|
|
250
|
+
typer.echo("❌ 会话 ID 为空", err=True)
|
|
251
|
+
raise typer.Exit(1)
|
|
252
|
+
|
|
253
|
+
typer.echo(f"📖 读取会话 {session_id[:20]}...")
|
|
254
|
+
|
|
255
|
+
messages = _get_session_messages(session_id, limit=limit)
|
|
256
|
+
if not messages:
|
|
257
|
+
typer.echo("❌ 无法读取会话消息", err=True)
|
|
258
|
+
raise typer.Exit(1)
|
|
259
|
+
|
|
260
|
+
typer.echo("🆕 创建新 Topic...")
|
|
261
|
+
|
|
262
|
+
async def do_handoff() -> tuple[bool, int | None]:
|
|
263
|
+
thread_id = await _create_handoff_topic(
|
|
264
|
+
token=token,
|
|
265
|
+
chat_id=chat_id,
|
|
266
|
+
session_id=session_id,
|
|
267
|
+
project=session_project or "unknown",
|
|
268
|
+
config_path=config_path,
|
|
269
|
+
)
|
|
270
|
+
if thread_id is None:
|
|
271
|
+
return False, None
|
|
272
|
+
|
|
273
|
+
handoff_msg = _format_handoff_message(
|
|
274
|
+
session_id=session_id,
|
|
275
|
+
messages=messages,
|
|
276
|
+
project=session_project,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
success = await _send_to_telegram(
|
|
280
|
+
token=token,
|
|
281
|
+
chat_id=chat_id,
|
|
282
|
+
message=handoff_msg,
|
|
283
|
+
thread_id=thread_id,
|
|
284
|
+
)
|
|
285
|
+
return success, thread_id
|
|
286
|
+
|
|
287
|
+
success, thread_id = anyio.run(do_handoff)
|
|
288
|
+
|
|
289
|
+
if success:
|
|
290
|
+
typer.echo("✅ 已发送到 Telegram!")
|
|
291
|
+
typer.echo(f" Session: {session_id}")
|
|
292
|
+
typer.echo(f" Project: {session_project}")
|
|
293
|
+
typer.echo(f" Topic ID: {thread_id}")
|
|
294
|
+
typer.echo(f" 消息数: {limit}")
|
|
295
|
+
else:
|
|
296
|
+
typer.echo("❌ 发送失败", err=True)
|
|
297
|
+
raise typer.Exit(1)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@app.callback(invoke_without_command=True)
|
|
301
|
+
def main(ctx: typer.Context) -> None:
|
|
302
|
+
"""Handoff session context to Telegram for mobile continuation."""
|
|
303
|
+
if ctx.invoked_subcommand is None:
|
|
304
|
+
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
|
|
|
@@ -7,7 +7,7 @@ _ID_RE = re.compile(ID_PATTERN)
|
|
|
7
7
|
|
|
8
8
|
RESERVED_CLI_COMMANDS = frozenset({"config", "doctor", "init", "plugins"})
|
|
9
9
|
RESERVED_CHAT_COMMANDS = frozenset(
|
|
10
|
-
{"cancel", "file", "new", "agent", "model", "reasoning", "trigger", "topic", "ctx"}
|
|
10
|
+
{"cancel", "file", "new", "fork", "agent", "model", "reasoning", "trigger", "topic", "ctx"}
|
|
11
11
|
)
|
|
12
12
|
RESERVED_ENGINE_IDS = RESERVED_CLI_COMMANDS | RESERVED_CHAT_COMMANDS
|
|
13
13
|
RESERVED_COMMAND_IDS = RESERVED_CLI_COMMANDS | RESERVED_CHAT_COMMANDS
|