agim-cli 1.0.1
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.
- package/CHANGELOG.md +1234 -0
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/README.zh-CN.md +414 -0
- package/dist/cli-ui/cmd-handlers.d.ts +11 -0
- package/dist/cli-ui/cmd-handlers.d.ts.map +1 -0
- package/dist/cli-ui/cmd-handlers.js +240 -0
- package/dist/cli-ui/cmd-handlers.js.map +1 -0
- package/dist/cli-ui/config-wizard.d.ts +3 -0
- package/dist/cli-ui/config-wizard.d.ts.map +1 -0
- package/dist/cli-ui/config-wizard.js +851 -0
- package/dist/cli-ui/config-wizard.js.map +1 -0
- package/dist/cli-ui/entry-menu.d.ts +28 -0
- package/dist/cli-ui/entry-menu.d.ts.map +1 -0
- package/dist/cli-ui/entry-menu.js +50 -0
- package/dist/cli-ui/entry-menu.js.map +1 -0
- package/dist/cli-ui/env-file.d.ts +35 -0
- package/dist/cli-ui/env-file.d.ts.map +1 -0
- package/dist/cli-ui/env-file.js +163 -0
- package/dist/cli-ui/env-file.js.map +1 -0
- package/dist/cli-ui/i18n.d.ts +204 -0
- package/dist/cli-ui/i18n.d.ts.map +1 -0
- package/dist/cli-ui/i18n.js +455 -0
- package/dist/cli-ui/i18n.js.map +1 -0
- package/dist/cli-ui/lang-picker.d.ts +10 -0
- package/dist/cli-ui/lang-picker.d.ts.map +1 -0
- package/dist/cli-ui/lang-picker.js +33 -0
- package/dist/cli-ui/lang-picker.js.map +1 -0
- package/dist/cli-ui/paths.d.ts +4 -0
- package/dist/cli-ui/paths.d.ts.map +1 -0
- package/dist/cli-ui/paths.js +11 -0
- package/dist/cli-ui/paths.js.map +1 -0
- package/dist/cli-ui/prompts.d.ts +65 -0
- package/dist/cli-ui/prompts.d.ts.map +1 -0
- package/dist/cli-ui/prompts.js +125 -0
- package/dist/cli-ui/prompts.js.map +1 -0
- package/dist/cli-ui/service.d.ts +41 -0
- package/dist/cli-ui/service.d.ts.map +1 -0
- package/dist/cli-ui/service.js +241 -0
- package/dist/cli-ui/service.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1143 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/acp-server.d.ts +8 -0
- package/dist/core/acp-server.d.ts.map +1 -0
- package/dist/core/acp-server.js +266 -0
- package/dist/core/acp-server.js.map +1 -0
- package/dist/core/agent-base.d.ts +94 -0
- package/dist/core/agent-base.d.ts.map +1 -0
- package/dist/core/agent-base.js +373 -0
- package/dist/core/agent-base.js.map +1 -0
- package/dist/core/agent-cwd.d.ts +48 -0
- package/dist/core/agent-cwd.d.ts.map +1 -0
- package/dist/core/agent-cwd.js +181 -0
- package/dist/core/agent-cwd.js.map +1 -0
- package/dist/core/agent-helper.d.ts +65 -0
- package/dist/core/agent-helper.d.ts.map +1 -0
- package/dist/core/agent-helper.js +150 -0
- package/dist/core/agent-helper.js.map +1 -0
- package/dist/core/agim-paths.d.ts +10 -0
- package/dist/core/agim-paths.d.ts.map +1 -0
- package/dist/core/agim-paths.js +64 -0
- package/dist/core/agim-paths.js.map +1 -0
- package/dist/core/approval-bus.d.ts +300 -0
- package/dist/core/approval-bus.d.ts.map +1 -0
- package/dist/core/approval-bus.js +990 -0
- package/dist/core/approval-bus.js.map +1 -0
- package/dist/core/approval-router.d.ts +101 -0
- package/dist/core/approval-router.d.ts.map +1 -0
- package/dist/core/approval-router.js +540 -0
- package/dist/core/approval-router.js.map +1 -0
- package/dist/core/audit-log.d.ts +55 -0
- package/dist/core/audit-log.d.ts.map +1 -0
- package/dist/core/audit-log.js +203 -0
- package/dist/core/audit-log.js.map +1 -0
- package/dist/core/bgjob-reader.d.ts +65 -0
- package/dist/core/bgjob-reader.d.ts.map +1 -0
- package/dist/core/bgjob-reader.js +212 -0
- package/dist/core/bgjob-reader.js.map +1 -0
- package/dist/core/circuit-breaker.d.ts +37 -0
- package/dist/core/circuit-breaker.d.ts.map +1 -0
- package/dist/core/circuit-breaker.js +115 -0
- package/dist/core/circuit-breaker.js.map +1 -0
- package/dist/core/commands/agent.d.ts +4 -0
- package/dist/core/commands/agent.d.ts.map +1 -0
- package/dist/core/commands/agent.js +40 -0
- package/dist/core/commands/agent.js.map +1 -0
- package/dist/core/commands/approval.d.ts +3 -0
- package/dist/core/commands/approval.d.ts.map +1 -0
- package/dist/core/commands/approval.js +85 -0
- package/dist/core/commands/approval.js.map +1 -0
- package/dist/core/commands/audit.d.ts +3 -0
- package/dist/core/commands/audit.d.ts.map +1 -0
- package/dist/core/commands/audit.js +84 -0
- package/dist/core/commands/audit.js.map +1 -0
- package/dist/core/commands/builtin.d.ts +3 -0
- package/dist/core/commands/builtin.d.ts.map +1 -0
- package/dist/core/commands/builtin.js +304 -0
- package/dist/core/commands/builtin.js.map +1 -0
- package/dist/core/commands/cron.d.ts +3 -0
- package/dist/core/commands/cron.d.ts.map +1 -0
- package/dist/core/commands/cron.js +128 -0
- package/dist/core/commands/cron.js.map +1 -0
- package/dist/core/commands/job.d.ts +3 -0
- package/dist/core/commands/job.d.ts.map +1 -0
- package/dist/core/commands/job.js +195 -0
- package/dist/core/commands/job.js.map +1 -0
- package/dist/core/commands/memo.d.ts +3 -0
- package/dist/core/commands/memo.d.ts.map +1 -0
- package/dist/core/commands/memo.js +151 -0
- package/dist/core/commands/memo.js.map +1 -0
- package/dist/core/commands/model.d.ts +9 -0
- package/dist/core/commands/model.d.ts.map +1 -0
- package/dist/core/commands/model.js +183 -0
- package/dist/core/commands/model.js.map +1 -0
- package/dist/core/commands/plan.d.ts +3 -0
- package/dist/core/commands/plan.d.ts.map +1 -0
- package/dist/core/commands/plan.js +75 -0
- package/dist/core/commands/plan.js.map +1 -0
- package/dist/core/commands/remind.d.ts +3 -0
- package/dist/core/commands/remind.d.ts.map +1 -0
- package/dist/core/commands/remind.js +271 -0
- package/dist/core/commands/remind.js.map +1 -0
- package/dist/core/commands/router.d.ts +3 -0
- package/dist/core/commands/router.d.ts.map +1 -0
- package/dist/core/commands/router.js +71 -0
- package/dist/core/commands/router.js.map +1 -0
- package/dist/core/commands/sessions.d.ts +3 -0
- package/dist/core/commands/sessions.d.ts.map +1 -0
- package/dist/core/commands/sessions.js +88 -0
- package/dist/core/commands/sessions.js.map +1 -0
- package/dist/core/commands/stats.d.ts +3 -0
- package/dist/core/commands/stats.d.ts.map +1 -0
- package/dist/core/commands/stats.js +73 -0
- package/dist/core/commands/stats.js.map +1 -0
- package/dist/core/commands/think.d.ts +3 -0
- package/dist/core/commands/think.d.ts.map +1 -0
- package/dist/core/commands/think.js +28 -0
- package/dist/core/commands/think.js.map +1 -0
- package/dist/core/commands/workspaces.d.ts +3 -0
- package/dist/core/commands/workspaces.d.ts.map +1 -0
- package/dist/core/commands/workspaces.js +47 -0
- package/dist/core/commands/workspaces.js.map +1 -0
- package/dist/core/config-schema.d.ts +60 -0
- package/dist/core/config-schema.d.ts.map +1 -0
- package/dist/core/config-schema.js +75 -0
- package/dist/core/config-schema.js.map +1 -0
- package/dist/core/coord-systems.d.ts +65 -0
- package/dist/core/coord-systems.d.ts.map +1 -0
- package/dist/core/coord-systems.js +229 -0
- package/dist/core/coord-systems.js.map +1 -0
- package/dist/core/cron.d.ts +29 -0
- package/dist/core/cron.d.ts.map +1 -0
- package/dist/core/cron.js +184 -0
- package/dist/core/cron.js.map +1 -0
- package/dist/core/event-bus.d.ts +80 -0
- package/dist/core/event-bus.d.ts.map +1 -0
- package/dist/core/event-bus.js +62 -0
- package/dist/core/event-bus.js.map +1 -0
- package/dist/core/intent-llm.d.ts +27 -0
- package/dist/core/intent-llm.d.ts.map +1 -0
- package/dist/core/intent-llm.js +170 -0
- package/dist/core/intent-llm.js.map +1 -0
- package/dist/core/intent.d.ts +12 -0
- package/dist/core/intent.d.ts.map +1 -0
- package/dist/core/intent.js +187 -0
- package/dist/core/intent.js.map +1 -0
- package/dist/core/job-board.d.ts +82 -0
- package/dist/core/job-board.d.ts.map +1 -0
- package/dist/core/job-board.js +379 -0
- package/dist/core/job-board.js.map +1 -0
- package/dist/core/location-context.d.ts +32 -0
- package/dist/core/location-context.d.ts.map +1 -0
- package/dist/core/location-context.js +69 -0
- package/dist/core/location-context.js.map +1 -0
- package/dist/core/location-token.d.ts +57 -0
- package/dist/core/location-token.d.ts.map +1 -0
- package/dist/core/location-token.js +128 -0
- package/dist/core/location-token.js.map +1 -0
- package/dist/core/logger.d.ts +6 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +54 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/memo-rpc.d.ts +13 -0
- package/dist/core/memo-rpc.d.ts.map +1 -0
- package/dist/core/memo-rpc.js +288 -0
- package/dist/core/memo-rpc.js.map +1 -0
- package/dist/core/memos.d.ts +163 -0
- package/dist/core/memos.d.ts.map +1 -0
- package/dist/core/memos.js +502 -0
- package/dist/core/memos.js.map +1 -0
- package/dist/core/metrics.d.ts +55 -0
- package/dist/core/metrics.d.ts.map +1 -0
- package/dist/core/metrics.js +291 -0
- package/dist/core/metrics.js.map +1 -0
- package/dist/core/onboarding.d.ts +99 -0
- package/dist/core/onboarding.d.ts.map +1 -0
- package/dist/core/onboarding.js +426 -0
- package/dist/core/onboarding.js.map +1 -0
- package/dist/core/pending-reminder.d.ts +25 -0
- package/dist/core/pending-reminder.d.ts.map +1 -0
- package/dist/core/pending-reminder.js +53 -0
- package/dist/core/pending-reminder.js.map +1 -0
- package/dist/core/rate-limiter.d.ts +44 -0
- package/dist/core/rate-limiter.d.ts.map +1 -0
- package/dist/core/rate-limiter.js +115 -0
- package/dist/core/rate-limiter.js.map +1 -0
- package/dist/core/registry.d.ts +32 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +126 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/remind-intent.d.ts +25 -0
- package/dist/core/remind-intent.d.ts.map +1 -0
- package/dist/core/remind-intent.js +196 -0
- package/dist/core/remind-intent.js.map +1 -0
- package/dist/core/reminder-rpc.d.ts +17 -0
- package/dist/core/reminder-rpc.d.ts.map +1 -0
- package/dist/core/reminder-rpc.js +169 -0
- package/dist/core/reminder-rpc.js.map +1 -0
- package/dist/core/reminders.d.ts +159 -0
- package/dist/core/reminders.d.ts.map +1 -0
- package/dist/core/reminders.js +977 -0
- package/dist/core/reminders.js.map +1 -0
- package/dist/core/router.d.ts +55 -0
- package/dist/core/router.d.ts.map +1 -0
- package/dist/core/router.js +497 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/schedule.d.ts +65 -0
- package/dist/core/schedule.d.ts.map +1 -0
- package/dist/core/schedule.js +323 -0
- package/dist/core/schedule.js.map +1 -0
- package/dist/core/session.d.ts +182 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +807 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/sqlite-helper.d.ts +37 -0
- package/dist/core/sqlite-helper.d.ts.map +1 -0
- package/dist/core/sqlite-helper.js +79 -0
- package/dist/core/sqlite-helper.js.map +1 -0
- package/dist/core/transcribe.d.ts +25 -0
- package/dist/core/transcribe.d.ts.map +1 -0
- package/dist/core/transcribe.js +217 -0
- package/dist/core/transcribe.js.map +1 -0
- package/dist/core/types.d.ts +360 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/workspace.d.ts +67 -0
- package/dist/core/workspace.d.ts.map +1 -0
- package/dist/core/workspace.js +113 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/agents/acp/acp-adapter.d.ts +16 -0
- package/dist/plugins/agents/acp/acp-adapter.d.ts.map +1 -0
- package/dist/plugins/agents/acp/acp-adapter.js +49 -0
- package/dist/plugins/agents/acp/acp-adapter.js.map +1 -0
- package/dist/plugins/agents/acp/acp-client.d.ts +32 -0
- package/dist/plugins/agents/acp/acp-client.d.ts.map +1 -0
- package/dist/plugins/agents/acp/acp-client.js +177 -0
- package/dist/plugins/agents/acp/acp-client.js.map +1 -0
- package/dist/plugins/agents/acp/discovery.d.ts +19 -0
- package/dist/plugins/agents/acp/discovery.d.ts.map +1 -0
- package/dist/plugins/agents/acp/discovery.js +111 -0
- package/dist/plugins/agents/acp/discovery.js.map +1 -0
- package/dist/plugins/agents/acp/index.d.ts +4 -0
- package/dist/plugins/agents/acp/index.d.ts.map +1 -0
- package/dist/plugins/agents/acp/index.js +4 -0
- package/dist/plugins/agents/acp/index.js.map +1 -0
- package/dist/plugins/agents/acp/types.d.ts +62 -0
- package/dist/plugins/agents/acp/types.d.ts.map +1 -0
- package/dist/plugins/agents/acp/types.js +5 -0
- package/dist/plugins/agents/acp/types.js.map +1 -0
- package/dist/plugins/agents/claude-code/index.d.ts +25 -0
- package/dist/plugins/agents/claude-code/index.d.ts.map +1 -0
- package/dist/plugins/agents/claude-code/index.js +184 -0
- package/dist/plugins/agents/claude-code/index.js.map +1 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +59 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.js +645 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -0
- package/dist/plugins/agents/codex/build-mcp-cli-args.d.ts +28 -0
- package/dist/plugins/agents/codex/build-mcp-cli-args.d.ts.map +1 -0
- package/dist/plugins/agents/codex/build-mcp-cli-args.js +74 -0
- package/dist/plugins/agents/codex/build-mcp-cli-args.js.map +1 -0
- package/dist/plugins/agents/codex/index.d.ts +53 -0
- package/dist/plugins/agents/codex/index.d.ts.map +1 -0
- package/dist/plugins/agents/codex/index.js +341 -0
- package/dist/plugins/agents/codex/index.js.map +1 -0
- package/dist/plugins/agents/copilot/index.d.ts +35 -0
- package/dist/plugins/agents/copilot/index.d.ts.map +1 -0
- package/dist/plugins/agents/copilot/index.js +182 -0
- package/dist/plugins/agents/copilot/index.js.map +1 -0
- package/dist/plugins/agents/opencode/ensure-mcp-config.d.ts +11 -0
- package/dist/plugins/agents/opencode/ensure-mcp-config.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/ensure-mcp-config.js +100 -0
- package/dist/plugins/agents/opencode/ensure-mcp-config.js.map +1 -0
- package/dist/plugins/agents/opencode/index.d.ts +5 -0
- package/dist/plugins/agents/opencode/index.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/index.js +30 -0
- package/dist/plugins/agents/opencode/index.js.map +1 -0
- package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts +166 -0
- package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/opencode-http-adapter.js +682 -0
- package/dist/plugins/agents/opencode/opencode-http-adapter.js.map +1 -0
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts +32 -0
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.js +137 -0
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.js.map +1 -0
- package/dist/plugins/agents/opencode/serve-manager.d.ts +27 -0
- package/dist/plugins/agents/opencode/serve-manager.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/serve-manager.js +194 -0
- package/dist/plugins/agents/opencode/serve-manager.js.map +1 -0
- package/dist/plugins/messengers/dingtalk/dingtalk-adapter.d.ts +57 -0
- package/dist/plugins/messengers/dingtalk/dingtalk-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/dingtalk/dingtalk-adapter.js +409 -0
- package/dist/plugins/messengers/dingtalk/dingtalk-adapter.js.map +1 -0
- package/dist/plugins/messengers/dingtalk/dingtalk-client.d.ts +48 -0
- package/dist/plugins/messengers/dingtalk/dingtalk-client.d.ts.map +1 -0
- package/dist/plugins/messengers/dingtalk/dingtalk-client.js +236 -0
- package/dist/plugins/messengers/dingtalk/dingtalk-client.js.map +1 -0
- package/dist/plugins/messengers/dingtalk/index.d.ts +3 -0
- package/dist/plugins/messengers/dingtalk/index.d.ts.map +1 -0
- package/dist/plugins/messengers/dingtalk/index.js +3 -0
- package/dist/plugins/messengers/dingtalk/index.js.map +1 -0
- package/dist/plugins/messengers/dingtalk/link-coords.d.ts +23 -0
- package/dist/plugins/messengers/dingtalk/link-coords.d.ts.map +1 -0
- package/dist/plugins/messengers/dingtalk/link-coords.js +89 -0
- package/dist/plugins/messengers/dingtalk/link-coords.js.map +1 -0
- package/dist/plugins/messengers/dingtalk/media-store.d.ts +16 -0
- package/dist/plugins/messengers/dingtalk/media-store.d.ts.map +1 -0
- package/dist/plugins/messengers/dingtalk/media-store.js +77 -0
- package/dist/plugins/messengers/dingtalk/media-store.js.map +1 -0
- package/dist/plugins/messengers/dingtalk/types.d.ts +82 -0
- package/dist/plugins/messengers/dingtalk/types.d.ts.map +1 -0
- package/dist/plugins/messengers/dingtalk/types.js +14 -0
- package/dist/plugins/messengers/dingtalk/types.js.map +1 -0
- package/dist/plugins/messengers/discord/discord-adapter.d.ts +21 -0
- package/dist/plugins/messengers/discord/discord-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/discord-adapter.js +238 -0
- package/dist/plugins/messengers/discord/discord-adapter.js.map +1 -0
- package/dist/plugins/messengers/discord/index.d.ts +4 -0
- package/dist/plugins/messengers/discord/index.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/index.js +4 -0
- package/dist/plugins/messengers/discord/index.js.map +1 -0
- package/dist/plugins/messengers/discord/markdown-to-discord.d.ts +11 -0
- package/dist/plugins/messengers/discord/markdown-to-discord.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/markdown-to-discord.js +59 -0
- package/dist/plugins/messengers/discord/markdown-to-discord.js.map +1 -0
- package/dist/plugins/messengers/discord/types.d.ts +9 -0
- package/dist/plugins/messengers/discord/types.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/types.js +3 -0
- package/dist/plugins/messengers/discord/types.js.map +1 -0
- package/dist/plugins/messengers/email/email-adapter.d.ts +33 -0
- package/dist/plugins/messengers/email/email-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/email/email-adapter.js +137 -0
- package/dist/plugins/messengers/email/email-adapter.js.map +1 -0
- package/dist/plugins/messengers/feishu/card-builder.d.ts +23 -0
- package/dist/plugins/messengers/feishu/card-builder.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/card-builder.js +89 -0
- package/dist/plugins/messengers/feishu/card-builder.js.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-adapter.d.ts +23 -0
- package/dist/plugins/messengers/feishu/feishu-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-adapter.js +250 -0
- package/dist/plugins/messengers/feishu/feishu-adapter.js.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-client.d.ts +43 -0
- package/dist/plugins/messengers/feishu/feishu-client.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-client.js +118 -0
- package/dist/plugins/messengers/feishu/feishu-client.js.map +1 -0
- package/dist/plugins/messengers/feishu/index.d.ts +4 -0
- package/dist/plugins/messengers/feishu/index.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/index.js +4 -0
- package/dist/plugins/messengers/feishu/index.js.map +1 -0
- package/dist/plugins/messengers/feishu/types.d.ts +113 -0
- package/dist/plugins/messengers/feishu/types.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/types.js +4 -0
- package/dist/plugins/messengers/feishu/types.js.map +1 -0
- package/dist/plugins/messengers/telegram/index.d.ts +4 -0
- package/dist/plugins/messengers/telegram/index.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/index.js +4 -0
- package/dist/plugins/messengers/telegram/index.js.map +1 -0
- package/dist/plugins/messengers/telegram/markdown-to-html.d.ts +5 -0
- package/dist/plugins/messengers/telegram/markdown-to-html.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/markdown-to-html.js +186 -0
- package/dist/plugins/messengers/telegram/markdown-to-html.js.map +1 -0
- package/dist/plugins/messengers/telegram/media-download.d.ts +59 -0
- package/dist/plugins/messengers/telegram/media-download.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/media-download.js +228 -0
- package/dist/plugins/messengers/telegram/media-download.js.map +1 -0
- package/dist/plugins/messengers/telegram/telegram-adapter.d.ts +77 -0
- package/dist/plugins/messengers/telegram/telegram-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/telegram-adapter.js +880 -0
- package/dist/plugins/messengers/telegram/telegram-adapter.js.map +1 -0
- package/dist/plugins/messengers/telegram/types.d.ts +47 -0
- package/dist/plugins/messengers/telegram/types.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/types.js +3 -0
- package/dist/plugins/messengers/telegram/types.js.map +1 -0
- package/dist/plugins/messengers/wechat/context-store.d.ts +18 -0
- package/dist/plugins/messengers/wechat/context-store.d.ts.map +1 -0
- package/dist/plugins/messengers/wechat/context-store.js +105 -0
- package/dist/plugins/messengers/wechat/context-store.js.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.d.ts +71 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.js +664 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.js.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-client.d.ts +75 -0
- package/dist/plugins/messengers/wechat/ilink-client.d.ts.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-client.js +331 -0
- package/dist/plugins/messengers/wechat/ilink-client.js.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-types.d.ts +181 -0
- package/dist/plugins/messengers/wechat/ilink-types.d.ts.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-types.js +22 -0
- package/dist/plugins/messengers/wechat/ilink-types.js.map +1 -0
- package/dist/plugins/messengers/wechat/media-download.d.ts +32 -0
- package/dist/plugins/messengers/wechat/media-download.d.ts.map +1 -0
- package/dist/plugins/messengers/wechat/media-download.js +78 -0
- package/dist/plugins/messengers/wechat/media-download.js.map +1 -0
- package/dist/scripts/migrate-gcj02-to-wgs84.d.ts +3 -0
- package/dist/scripts/migrate-gcj02-to-wgs84.d.ts.map +1 -0
- package/dist/scripts/migrate-gcj02-to-wgs84.js +52 -0
- package/dist/scripts/migrate-gcj02-to-wgs84.js.map +1 -0
- package/dist/utils/backoff.d.ts +35 -0
- package/dist/utils/backoff.d.ts.map +1 -0
- package/dist/utils/backoff.js +59 -0
- package/dist/utils/backoff.js.map +1 -0
- package/dist/utils/cross-platform.d.ts +26 -0
- package/dist/utils/cross-platform.d.ts.map +1 -0
- package/dist/utils/cross-platform.js +58 -0
- package/dist/utils/cross-platform.js.map +1 -0
- package/dist/utils/message-split.d.ts +14 -0
- package/dist/utils/message-split.d.ts.map +1 -0
- package/dist/utils/message-split.js +65 -0
- package/dist/utils/message-split.js.map +1 -0
- package/dist/utils/safe-equal.d.ts +2 -0
- package/dist/utils/safe-equal.d.ts.map +1 -0
- package/dist/utils/safe-equal.js +11 -0
- package/dist/utils/safe-equal.js.map +1 -0
- package/dist/web/public/_app.js +196 -0
- package/dist/web/public/index.html +936 -0
- package/dist/web/public/loc.html +305 -0
- package/dist/web/public/login.html +106 -0
- package/dist/web/public/memos.html +271 -0
- package/dist/web/public/reminders.html +234 -0
- package/dist/web/public/settings.html +1355 -0
- package/dist/web/public/tasks.html +1835 -0
- package/dist/web/server.d.ts +12 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +2399 -0
- package/dist/web/server.js.map +1 -0
- package/package.json +92 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
// Reminders — lightweight, one-shot, persistent timers.
|
|
2
|
+
//
|
|
3
|
+
// Scope: "remind me to drink water in 2 minutes". Distinct from schedule.ts
|
|
4
|
+
// (cron-driven, agent-spawning) — reminders fire once and just deliver a
|
|
5
|
+
// plain text back to the originating IM thread via the registry's
|
|
6
|
+
// MessengerAdapter.sendMessage. No LLM is invoked, no Job is created.
|
|
7
|
+
//
|
|
8
|
+
// Storage: a dedicated SQLite file (~/.im-hub/reminders.db) using the
|
|
9
|
+
// same fail-soft helper as audit-log / job-board / schedule. Engine is a
|
|
10
|
+
// 5-second tick; pending rows survive process restart.
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { AGIM_HOME } from './agim-paths.js';
|
|
13
|
+
import { logger as rootLogger } from './logger.js';
|
|
14
|
+
import { createSqliteHelper } from './sqlite-helper.js';
|
|
15
|
+
import { registry } from './registry.js';
|
|
16
|
+
import { callHelperAgent } from './agent-helper.js';
|
|
17
|
+
const REMINDERS_DB = process.env.IMHUB_REMINDERS_DB
|
|
18
|
+
?? join(AGIM_HOME, 'reminders.db');
|
|
19
|
+
const TICK_INTERVAL_MS = 5 * 1000;
|
|
20
|
+
const BATCH_SIZE = 50;
|
|
21
|
+
export const MAX_PENDING_PER_USER = 50;
|
|
22
|
+
export const MIN_FUTURE_MS = 10 * 1000; // ≥ 10s
|
|
23
|
+
export const MAX_FUTURE_MS = 7 * 24 * 60 * 60 * 1000; // ≤ 7d
|
|
24
|
+
export const MAX_TEXT_LEN = 1000;
|
|
25
|
+
const LATE_THRESHOLD_MS = 60 * 60 * 1000; // > 1h late → tag delivery as ⏰ delayed
|
|
26
|
+
const log = rootLogger.child({ component: 'reminders' });
|
|
27
|
+
let tickTimer = null;
|
|
28
|
+
// ─── Schema ──────────────────────────────────────────────────────────
|
|
29
|
+
const helper = createSqliteHelper({
|
|
30
|
+
file: REMINDERS_DB,
|
|
31
|
+
component: 'reminders',
|
|
32
|
+
logger: rootLogger,
|
|
33
|
+
schema: `
|
|
34
|
+
CREATE TABLE IF NOT EXISTS reminders (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
fire_at TEXT NOT NULL,
|
|
37
|
+
text TEXT NOT NULL,
|
|
38
|
+
platform TEXT NOT NULL,
|
|
39
|
+
channel_id TEXT NOT NULL,
|
|
40
|
+
thread_id TEXT NOT NULL,
|
|
41
|
+
user_id TEXT NOT NULL DEFAULT '',
|
|
42
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
43
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
44
|
+
fired_at TEXT,
|
|
45
|
+
error TEXT,
|
|
46
|
+
source TEXT NOT NULL DEFAULT 'slash',
|
|
47
|
+
parent_id INTEGER,
|
|
48
|
+
recurrence TEXT,
|
|
49
|
+
last_fired_at TEXT,
|
|
50
|
+
prompt_mode TEXT NOT NULL DEFAULT 'llm'
|
|
51
|
+
);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_reminders_fire_at ON reminders(fire_at, status);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_reminders_user ON reminders(user_id, status);
|
|
54
|
+
|
|
55
|
+
-- Email bindings: (platform, user_id) -> email. Co-located here because
|
|
56
|
+
-- it's the same lifetime / ownership scope as a reminder owner. One row
|
|
57
|
+
-- per IM-user; UPSERT on rebind. Unbind = DELETE.
|
|
58
|
+
CREATE TABLE IF NOT EXISTS user_email_bindings (
|
|
59
|
+
platform TEXT NOT NULL,
|
|
60
|
+
user_id TEXT NOT NULL,
|
|
61
|
+
email TEXT NOT NULL,
|
|
62
|
+
bound_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
63
|
+
PRIMARY KEY (platform, user_id)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
-- Reminder-intent watch settings: per-IM-user toggle for the LLM
|
|
67
|
+
-- detector that watches non-slash messages and proposes reminders.
|
|
68
|
+
-- Default on (no row = enabled). Off via /remind aiwatch off.
|
|
69
|
+
CREATE TABLE IF NOT EXISTS user_aiwatch_settings (
|
|
70
|
+
platform TEXT NOT NULL,
|
|
71
|
+
user_id TEXT NOT NULL,
|
|
72
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
73
|
+
set_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
74
|
+
PRIMARY KEY (platform, user_id)
|
|
75
|
+
);
|
|
76
|
+
`,
|
|
77
|
+
init: (d) => {
|
|
78
|
+
// Migration: add recurrence + last_fired_at columns to existing DBs.
|
|
79
|
+
const cols = d.pragma('table_info(reminders)');
|
|
80
|
+
const have = new Set(cols.map((c) => c.name));
|
|
81
|
+
if (!have.has('recurrence'))
|
|
82
|
+
d.exec('ALTER TABLE reminders ADD COLUMN recurrence TEXT');
|
|
83
|
+
if (!have.has('last_fired_at'))
|
|
84
|
+
d.exec('ALTER TABLE reminders ADD COLUMN last_fired_at TEXT');
|
|
85
|
+
if (!have.has('prompt_mode')) {
|
|
86
|
+
d.exec("ALTER TABLE reminders ADD COLUMN prompt_mode TEXT NOT NULL DEFAULT 'llm'");
|
|
87
|
+
}
|
|
88
|
+
// Crash recovery: any row stuck in 'firing' means we crashed mid-deliver.
|
|
89
|
+
// Reset to 'pending' so the next tick retries.
|
|
90
|
+
d.prepare("UPDATE reminders SET status = 'pending' WHERE status = 'firing'").run();
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
function getDb() {
|
|
94
|
+
return helper.get();
|
|
95
|
+
}
|
|
96
|
+
export class ReminderError extends Error {
|
|
97
|
+
code;
|
|
98
|
+
constructor(code, msg) {
|
|
99
|
+
super(msg);
|
|
100
|
+
this.code = code;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ─── Time parsing ────────────────────────────────────────────────────
|
|
104
|
+
/** "2m" / "90s" / "3h" / "1d" → milliseconds. Rejects 0/negative/non-int. */
|
|
105
|
+
export function parseDuration(s) {
|
|
106
|
+
const m = /^(\d+)(s|m|h|d)$/.exec(s.trim());
|
|
107
|
+
if (!m)
|
|
108
|
+
return null;
|
|
109
|
+
const n = Number.parseInt(m[1], 10);
|
|
110
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
111
|
+
return null;
|
|
112
|
+
const mult = {
|
|
113
|
+
s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000,
|
|
114
|
+
};
|
|
115
|
+
return n * mult[m[2]];
|
|
116
|
+
}
|
|
117
|
+
/** "18:30" → today's 18:30 if still in the future, else tomorrow's 18:30.
|
|
118
|
+
* Uses server TZ (override at process level via TZ=… env). */
|
|
119
|
+
export function parseClock(s, now = new Date()) {
|
|
120
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(s.trim());
|
|
121
|
+
if (!m)
|
|
122
|
+
return null;
|
|
123
|
+
const h = Number.parseInt(m[1], 10);
|
|
124
|
+
const mi = Number.parseInt(m[2], 10);
|
|
125
|
+
if (h < 0 || h > 23 || mi < 0 || mi > 59)
|
|
126
|
+
return null;
|
|
127
|
+
const fire = new Date(now);
|
|
128
|
+
fire.setHours(h, mi, 0, 0);
|
|
129
|
+
if (fire.getTime() <= now.getTime()) {
|
|
130
|
+
fire.setDate(fire.getDate() + 1);
|
|
131
|
+
}
|
|
132
|
+
return fire;
|
|
133
|
+
}
|
|
134
|
+
/** ISO-ish absolute datetime "2026-05-09T20:00" / "2026-05-09T20:00:00Z" etc.
|
|
135
|
+
* Trailing seconds and timezone suffix are optional. */
|
|
136
|
+
export function parseISO(s) {
|
|
137
|
+
if (!/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test(s.trim()))
|
|
138
|
+
return null;
|
|
139
|
+
const d = new Date(s);
|
|
140
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
141
|
+
}
|
|
142
|
+
/** Unified parser tried in order: duration → clock → ISO. */
|
|
143
|
+
export function parseWhen(s, now = new Date()) {
|
|
144
|
+
const trimmed = s.trim();
|
|
145
|
+
if (!trimmed)
|
|
146
|
+
return { ok: false, reason: '时间不能为空' };
|
|
147
|
+
const dur = parseDuration(trimmed);
|
|
148
|
+
if (dur !== null)
|
|
149
|
+
return { ok: true, fireAt: new Date(now.getTime() + dur) };
|
|
150
|
+
const clock = parseClock(trimmed, now);
|
|
151
|
+
if (clock)
|
|
152
|
+
return { ok: true, fireAt: clock };
|
|
153
|
+
const iso = parseISO(trimmed);
|
|
154
|
+
if (iso)
|
|
155
|
+
return { ok: true, fireAt: iso };
|
|
156
|
+
return { ok: false, reason: `无法解析时间:"${trimmed}"。支持 2m / 90s / 3h / 1d / 18:30 / 2026-05-09T20:00` };
|
|
157
|
+
}
|
|
158
|
+
// ─── Prefix parsing (CN + EN, separator-optional) ───────────────────
|
|
159
|
+
const CN_UNIT_MS = {
|
|
160
|
+
'秒': 1_000,
|
|
161
|
+
'分钟': 60_000,
|
|
162
|
+
'分': 60_000, // bare 分 (rare; resolved by ordering 分钟 first)
|
|
163
|
+
'小时': 3_600_000,
|
|
164
|
+
'钟头': 3_600_000,
|
|
165
|
+
'时': 3_600_000, // 时 alone is unusual; safer to treat as small-hour
|
|
166
|
+
'天': 86_400_000,
|
|
167
|
+
};
|
|
168
|
+
// Match longer units first to avoid 分钟 → 分 misparse.
|
|
169
|
+
const CN_UNITS_ORDERED = ['分钟', '小时', '钟头', '秒', '分', '时', '天'];
|
|
170
|
+
/**
|
|
171
|
+
* Greedily consume a Chinese duration prefix from the head: handles atoms
|
|
172
|
+
* like "40秒" "2分钟" "半小时" "1小时20分" "1小时半" "0.5天", with optional
|
|
173
|
+
* trailing 后/内. Returns ms + remainder, or null if no prefix matched.
|
|
174
|
+
*/
|
|
175
|
+
function consumeChineseDuration(s) {
|
|
176
|
+
let total = 0;
|
|
177
|
+
let consumed = 0;
|
|
178
|
+
let lastUnit = '';
|
|
179
|
+
while (consumed < s.length) {
|
|
180
|
+
const remaining = s.slice(consumed);
|
|
181
|
+
let matched = false;
|
|
182
|
+
for (const u of CN_UNITS_ORDERED) {
|
|
183
|
+
const re = new RegExp(`^(?:(\\d+(?:\\.\\d+)?)|半个?)\\s*${u}\\s*`);
|
|
184
|
+
const m = re.exec(remaining);
|
|
185
|
+
if (m) {
|
|
186
|
+
const num = m[1] ? Number.parseFloat(m[1]) : 0.5;
|
|
187
|
+
if (!Number.isFinite(num) || num <= 0)
|
|
188
|
+
return null;
|
|
189
|
+
total += num * CN_UNIT_MS[u];
|
|
190
|
+
consumed += m[0].length;
|
|
191
|
+
lastUnit = u;
|
|
192
|
+
matched = true;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!matched)
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
if (consumed === 0)
|
|
200
|
+
return null;
|
|
201
|
+
// Trailing 半: e.g. "1小时半" = 1.5h
|
|
202
|
+
if (lastUnit && s.slice(consumed).startsWith('半')) {
|
|
203
|
+
total += 0.5 * CN_UNIT_MS[lastUnit];
|
|
204
|
+
consumed += 1;
|
|
205
|
+
}
|
|
206
|
+
let rest = s.slice(consumed);
|
|
207
|
+
if (rest.startsWith('后') || rest.startsWith('内'))
|
|
208
|
+
rest = rest.slice(1);
|
|
209
|
+
return { ms: total, rest: rest.trim() };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Consume a Chinese clock prefix: "下午6点" / "下午6点半" / "晚上8点45" /
|
|
213
|
+
* "12点半" / "8点". AM/PM via period word. No prefix → uses parseClock's
|
|
214
|
+
* "today if future else tomorrow" rule.
|
|
215
|
+
*/
|
|
216
|
+
function consumeChineseClock(s, now) {
|
|
217
|
+
const re = /^(早上|上午|中午|下午|晚上|凌晨)?\s*(\d{1,2})\s*(?:点|时)\s*(?:(\d{1,2})\s*(?:分钟|分)?|半)?\s*/;
|
|
218
|
+
const m = re.exec(s);
|
|
219
|
+
if (!m || !m[2])
|
|
220
|
+
return null;
|
|
221
|
+
// Reject if no period word AND no 点/时 actually consumed beyond just digits
|
|
222
|
+
// (the regex requires 点|时 so this is implicit).
|
|
223
|
+
let h = Number.parseInt(m[2], 10);
|
|
224
|
+
let mi = 0;
|
|
225
|
+
if (m[3]) {
|
|
226
|
+
mi = Number.parseInt(m[3], 10);
|
|
227
|
+
}
|
|
228
|
+
else if (/半\s*$/.test(m[0])) {
|
|
229
|
+
mi = 30;
|
|
230
|
+
}
|
|
231
|
+
const period = m[1];
|
|
232
|
+
if (period === '下午' || period === '晚上') {
|
|
233
|
+
if (h < 12)
|
|
234
|
+
h += 12;
|
|
235
|
+
}
|
|
236
|
+
else if (period === '中午') {
|
|
237
|
+
h = 12;
|
|
238
|
+
}
|
|
239
|
+
// 早上 / 上午 / 凌晨 → leave hour as-is (assume 0-12 range already)
|
|
240
|
+
if (h < 0 || h > 23 || mi < 0 || mi > 59)
|
|
241
|
+
return null;
|
|
242
|
+
const fire = new Date(now);
|
|
243
|
+
fire.setHours(h, mi, 0, 0);
|
|
244
|
+
if (fire.getTime() <= now.getTime())
|
|
245
|
+
fire.setDate(fire.getDate() + 1);
|
|
246
|
+
return { fireAt: fire, rest: s.slice(m[0].length).trim() };
|
|
247
|
+
}
|
|
248
|
+
/** Consume an English duration prefix: "40s" / "2m" / "1h" / "1d", optionally
|
|
249
|
+
* separated from the rest by whitespace. */
|
|
250
|
+
function consumeEnglishDuration(s) {
|
|
251
|
+
const m = /^(\d+)(s|m|h|d)(?=\s|$)\s*/.exec(s);
|
|
252
|
+
if (!m)
|
|
253
|
+
return null;
|
|
254
|
+
const n = Number.parseInt(m[1], 10);
|
|
255
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
256
|
+
return null;
|
|
257
|
+
const mult = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
258
|
+
return { ms: n * mult[m[2]], rest: s.slice(m[0].length).trim() };
|
|
259
|
+
}
|
|
260
|
+
/** Consume an HH:MM clock prefix. */
|
|
261
|
+
function consumeClock(s, now) {
|
|
262
|
+
const m = /^(\d{1,2}):(\d{2})(?=\s|$)\s*/.exec(s);
|
|
263
|
+
if (!m)
|
|
264
|
+
return null;
|
|
265
|
+
const h = Number.parseInt(m[1], 10);
|
|
266
|
+
const mi = Number.parseInt(m[2], 10);
|
|
267
|
+
if (h < 0 || h > 23 || mi < 0 || mi > 59)
|
|
268
|
+
return null;
|
|
269
|
+
const fire = new Date(now);
|
|
270
|
+
fire.setHours(h, mi, 0, 0);
|
|
271
|
+
if (fire.getTime() <= now.getTime())
|
|
272
|
+
fire.setDate(fire.getDate() + 1);
|
|
273
|
+
return { fireAt: fire, rest: s.slice(m[0].length).trim() };
|
|
274
|
+
}
|
|
275
|
+
/** Consume an ISO datetime prefix. */
|
|
276
|
+
function consumeISO(s) {
|
|
277
|
+
const m = /^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2})?(?:Z|[+-]\d{2}:?\d{2})?)\s*/.exec(s);
|
|
278
|
+
if (!m)
|
|
279
|
+
return null;
|
|
280
|
+
const d = new Date(m[1]);
|
|
281
|
+
if (Number.isNaN(d.getTime()))
|
|
282
|
+
return null;
|
|
283
|
+
return { fireAt: d, rest: s.slice(m[0].length).trim() };
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Consume a time-expression prefix from the head of `s`, supporting Chinese
|
|
287
|
+
* (no-separator allowed: "40秒喝水" / "下午6点下班") and English/ISO formats.
|
|
288
|
+
* Returns { fireAt, rest } on success, null when nothing matches.
|
|
289
|
+
*
|
|
290
|
+
* This is the main entry point used by the /remind slash handler so users
|
|
291
|
+
* don't need to remember strict syntax.
|
|
292
|
+
*/
|
|
293
|
+
export function parseWhenPrefix(s, now = new Date()) {
|
|
294
|
+
const trimmed = s.trim();
|
|
295
|
+
if (!trimmed)
|
|
296
|
+
return null;
|
|
297
|
+
const cd = consumeChineseDuration(trimmed);
|
|
298
|
+
if (cd)
|
|
299
|
+
return { fireAt: new Date(now.getTime() + cd.ms), rest: cd.rest };
|
|
300
|
+
const cc = consumeChineseClock(trimmed, now);
|
|
301
|
+
if (cc)
|
|
302
|
+
return cc;
|
|
303
|
+
const ed = consumeEnglishDuration(trimmed);
|
|
304
|
+
if (ed)
|
|
305
|
+
return { fireAt: new Date(now.getTime() + ed.ms), rest: ed.rest };
|
|
306
|
+
const ck = consumeClock(trimmed, now);
|
|
307
|
+
if (ck)
|
|
308
|
+
return ck;
|
|
309
|
+
const iso = consumeISO(trimmed);
|
|
310
|
+
if (iso)
|
|
311
|
+
return iso;
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
// ─── Recurrence ──────────────────────────────────────────────────────
|
|
315
|
+
export const MIN_RECURRENCE_INTERVAL_MS = 60 * 1000; // ≥ 1 minute
|
|
316
|
+
const DOW_CN = {
|
|
317
|
+
'一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '日': 7, '天': 7,
|
|
318
|
+
};
|
|
319
|
+
/** Validate a recurrence spec string. Returns true if parseable. */
|
|
320
|
+
export function isValidRecurrence(spec) {
|
|
321
|
+
if (spec.startsWith('every:')) {
|
|
322
|
+
const ms = parseDuration(spec.slice(6));
|
|
323
|
+
return ms !== null && ms >= MIN_RECURRENCE_INTERVAL_MS;
|
|
324
|
+
}
|
|
325
|
+
if (spec.startsWith('daily:')) {
|
|
326
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(spec.slice(6));
|
|
327
|
+
if (!m)
|
|
328
|
+
return false;
|
|
329
|
+
const hh = Number.parseInt(m[1], 10), mm = Number.parseInt(m[2], 10);
|
|
330
|
+
return hh >= 0 && hh <= 23 && mm >= 0 && mm <= 59;
|
|
331
|
+
}
|
|
332
|
+
if (spec.startsWith('weekly:')) {
|
|
333
|
+
const m = /^([\d,]+):(\d{1,2}):(\d{2})$/.exec(spec.slice(7));
|
|
334
|
+
if (!m)
|
|
335
|
+
return false;
|
|
336
|
+
const days = m[1].split(',').map(Number);
|
|
337
|
+
if (!days.length || days.some((d) => !Number.isInteger(d) || d < 1 || d > 7))
|
|
338
|
+
return false;
|
|
339
|
+
const hh = Number.parseInt(m[2], 10), mm = Number.parseInt(m[3], 10);
|
|
340
|
+
return hh >= 0 && hh <= 23 && mm >= 0 && mm <= 59;
|
|
341
|
+
}
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Compute the next fire time for a recurring reminder.
|
|
346
|
+
*
|
|
347
|
+
* `anchor` is the previous fire_at (for `every:` we step forward from there;
|
|
348
|
+
* if we missed cycles while offline, we fast-forward past `now`). For clock-
|
|
349
|
+
* based recurrences (daily/weekly) anchor is ignored — we always pick the
|
|
350
|
+
* next future slot from `now`.
|
|
351
|
+
*
|
|
352
|
+
* Returns null if the spec is malformed or has no satisfiable next slot.
|
|
353
|
+
*/
|
|
354
|
+
export function computeNext(spec, anchor, now) {
|
|
355
|
+
if (spec.startsWith('every:')) {
|
|
356
|
+
const dur = parseDuration(spec.slice(6));
|
|
357
|
+
if (dur === null || dur < MIN_RECURRENCE_INTERVAL_MS)
|
|
358
|
+
return null;
|
|
359
|
+
let next = new Date(anchor.getTime() + dur);
|
|
360
|
+
while (next.getTime() <= now.getTime()) {
|
|
361
|
+
next = new Date(next.getTime() + dur);
|
|
362
|
+
}
|
|
363
|
+
return next;
|
|
364
|
+
}
|
|
365
|
+
if (spec.startsWith('daily:')) {
|
|
366
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(spec.slice(6));
|
|
367
|
+
if (!m)
|
|
368
|
+
return null;
|
|
369
|
+
const hh = Number.parseInt(m[1], 10), mm = Number.parseInt(m[2], 10);
|
|
370
|
+
if (hh > 23 || mm > 59)
|
|
371
|
+
return null;
|
|
372
|
+
const next = new Date(now);
|
|
373
|
+
next.setHours(hh, mm, 0, 0);
|
|
374
|
+
while (next.getTime() <= now.getTime()) {
|
|
375
|
+
next.setDate(next.getDate() + 1);
|
|
376
|
+
}
|
|
377
|
+
return next;
|
|
378
|
+
}
|
|
379
|
+
if (spec.startsWith('weekly:')) {
|
|
380
|
+
const body = spec.slice(7);
|
|
381
|
+
const m = /^([\d,]+):(\d{1,2}):(\d{2})$/.exec(body);
|
|
382
|
+
if (!m)
|
|
383
|
+
return null;
|
|
384
|
+
const days = m[1].split(',').map(Number).filter((d) => d >= 1 && d <= 7);
|
|
385
|
+
if (!days.length)
|
|
386
|
+
return null;
|
|
387
|
+
const hh = Number.parseInt(m[2], 10), mm = Number.parseInt(m[3], 10);
|
|
388
|
+
if (hh > 23 || mm > 59)
|
|
389
|
+
return null;
|
|
390
|
+
for (let i = 0; i <= 7; i++) {
|
|
391
|
+
const cand = new Date(now);
|
|
392
|
+
cand.setDate(cand.getDate() + i);
|
|
393
|
+
cand.setHours(hh, mm, 0, 0);
|
|
394
|
+
const jsDay = cand.getDay(); // 0=Sun..6=Sat
|
|
395
|
+
const dow = jsDay === 0 ? 7 : jsDay;
|
|
396
|
+
if (days.includes(dow) && cand.getTime() > now.getTime())
|
|
397
|
+
return cand;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
/** Human-readable recurrence label for /remind list. */
|
|
404
|
+
export function describeRecurrence(spec) {
|
|
405
|
+
if (spec.startsWith('every:'))
|
|
406
|
+
return `每${spec.slice(6)}`;
|
|
407
|
+
if (spec.startsWith('daily:'))
|
|
408
|
+
return `每天 ${spec.slice(6)}`;
|
|
409
|
+
if (spec.startsWith('weekly:')) {
|
|
410
|
+
const body = spec.slice(7);
|
|
411
|
+
const m = /^([\d,]+):(\d{1,2}:\d{2})$/.exec(body);
|
|
412
|
+
if (!m)
|
|
413
|
+
return spec;
|
|
414
|
+
const days = m[1].split(',').map(Number).sort();
|
|
415
|
+
const dayLabels = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五', 6: '六', 7: '日' };
|
|
416
|
+
const isWorkday = days.length === 5 && days.join(',') === '1,2,3,4,5';
|
|
417
|
+
const isWeekend = days.length === 2 && days.join(',') === '6,7';
|
|
418
|
+
const label = isWorkday ? '工作日' : isWeekend ? '周末' : `周${days.map((d) => dayLabels[d]).join('')}`;
|
|
419
|
+
return `每${label} ${m[2]}`;
|
|
420
|
+
}
|
|
421
|
+
return spec;
|
|
422
|
+
}
|
|
423
|
+
/** Try Chinese-clock first, fall back to HH:MM — for recurrence "X 点 / X:MM". */
|
|
424
|
+
function consumeClockEither(s, now) {
|
|
425
|
+
return consumeChineseClock(s, now) ?? consumeClock(s, now);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Consume a recurrence prefix from the head of `s`. Supports:
|
|
429
|
+
*
|
|
430
|
+
* 每5分钟 / 每隔5分钟 / 每2小时 / 每30秒(拒绝, 不做亚分钟级)
|
|
431
|
+
* 每天早上8点 / 每日下午6点半 / 每天8:30
|
|
432
|
+
* 每周一8点 / 每周一三五9点 / 每周末10点 / 每个工作日9点
|
|
433
|
+
* every 5m / every 1h
|
|
434
|
+
*
|
|
435
|
+
* Returns `{ recurrence, fireAt, rest }` where `fireAt` is the FIRST occurrence
|
|
436
|
+
* (≥ now). `null` when no recurrence pattern matched.
|
|
437
|
+
*/
|
|
438
|
+
export function parseRecurrencePrefix(s, now = new Date()) {
|
|
439
|
+
const trimmed = s.trim();
|
|
440
|
+
if (!trimmed)
|
|
441
|
+
return null;
|
|
442
|
+
// ── English: "every 5m" / "every 1h" / "every 1d" ────────────────
|
|
443
|
+
const enEvery = /^every\s+(\d+)(s|m|h|d)(?=\s|$)\s*/i.exec(trimmed);
|
|
444
|
+
if (enEvery) {
|
|
445
|
+
const n = Number.parseInt(enEvery[1], 10);
|
|
446
|
+
const unit = enEvery[2].toLowerCase();
|
|
447
|
+
const ms = n * (unit === 's' ? 1000 : unit === 'm' ? 60_000 : unit === 'h' ? 3_600_000 : 86_400_000);
|
|
448
|
+
if (ms < MIN_RECURRENCE_INTERVAL_MS)
|
|
449
|
+
return null;
|
|
450
|
+
return {
|
|
451
|
+
recurrence: `every:${n}${unit}`,
|
|
452
|
+
fireAt: new Date(now.getTime() + ms),
|
|
453
|
+
rest: trimmed.slice(enEvery[0].length).trim(),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
// ── Chinese: must start with 每[隔]? ─────────────────────────────
|
|
457
|
+
const everyMatch = /^每(?:隔)?\s*/.exec(trimmed);
|
|
458
|
+
if (!everyMatch)
|
|
459
|
+
return null;
|
|
460
|
+
const after = trimmed.slice(everyMatch[0].length);
|
|
461
|
+
// 每个工作日 X 点 → weekly:1,2,3,4,5
|
|
462
|
+
const wdMatch = /^(?:个)?工作日\s*/.exec(after);
|
|
463
|
+
if (wdMatch) {
|
|
464
|
+
const cc = consumeClockEither(after.slice(wdMatch[0].length).trim(), now);
|
|
465
|
+
if (!cc)
|
|
466
|
+
return null;
|
|
467
|
+
const hh = String(cc.fireAt.getHours()).padStart(2, '0');
|
|
468
|
+
const mm = String(cc.fireAt.getMinutes()).padStart(2, '0');
|
|
469
|
+
const recurrence = `weekly:1,2,3,4,5:${hh}:${mm}`;
|
|
470
|
+
const first = computeNext(recurrence, now, new Date(now.getTime() - 1));
|
|
471
|
+
if (!first)
|
|
472
|
+
return null;
|
|
473
|
+
return { recurrence, fireAt: first, rest: cc.rest };
|
|
474
|
+
}
|
|
475
|
+
// 每周末 X 点 → weekly:6,7
|
|
476
|
+
if (after.startsWith('周末')) {
|
|
477
|
+
const cc = consumeClockEither(after.slice(2).trim(), now);
|
|
478
|
+
if (!cc)
|
|
479
|
+
return null;
|
|
480
|
+
const hh = String(cc.fireAt.getHours()).padStart(2, '0');
|
|
481
|
+
const mm = String(cc.fireAt.getMinutes()).padStart(2, '0');
|
|
482
|
+
const recurrence = `weekly:6,7:${hh}:${mm}`;
|
|
483
|
+
const first = computeNext(recurrence, now, new Date(now.getTime() - 1));
|
|
484
|
+
if (!first)
|
|
485
|
+
return null;
|
|
486
|
+
return { recurrence, fireAt: first, rest: cc.rest };
|
|
487
|
+
}
|
|
488
|
+
// 每周一 / 每周一三五 / 每周二四 → weekly:days
|
|
489
|
+
if (after.startsWith('周')) {
|
|
490
|
+
const afterZhou = after.slice(1);
|
|
491
|
+
const days = [];
|
|
492
|
+
let i = 0;
|
|
493
|
+
while (i < afterZhou.length) {
|
|
494
|
+
const ch = afterZhou[i];
|
|
495
|
+
if (DOW_CN[ch] !== undefined) {
|
|
496
|
+
days.push(DOW_CN[ch]);
|
|
497
|
+
i++;
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (!days.length)
|
|
504
|
+
return null;
|
|
505
|
+
const cc = consumeClockEither(afterZhou.slice(i).trim(), now);
|
|
506
|
+
if (!cc)
|
|
507
|
+
return null;
|
|
508
|
+
const hh = String(cc.fireAt.getHours()).padStart(2, '0');
|
|
509
|
+
const mm = String(cc.fireAt.getMinutes()).padStart(2, '0');
|
|
510
|
+
const uniqDays = [...new Set(days)].sort((a, b) => a - b);
|
|
511
|
+
const recurrence = `weekly:${uniqDays.join(',')}:${hh}:${mm}`;
|
|
512
|
+
const first = computeNext(recurrence, now, new Date(now.getTime() - 1));
|
|
513
|
+
if (!first)
|
|
514
|
+
return null;
|
|
515
|
+
return { recurrence, fireAt: first, rest: cc.rest };
|
|
516
|
+
}
|
|
517
|
+
// 每天 / 每日 [time] → daily:HH:MM
|
|
518
|
+
if (after.startsWith('天') || after.startsWith('日')) {
|
|
519
|
+
const afterDay = after.slice(1).trim();
|
|
520
|
+
const cc = consumeClockEither(afterDay, now);
|
|
521
|
+
if (!cc)
|
|
522
|
+
return null;
|
|
523
|
+
const hh = String(cc.fireAt.getHours()).padStart(2, '0');
|
|
524
|
+
const mm = String(cc.fireAt.getMinutes()).padStart(2, '0');
|
|
525
|
+
return {
|
|
526
|
+
recurrence: `daily:${hh}:${mm}`,
|
|
527
|
+
fireAt: cc.fireAt,
|
|
528
|
+
rest: cc.rest,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
// 每N(分钟|小时|秒|天) → every:Nu (reject sub-minute)
|
|
532
|
+
for (const u of CN_UNITS_ORDERED) {
|
|
533
|
+
const re = new RegExp(`^(\\d+(?:\\.\\d+)?)\\s*${u}\\s*`);
|
|
534
|
+
const m = re.exec(after);
|
|
535
|
+
if (m) {
|
|
536
|
+
const n = Number.parseFloat(m[1]);
|
|
537
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
538
|
+
return null;
|
|
539
|
+
const ms = n * CN_UNIT_MS[u];
|
|
540
|
+
if (ms < MIN_RECURRENCE_INTERVAL_MS)
|
|
541
|
+
return null;
|
|
542
|
+
const shortUnit = u === '秒'
|
|
543
|
+
? 's'
|
|
544
|
+
: (u === '分钟' || u === '分')
|
|
545
|
+
? 'm'
|
|
546
|
+
: (u === '小时' || u === '钟头' || u === '时')
|
|
547
|
+
? 'h'
|
|
548
|
+
: 'd';
|
|
549
|
+
// Use integer count when the input was integer (no .5)
|
|
550
|
+
const count = Number.isInteger(n) ? String(n) : String(n);
|
|
551
|
+
return {
|
|
552
|
+
recurrence: `every:${count}${shortUnit}`,
|
|
553
|
+
fireAt: new Date(now.getTime() + ms),
|
|
554
|
+
rest: after.slice(m[0].length).trim(),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
// ─── CRUD ────────────────────────────────────────────────────────────
|
|
561
|
+
function countPendingForUser(d, userId) {
|
|
562
|
+
const row = d.prepare("SELECT COUNT(*) AS n FROM reminders WHERE user_id = ? AND status IN ('pending','firing')").get(userId);
|
|
563
|
+
return row.n;
|
|
564
|
+
}
|
|
565
|
+
export function createReminder(input) {
|
|
566
|
+
const d = getDb();
|
|
567
|
+
if (!d)
|
|
568
|
+
throw new ReminderError('db_unavailable', '提醒功能未就绪(SQLite 不可用)');
|
|
569
|
+
const text = input.text.trim();
|
|
570
|
+
if (!text)
|
|
571
|
+
throw new ReminderError('text_empty', '提醒内容不能为空');
|
|
572
|
+
if (text.length > MAX_TEXT_LEN) {
|
|
573
|
+
throw new ReminderError('text_too_long', `提醒内容超过 ${MAX_TEXT_LEN} 字符`);
|
|
574
|
+
}
|
|
575
|
+
const now = Date.now();
|
|
576
|
+
const delta = input.fireAt.getTime() - now;
|
|
577
|
+
if (delta < MIN_FUTURE_MS) {
|
|
578
|
+
throw new ReminderError('too_soon', `触发时间必须至少 ${MIN_FUTURE_MS / 1000} 秒之后`);
|
|
579
|
+
}
|
|
580
|
+
if (delta > MAX_FUTURE_MS) {
|
|
581
|
+
throw new ReminderError('too_far', '触发时间不能超过 7 天后;如需更长周期请用 /cron');
|
|
582
|
+
}
|
|
583
|
+
if (input.userId) {
|
|
584
|
+
const n = countPendingForUser(d, input.userId);
|
|
585
|
+
if (n >= MAX_PENDING_PER_USER) {
|
|
586
|
+
throw new ReminderError('quota_exceeded', `待发提醒已达上限 ${MAX_PENDING_PER_USER} 条;先 /remind clear 清理`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
let recurrence = null;
|
|
590
|
+
if (input.recurrence !== undefined) {
|
|
591
|
+
if (!isValidRecurrence(input.recurrence)) {
|
|
592
|
+
throw new ReminderError('bad_recurrence', `循环规则非法:"${input.recurrence}"`);
|
|
593
|
+
}
|
|
594
|
+
recurrence = input.recurrence;
|
|
595
|
+
}
|
|
596
|
+
const promptMode = input.promptMode ?? 'llm';
|
|
597
|
+
// High-frequency + LLM polish → refuse. Why: 每5分钟 × LLM 调用 = burst
|
|
598
|
+
// load on whichever agent the user has registered (claude-code subprocess
|
|
599
|
+
// restart, opencode HTTP, codex CLI), independent of any cost concerns.
|
|
600
|
+
// The user can either lengthen the interval or pass promptMode='literal'.
|
|
601
|
+
if (promptMode === 'llm' && recurrence?.startsWith('every:')) {
|
|
602
|
+
const intervalMs = parseDuration(recurrence.slice(6));
|
|
603
|
+
if (intervalMs !== null && intervalMs < 60 * 60_000) {
|
|
604
|
+
throw new ReminderError('high_freq_llm', '高频循环(< 1 小时)不能用 LLM 润色——会让你的 agent 频繁触发。' +
|
|
605
|
+
'改用更长间隔,或加 `literal` 子命令保持字面文本。');
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const info = d.prepare(`INSERT INTO reminders
|
|
609
|
+
(fire_at, text, platform, channel_id, thread_id, user_id, source, parent_id, recurrence, prompt_mode)
|
|
610
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.fireAt.toISOString(), text, input.platform, input.channelId, input.threadId, input.userId, input.source || 'slash', input.parentId ?? null, recurrence, promptMode);
|
|
611
|
+
return Number(info.lastInsertRowid);
|
|
612
|
+
}
|
|
613
|
+
export function listReminders(opts = {}) {
|
|
614
|
+
const d = getDb();
|
|
615
|
+
if (!d)
|
|
616
|
+
return [];
|
|
617
|
+
const conditions = [];
|
|
618
|
+
const params = [];
|
|
619
|
+
if (opts.creatorId !== undefined) {
|
|
620
|
+
conditions.push("(user_id = ? OR user_id = '')");
|
|
621
|
+
params.push(opts.creatorId);
|
|
622
|
+
}
|
|
623
|
+
if (opts.status) {
|
|
624
|
+
conditions.push('status = ?');
|
|
625
|
+
params.push(opts.status);
|
|
626
|
+
}
|
|
627
|
+
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
628
|
+
return d.prepare(`SELECT * FROM reminders ${where} ORDER BY fire_at ASC LIMIT ?`).all(...params, opts.limit ?? 50);
|
|
629
|
+
}
|
|
630
|
+
export function getReminder(id, opts = {}) {
|
|
631
|
+
const d = getDb();
|
|
632
|
+
if (!d)
|
|
633
|
+
return null;
|
|
634
|
+
const row = (opts.creatorId !== undefined
|
|
635
|
+
? d.prepare("SELECT * FROM reminders WHERE id = ? AND (user_id = ? OR user_id = '')").get(id, opts.creatorId)
|
|
636
|
+
: d.prepare('SELECT * FROM reminders WHERE id = ?').get(id));
|
|
637
|
+
return row || null;
|
|
638
|
+
}
|
|
639
|
+
export function cancelReminder(id, opts = {}) {
|
|
640
|
+
const d = getDb();
|
|
641
|
+
if (!d)
|
|
642
|
+
return false;
|
|
643
|
+
// Only cancel rows that are still pending — fired/cancelled/failed are terminal.
|
|
644
|
+
if (opts.creatorId !== undefined) {
|
|
645
|
+
return d.prepare(`UPDATE reminders SET status = 'cancelled'
|
|
646
|
+
WHERE id = ? AND status = 'pending' AND (user_id = ? OR user_id = '')`).run(id, opts.creatorId).changes > 0;
|
|
647
|
+
}
|
|
648
|
+
return d.prepare("UPDATE reminders SET status = 'cancelled' WHERE id = ? AND status = 'pending'").run(id).changes > 0;
|
|
649
|
+
}
|
|
650
|
+
export function clearReminders(opts) {
|
|
651
|
+
const d = getDb();
|
|
652
|
+
if (!d)
|
|
653
|
+
return 0;
|
|
654
|
+
return d.prepare("UPDATE reminders SET status = 'cancelled' WHERE status = 'pending' AND user_id = ?").run(opts.creatorId).changes;
|
|
655
|
+
}
|
|
656
|
+
/** Snooze an existing reminder by `deltaMs`: clones the row's text and target
|
|
657
|
+
* thread to a new pending row, then marks the original 'cancelled' (if still
|
|
658
|
+
* pending) or just chains parent_id (if already fired). Returns new id. */
|
|
659
|
+
export function snoozeReminder(id, deltaMs, opts = {}) {
|
|
660
|
+
const d = getDb();
|
|
661
|
+
if (!d)
|
|
662
|
+
throw new ReminderError('db_unavailable', '提醒功能未就绪');
|
|
663
|
+
const orig = getReminder(id, opts);
|
|
664
|
+
if (!orig)
|
|
665
|
+
throw new ReminderError('not_found', `找不到提醒 #${id}(或不属于你)`);
|
|
666
|
+
if (deltaMs < MIN_FUTURE_MS) {
|
|
667
|
+
throw new ReminderError('too_soon', `延后时间必须至少 ${MIN_FUTURE_MS / 1000} 秒`);
|
|
668
|
+
}
|
|
669
|
+
const newId = createReminder({
|
|
670
|
+
fireAt: new Date(Date.now() + deltaMs),
|
|
671
|
+
text: orig.text,
|
|
672
|
+
platform: orig.platform,
|
|
673
|
+
channelId: orig.channel_id,
|
|
674
|
+
threadId: orig.thread_id,
|
|
675
|
+
userId: orig.user_id,
|
|
676
|
+
source: 'slash',
|
|
677
|
+
parentId: orig.id,
|
|
678
|
+
});
|
|
679
|
+
// If the original is still pending, cancel it (otherwise both would fire).
|
|
680
|
+
if (orig.status === 'pending') {
|
|
681
|
+
d.prepare("UPDATE reminders SET status = 'cancelled' WHERE id = ?").run(orig.id);
|
|
682
|
+
}
|
|
683
|
+
return newId;
|
|
684
|
+
}
|
|
685
|
+
// ─── Email bindings ──────────────────────────────────────────────────
|
|
686
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
687
|
+
/** Bind (or rebind) an email address to an IM user. Returns the bound email. */
|
|
688
|
+
export function bindEmail(platform, userId, email) {
|
|
689
|
+
const d = getDb();
|
|
690
|
+
if (!d)
|
|
691
|
+
throw new ReminderError('db_unavailable', '邮箱绑定功能未就绪');
|
|
692
|
+
const trimmed = email.trim();
|
|
693
|
+
if (!EMAIL_RE.test(trimmed))
|
|
694
|
+
throw new ReminderError('bad_email', `邮箱格式非法:"${email}"`);
|
|
695
|
+
if (!userId)
|
|
696
|
+
throw new ReminderError('no_user', '无法识别身份,无法绑定邮箱');
|
|
697
|
+
d.prepare(`INSERT INTO user_email_bindings (platform, user_id, email)
|
|
698
|
+
VALUES (?, ?, ?)
|
|
699
|
+
ON CONFLICT(platform, user_id) DO UPDATE SET
|
|
700
|
+
email = excluded.email,
|
|
701
|
+
bound_at = datetime('now')`).run(platform, userId, trimmed);
|
|
702
|
+
return trimmed;
|
|
703
|
+
}
|
|
704
|
+
/** Look up the bound email for an IM user, or null. */
|
|
705
|
+
export function getBoundEmail(platform, userId) {
|
|
706
|
+
const d = getDb();
|
|
707
|
+
if (!d || !userId)
|
|
708
|
+
return null;
|
|
709
|
+
const row = d.prepare('SELECT email FROM user_email_bindings WHERE platform = ? AND user_id = ?').get(platform, userId);
|
|
710
|
+
return row?.email ?? null;
|
|
711
|
+
}
|
|
712
|
+
/** Remove the binding for an IM user. Returns true if a row was deleted. */
|
|
713
|
+
export function unbindEmail(platform, userId) {
|
|
714
|
+
const d = getDb();
|
|
715
|
+
if (!d || !userId)
|
|
716
|
+
return false;
|
|
717
|
+
return d.prepare('DELETE FROM user_email_bindings WHERE platform = ? AND user_id = ?').run(platform, userId).changes > 0;
|
|
718
|
+
}
|
|
719
|
+
// ─── Context snippet (for agent prompt injection) ───────────────────
|
|
720
|
+
const CONTEXT_MAX_REMINDERS = 5;
|
|
721
|
+
const CONTEXT_MAX_TEXT_LEN = 60;
|
|
722
|
+
/**
|
|
723
|
+
* Render a compact "background context" block listing the user's pending
|
|
724
|
+
* reminders for the current platform. Used by router.ts to prepend to the
|
|
725
|
+
* agent prompt so the agent has live visibility into the user's queue —
|
|
726
|
+
* lets it answer "what reminders do I have?" without an extra
|
|
727
|
+
* list_reminders tool call, and avoids creating duplicates.
|
|
728
|
+
*
|
|
729
|
+
* Empty string when nothing pending — caller should noop.
|
|
730
|
+
*
|
|
731
|
+
* Cost: ~80-200 tokens per message when populated. Bounded to 5 reminders.
|
|
732
|
+
*/
|
|
733
|
+
export function buildContextSnippet(platform, userId) {
|
|
734
|
+
if (!userId)
|
|
735
|
+
return '';
|
|
736
|
+
const d = getDb();
|
|
737
|
+
if (!d)
|
|
738
|
+
return '';
|
|
739
|
+
const rows = d.prepare(`SELECT * FROM reminders
|
|
740
|
+
WHERE status = 'pending'
|
|
741
|
+
AND platform = ?
|
|
742
|
+
AND (user_id = ? OR user_id = '')
|
|
743
|
+
ORDER BY fire_at ASC LIMIT ?`).all(platform, userId, CONTEXT_MAX_REMINDERS);
|
|
744
|
+
if (rows.length === 0)
|
|
745
|
+
return '';
|
|
746
|
+
const lines = rows.map((r) => {
|
|
747
|
+
const t = new Date(r.fire_at);
|
|
748
|
+
const when = t.toLocaleString();
|
|
749
|
+
const recur = r.recurrence ? ` [${describeRecurrence(r.recurrence)}]` : '';
|
|
750
|
+
const text = r.text.length > CONTEXT_MAX_TEXT_LEN
|
|
751
|
+
? `${r.text.slice(0, CONTEXT_MAX_TEXT_LEN)}…`
|
|
752
|
+
: r.text;
|
|
753
|
+
return ` #${r.id} ${when}${recur}: ${text}`;
|
|
754
|
+
});
|
|
755
|
+
return [
|
|
756
|
+
'[im-hub context — user\'s pending reminders. Use snooze_reminder / cancel_reminder tools to modify; reference these naturally if relevant. Ignore if unrelated to the current message.]',
|
|
757
|
+
...lines,
|
|
758
|
+
'',
|
|
759
|
+
].join('\n');
|
|
760
|
+
}
|
|
761
|
+
// ─── Aiwatch (reminder-intent detector) settings ─────────────────────
|
|
762
|
+
/** Whether the LLM intent detector should run for this user's non-slash
|
|
763
|
+
* messages. Default on (no row = enabled). DB-unavailable also returns
|
|
764
|
+
* true so users on bun/fail-soft don't lose the feature silently. */
|
|
765
|
+
export function isAiwatchEnabled(platform, userId) {
|
|
766
|
+
const d = getDb();
|
|
767
|
+
if (!d || !userId)
|
|
768
|
+
return true;
|
|
769
|
+
const row = d.prepare('SELECT enabled FROM user_aiwatch_settings WHERE platform = ? AND user_id = ?').get(platform, userId);
|
|
770
|
+
if (!row)
|
|
771
|
+
return true;
|
|
772
|
+
return row.enabled !== 0;
|
|
773
|
+
}
|
|
774
|
+
/** Set the aiwatch enabled flag. UPSERT semantics. Returns the new value. */
|
|
775
|
+
export function setAiwatchEnabled(platform, userId, enabled) {
|
|
776
|
+
const d = getDb();
|
|
777
|
+
if (!d || !userId)
|
|
778
|
+
return enabled;
|
|
779
|
+
d.prepare(`INSERT INTO user_aiwatch_settings (platform, user_id, enabled, set_at)
|
|
780
|
+
VALUES (?, ?, ?, datetime('now'))
|
|
781
|
+
ON CONFLICT(platform, user_id) DO UPDATE SET
|
|
782
|
+
enabled = excluded.enabled,
|
|
783
|
+
set_at = excluded.set_at`).run(platform, userId, enabled ? 1 : 0);
|
|
784
|
+
return enabled;
|
|
785
|
+
}
|
|
786
|
+
// ─── Engine ──────────────────────────────────────────────────────────
|
|
787
|
+
/**
|
|
788
|
+
* Resolve a vendor-agnostic platform name (`'wechat'`, `'telegram'`, …) to a
|
|
789
|
+
* registered messenger adapter. Some adapters register under specific names
|
|
790
|
+
* like `'wechat-ilink'` while RouteContext.platform stays generic; we walk
|
|
791
|
+
* the registry once and fuzzy-match by `${platform}-*` prefix.
|
|
792
|
+
*/
|
|
793
|
+
function resolveMessenger(platform) {
|
|
794
|
+
const exact = registry.getMessenger(platform);
|
|
795
|
+
if (exact)
|
|
796
|
+
return exact;
|
|
797
|
+
for (const name of registry.listMessengers()) {
|
|
798
|
+
if (name === platform || name.startsWith(`${platform}-`) || name.startsWith(`${platform}_`)) {
|
|
799
|
+
return registry.getMessenger(name);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return undefined;
|
|
803
|
+
}
|
|
804
|
+
/** Move a failed/cancelled reminder back to pending with a fresh fireAt
|
|
805
|
+
* (now + MIN_FUTURE_MS). Owner-scoped. Returns true on success. */
|
|
806
|
+
export function retryReminder(id, opts = {}) {
|
|
807
|
+
const d = getDb();
|
|
808
|
+
if (!d)
|
|
809
|
+
return false;
|
|
810
|
+
const r = getReminder(id, opts);
|
|
811
|
+
if (!r)
|
|
812
|
+
return false;
|
|
813
|
+
if (r.status === 'pending' || r.status === 'firing')
|
|
814
|
+
return false;
|
|
815
|
+
const fireAt = new Date(Date.now() + MIN_FUTURE_MS).toISOString();
|
|
816
|
+
return d.prepare(`UPDATE reminders SET status = 'pending', fire_at = ?, error = NULL, fired_at = NULL
|
|
817
|
+
WHERE id = ?`).run(fireAt, id).changes > 0;
|
|
818
|
+
}
|
|
819
|
+
function dueReminders(d, now) {
|
|
820
|
+
return d.prepare(`SELECT * FROM reminders
|
|
821
|
+
WHERE status = 'pending' AND fire_at <= ?
|
|
822
|
+
ORDER BY fire_at ASC LIMIT ?`).all(now.toISOString(), BATCH_SIZE);
|
|
823
|
+
}
|
|
824
|
+
const POLISH_TIMEOUT_MS = 20_000;
|
|
825
|
+
const POLISH_MAX_OUTPUT = 800;
|
|
826
|
+
/**
|
|
827
|
+
* System prompt used when prompt_mode === 'llm'. Tone constraints come
|
|
828
|
+
* straight from product feedback: no flattery, no excess humor, no
|
|
829
|
+
* exaggeration. The model is told to act as a *terse* notifier, not a
|
|
830
|
+
* cheerleader. Caller still wraps the result with the 🔔/🔁 prefix.
|
|
831
|
+
*/
|
|
832
|
+
const POLISH_SYSTEM_PROMPT = [
|
|
833
|
+
'你是一个简短、克制的提醒助手。',
|
|
834
|
+
'收到一条用户给自己的提醒指令后,你需要把它转化为一条要发给用户的中文提醒文本。',
|
|
835
|
+
'',
|
|
836
|
+
'严格遵守:',
|
|
837
|
+
'1. 风格中性、专业、自然,**不要献媚奉承、不要过度幽默、不要夸大夸张**。',
|
|
838
|
+
'2. 输出只是一条提醒,不要打招呼套话("你好呀!"、"亲爱的"),不要表情包堆砌。',
|
|
839
|
+
'3. 长度 ≤ 80 字。能一句说清就一句。',
|
|
840
|
+
'4. 直接输出最终文本,不要 JSON、不要解释、不要重复指令、不要带"提醒:"等前缀(前缀由系统加)。',
|
|
841
|
+
'5. 如果指令本身已经是一句完整、简洁、可投递的话,几乎原样输出即可,别画蛇添足。',
|
|
842
|
+
].join('\n');
|
|
843
|
+
/**
|
|
844
|
+
* Ask the active agent to compose a delivery message from the literal seed
|
|
845
|
+
* text. Returns the composed text, or null on any failure / timeout / no
|
|
846
|
+
* agent → caller falls back to the literal seed.
|
|
847
|
+
*/
|
|
848
|
+
async function polishReminderText(r, now) {
|
|
849
|
+
const userPrompt = [
|
|
850
|
+
`当前时间:${now.toLocaleString()}`,
|
|
851
|
+
r.recurrence ? `(这是一条循环提醒:${r.recurrence})` : '',
|
|
852
|
+
'',
|
|
853
|
+
`用户的提醒指令:${r.text}`,
|
|
854
|
+
].filter(Boolean).join('\n');
|
|
855
|
+
const polished = await callHelperAgent({
|
|
856
|
+
platform: r.platform,
|
|
857
|
+
channelId: r.channel_id,
|
|
858
|
+
threadId: r.thread_id,
|
|
859
|
+
systemPrompt: POLISH_SYSTEM_PROMPT,
|
|
860
|
+
userPrompt,
|
|
861
|
+
timeoutMs: POLISH_TIMEOUT_MS,
|
|
862
|
+
maxOutputChars: POLISH_MAX_OUTPUT,
|
|
863
|
+
});
|
|
864
|
+
if (!polished)
|
|
865
|
+
return null;
|
|
866
|
+
// Defensive: collapse multi-line output so it stays one tidy chat bubble.
|
|
867
|
+
// Keep paragraph breaks but strip leading/trailing whitespace per line.
|
|
868
|
+
return polished.split('\n').map((s) => s.trim()).filter(Boolean).join('\n').slice(0, 500);
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Format the delivery payload. Late delivery (> 1h overdue) gets a tagged
|
|
872
|
+
* prefix so the user sees that the bot was offline. Body comes from the
|
|
873
|
+
* pre-composed `body` argument (literal text or LLM polish).
|
|
874
|
+
*/
|
|
875
|
+
function formatDelivery(r, body, now) {
|
|
876
|
+
const fireAt = new Date(r.fire_at);
|
|
877
|
+
const lateMs = now.getTime() - fireAt.getTime();
|
|
878
|
+
const lateTag = lateMs > LATE_THRESHOLD_MS
|
|
879
|
+
? `⏰ 延迟投递(原计划 ${fireAt.toLocaleString()})\n`
|
|
880
|
+
: '';
|
|
881
|
+
const icon = r.recurrence ? '🔁' : '🔔';
|
|
882
|
+
return `${lateTag}${icon} 提醒:${body}`;
|
|
883
|
+
}
|
|
884
|
+
async function fireOne(r, now) {
|
|
885
|
+
const d = getDb();
|
|
886
|
+
if (!d)
|
|
887
|
+
return;
|
|
888
|
+
// Atomically claim: only proceed if still pending.
|
|
889
|
+
const claim = d.prepare("UPDATE reminders SET status = 'firing' WHERE id = ? AND status = 'pending'").run(r.id);
|
|
890
|
+
if (claim.changes === 0)
|
|
891
|
+
return; // already taken
|
|
892
|
+
const messenger = resolveMessenger(r.platform);
|
|
893
|
+
if (!messenger) {
|
|
894
|
+
d.prepare("UPDATE reminders SET status = 'failed', error = ? WHERE id = ?")
|
|
895
|
+
.run(`messenger '${r.platform}' not registered`, r.id);
|
|
896
|
+
log.warn({ event: 'reminders.no_messenger', id: r.id, platform: r.platform });
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
try {
|
|
900
|
+
// Compose the body. Polish is best-effort: if the helper agent isn't
|
|
901
|
+
// available or times out, fall back to the literal seed text. The user
|
|
902
|
+
// still gets their reminder either way.
|
|
903
|
+
let body = r.text;
|
|
904
|
+
if (r.prompt_mode === 'llm') {
|
|
905
|
+
const polished = await polishReminderText(r, now);
|
|
906
|
+
if (polished) {
|
|
907
|
+
body = polished;
|
|
908
|
+
log.debug({ event: 'reminders.polished', id: r.id, polishedLen: polished.length });
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
log.debug({ event: 'reminders.polish_skipped', id: r.id }, 'helper agent unavailable — using literal text');
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
await messenger.sendMessage(r.thread_id, formatDelivery(r, body, now));
|
|
915
|
+
const firedAtIso = new Date().toISOString();
|
|
916
|
+
if (r.recurrence) {
|
|
917
|
+
// Reschedule to next occurrence; row stays pending. If computeNext returns
|
|
918
|
+
// null (malformed spec — shouldn't happen since we validate on create) we
|
|
919
|
+
// fall back to terminal 'fired' so the row doesn't get stuck.
|
|
920
|
+
const fireAt = new Date(r.fire_at);
|
|
921
|
+
const next = computeNext(r.recurrence, fireAt, now);
|
|
922
|
+
if (next) {
|
|
923
|
+
d.prepare(`UPDATE reminders
|
|
924
|
+
SET status = 'pending', fire_at = ?, last_fired_at = ?, error = NULL
|
|
925
|
+
WHERE id = ?`).run(next.toISOString(), firedAtIso, r.id);
|
|
926
|
+
log.info({
|
|
927
|
+
event: 'reminders.fired_recurring',
|
|
928
|
+
id: r.id, platform: r.platform, threadId: r.thread_id,
|
|
929
|
+
next: next.toISOString(), recurrence: r.recurrence,
|
|
930
|
+
});
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
log.warn({ event: 'reminders.recurrence_terminal', id: r.id, recurrence: r.recurrence }, 'computeNext returned null — terminating recurring reminder');
|
|
934
|
+
}
|
|
935
|
+
d.prepare("UPDATE reminders SET status = 'fired', fired_at = ? WHERE id = ?")
|
|
936
|
+
.run(firedAtIso, r.id);
|
|
937
|
+
log.info({ event: 'reminders.fired', id: r.id, platform: r.platform, threadId: r.thread_id });
|
|
938
|
+
}
|
|
939
|
+
catch (err) {
|
|
940
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
941
|
+
d.prepare("UPDATE reminders SET status = 'failed', error = ? WHERE id = ?").run(msg, r.id);
|
|
942
|
+
log.warn({ event: 'reminders.failed', id: r.id, platform: r.platform, err: msg });
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
/** Run a single tick. Exposed for tests so they can advance the clock manually. */
|
|
946
|
+
export async function tick(now = new Date()) {
|
|
947
|
+
const d = getDb();
|
|
948
|
+
if (!d)
|
|
949
|
+
return 0;
|
|
950
|
+
const due = dueReminders(d, now);
|
|
951
|
+
for (const r of due) {
|
|
952
|
+
await fireOne(r, now).catch((err) => {
|
|
953
|
+
log.error({ id: r.id, err: err instanceof Error ? err.message : String(err) }, 'fireOne threw — leaving row in firing state for next-restart recovery');
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
return due.length;
|
|
957
|
+
}
|
|
958
|
+
/** Start the periodic ticker. Idempotent. */
|
|
959
|
+
export function startReminderEngine() {
|
|
960
|
+
if (tickTimer)
|
|
961
|
+
return;
|
|
962
|
+
tickTimer = setInterval(() => { void tick(); }, TICK_INTERVAL_MS);
|
|
963
|
+
if (typeof tickTimer === 'object' && tickTimer && 'unref' in tickTimer) {
|
|
964
|
+
tickTimer.unref();
|
|
965
|
+
}
|
|
966
|
+
log.info({ tickMs: TICK_INTERVAL_MS }, 'Reminder engine started');
|
|
967
|
+
}
|
|
968
|
+
export function stopReminderEngine() {
|
|
969
|
+
if (tickTimer) {
|
|
970
|
+
clearInterval(tickTimer);
|
|
971
|
+
tickTimer = null;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
export function closeReminderDb() {
|
|
975
|
+
helper.close();
|
|
976
|
+
}
|
|
977
|
+
//# sourceMappingURL=reminders.js.map
|