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,664 @@
|
|
|
1
|
+
// WeChat iLink Bot API Adapter
|
|
2
|
+
// Implements MessengerAdapter using the iLink HTTP API
|
|
3
|
+
import { ILinkClient } from './ilink-client.js';
|
|
4
|
+
import { ILINK_ERRORS, ITEM_TYPE } from './ilink-types.js';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { AGIM_HOME } from '../../../core/agim-paths.js';
|
|
7
|
+
import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
|
|
8
|
+
import { logger as rootLogger } from '../../../core/logger.js';
|
|
9
|
+
import { Backoff } from '../../../utils/backoff.js';
|
|
10
|
+
import { downloadWechatMedia, extractCdnMedia, pickExtensionForImage, pickExtensionForVoice, pickExtensionForFile, pickExtensionForVideo, } from './media-download.js';
|
|
11
|
+
import { transcribe, detectProvider, TranscribeError } from '../../../core/transcribe.js';
|
|
12
|
+
import { persistContextToken, loadContextToken, loadAllContextTokens, cleanupExpiredTokens as cleanupExpiredTokensInStore, closeContextStore, } from './context-store.js';
|
|
13
|
+
const log = rootLogger.child({ component: 'wechat-ilink' });
|
|
14
|
+
const CREDENTIALS_FILE = join(AGIM_HOME, 'wechat-credentials.json');
|
|
15
|
+
const _POLL_TIMEOUT = 30000; // 30 seconds
|
|
16
|
+
const CONTEXT_TOKEN_TTL = 30 * 60 * 1000; // 30 minutes
|
|
17
|
+
const PROCESSED_MESSAGES_TTL = 60 * 1000; // 1 minute
|
|
18
|
+
export class ILinkWeChatAdapter {
|
|
19
|
+
name = 'wechat-ilink';
|
|
20
|
+
client;
|
|
21
|
+
messageHandler;
|
|
22
|
+
isRunning = false;
|
|
23
|
+
pollState = {
|
|
24
|
+
getUpdatesBuf: '',
|
|
25
|
+
isPolling: false,
|
|
26
|
+
lastPollTime: 0,
|
|
27
|
+
};
|
|
28
|
+
contextTokens = new Map();
|
|
29
|
+
typingTickets = new Map();
|
|
30
|
+
processedMessages = new Map(); // message_id -> timestamp
|
|
31
|
+
constructor() {
|
|
32
|
+
this.client = new ILinkClient();
|
|
33
|
+
}
|
|
34
|
+
// ============================================
|
|
35
|
+
// Lifecycle
|
|
36
|
+
// ============================================
|
|
37
|
+
async start() {
|
|
38
|
+
// Load saved credentials
|
|
39
|
+
const credentials = await this.loadCredentials();
|
|
40
|
+
if (credentials) {
|
|
41
|
+
this.client.setCredentials(credentials);
|
|
42
|
+
log.info('Credentials loaded from cache');
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw new Error('No WeChat credentials found. Run "im-hub config wechat" first.');
|
|
46
|
+
}
|
|
47
|
+
// Warm in-memory context-token cache from persistent store. Without this,
|
|
48
|
+
// any user who messaged before the last restart but is silent now can't
|
|
49
|
+
// receive pushes (e.g. recurring reminders) until they speak again.
|
|
50
|
+
const warmed = loadAllContextTokens(CONTEXT_TOKEN_TTL, Date.now());
|
|
51
|
+
for (const t of warmed) {
|
|
52
|
+
this.contextTokens.set(t.userId, {
|
|
53
|
+
userId: t.userId,
|
|
54
|
+
contextToken: t.token,
|
|
55
|
+
timestamp: t.fetchedAt,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (warmed.length > 0) {
|
|
59
|
+
log.info({ event: 'wechat.context.warm', count: warmed.length }, 'Warmed context-token cache from persistent store');
|
|
60
|
+
}
|
|
61
|
+
this.isRunning = true;
|
|
62
|
+
log.info('WeChat iLink adapter started');
|
|
63
|
+
// Start polling in background
|
|
64
|
+
this.startPolling();
|
|
65
|
+
}
|
|
66
|
+
async stop() {
|
|
67
|
+
this.isRunning = false;
|
|
68
|
+
this.client.clearCredentials();
|
|
69
|
+
closeContextStore();
|
|
70
|
+
log.info('WeChat iLink adapter stopped');
|
|
71
|
+
}
|
|
72
|
+
// ============================================
|
|
73
|
+
// Message Handling
|
|
74
|
+
// ============================================
|
|
75
|
+
onMessage(handler) {
|
|
76
|
+
this.messageHandler = handler;
|
|
77
|
+
}
|
|
78
|
+
async sendMessage(threadId, text) {
|
|
79
|
+
if (!this.client.hasCredentials()) {
|
|
80
|
+
throw new Error('WeChat adapter not authenticated');
|
|
81
|
+
}
|
|
82
|
+
// Extract user ID from threadId (format: user:xxx or room:xxx)
|
|
83
|
+
const userId = threadId.replace(/^(user|room):/, '');
|
|
84
|
+
// Get context token for this user
|
|
85
|
+
const contextToken = this.getContextToken(userId);
|
|
86
|
+
if (!contextToken) {
|
|
87
|
+
throw new Error('No context token available for this user');
|
|
88
|
+
}
|
|
89
|
+
// Split long messages
|
|
90
|
+
const chunks = this.splitMessage(text);
|
|
91
|
+
for (const chunk of chunks) {
|
|
92
|
+
const response = await this.client.sendMessage(userId, chunk, contextToken);
|
|
93
|
+
if (response.ret !== 0 && response.ret !== undefined) {
|
|
94
|
+
if (response.ret === ILINK_ERRORS.SESSION_EXPIRED) {
|
|
95
|
+
throw new Error('WeChat session expired. Please re-login.');
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`Failed to send message: ${response.errmsg || response.ret}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ============================================
|
|
102
|
+
// Typing Indicator
|
|
103
|
+
// ============================================
|
|
104
|
+
// Note: WeChat does NOT implement sendThinking on purpose. The previous
|
|
105
|
+
// "🤔 思考中…" placeholder was added in May 2026 as a fallback when the
|
|
106
|
+
// native typing indicator was deemed not prominent enough. After the
|
|
107
|
+
// remind-intent perf fix (avg reply latency 80s → 3s) the case
|
|
108
|
+
// collapsed: the bubble would linger forever (iLink has no recall API)
|
|
109
|
+
// and just polluted chat history. Native sendTyping below covers the
|
|
110
|
+
// immediate-feedback need on its own. Feishu still keeps sendThinking
|
|
111
|
+
// because its sendTyping is a no-op.
|
|
112
|
+
async sendTyping(threadId, isTyping) {
|
|
113
|
+
if (!this.client.hasCredentials()) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Extract user ID from threadId (format: user:xxx or room:xxx)
|
|
117
|
+
const userId = threadId.replace(/^(user|room):/, '');
|
|
118
|
+
// Get or fetch typing ticket
|
|
119
|
+
let typingTicket = this.typingTickets.get(userId);
|
|
120
|
+
if (!typingTicket) {
|
|
121
|
+
const contextToken = this.getContextToken(userId);
|
|
122
|
+
const ticket = await this.client.getTypingTicket(userId, contextToken ?? undefined);
|
|
123
|
+
if (!ticket) {
|
|
124
|
+
log.warn({ userId }, 'Could not get typing ticket');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
typingTicket = ticket;
|
|
128
|
+
this.typingTickets.set(userId, ticket);
|
|
129
|
+
}
|
|
130
|
+
// Send typing status (1 = start, 2 = stop)
|
|
131
|
+
const status = isTyping ? 1 : 2;
|
|
132
|
+
const success = await this.client.sendTyping(userId, typingTicket, status);
|
|
133
|
+
if (!success) {
|
|
134
|
+
// Ticket might be expired, clear it for next attempt
|
|
135
|
+
this.typingTickets.delete(userId);
|
|
136
|
+
log.warn({ userId }, 'Failed to send typing indicator, cleared ticket');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// ============================================
|
|
140
|
+
// QR Code Login
|
|
141
|
+
// ============================================
|
|
142
|
+
/**
|
|
143
|
+
* Start QR code login flow
|
|
144
|
+
* Returns QR code URL and token for polling
|
|
145
|
+
*/
|
|
146
|
+
async startQRLogin() {
|
|
147
|
+
const response = await this.client.getQRCode();
|
|
148
|
+
return {
|
|
149
|
+
qrUrl: response.qrcode_img_content,
|
|
150
|
+
qrToken: response.qrcode,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Poll QR code status until confirmed or expired
|
|
155
|
+
*/
|
|
156
|
+
async waitForQRLogin(qrToken, onStatus) {
|
|
157
|
+
const maxAttempts = 120; // 2 minutes with 1s interval
|
|
158
|
+
let attempts = 0;
|
|
159
|
+
while (attempts < maxAttempts) {
|
|
160
|
+
const status = await this.client.getQRCodeStatus(qrToken);
|
|
161
|
+
switch (status.status) {
|
|
162
|
+
case 'wait':
|
|
163
|
+
onStatus?.('Waiting for scan...');
|
|
164
|
+
break;
|
|
165
|
+
case 'scaned':
|
|
166
|
+
onStatus?.('QR code scanned! Waiting for confirmation...');
|
|
167
|
+
break;
|
|
168
|
+
case 'confirmed':
|
|
169
|
+
if (status.bot_token && status.ilink_bot_id && status.ilink_user_id) {
|
|
170
|
+
const credentials = {
|
|
171
|
+
bot_token: status.bot_token,
|
|
172
|
+
baseUrl: status.baseurl || 'https://ilinkai.weixin.qq.com',
|
|
173
|
+
accountId: status.ilink_bot_id,
|
|
174
|
+
userId: status.ilink_user_id,
|
|
175
|
+
savedAt: new Date().toISOString(),
|
|
176
|
+
};
|
|
177
|
+
// Save credentials
|
|
178
|
+
await this.saveCredentials(credentials);
|
|
179
|
+
this.client.setCredentials(credentials);
|
|
180
|
+
onStatus?.('Login successful!');
|
|
181
|
+
return credentials;
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
case 'expired':
|
|
185
|
+
onStatus?.('QR code expired');
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
// Wait 1 second before next poll
|
|
189
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
190
|
+
attempts++;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
// ============================================
|
|
195
|
+
// Polling
|
|
196
|
+
// ============================================
|
|
197
|
+
startPolling() {
|
|
198
|
+
if (this.pollState.isPolling)
|
|
199
|
+
return;
|
|
200
|
+
this.pollState.isPolling = true;
|
|
201
|
+
this.pollLoop();
|
|
202
|
+
}
|
|
203
|
+
async pollLoop() {
|
|
204
|
+
log.info('Polling started');
|
|
205
|
+
let consecutiveFailures = 0;
|
|
206
|
+
let lastHeartbeatTime = Date.now();
|
|
207
|
+
const HEARTBEAT_INTERVAL_MS = 60 * 1000; // 60 seconds
|
|
208
|
+
// M9: shared exponential-with-jitter helper. The previous inline
|
|
209
|
+
// `1000 * Math.pow(2, n - 1)` was deterministic, so a fleet of im-hub
|
|
210
|
+
// instances recovering from a shared network event would all
|
|
211
|
+
// reconnect in lock-step. Backoff(jitter=0.5) spreads the storm
|
|
212
|
+
// across a ±50% window without changing the underlying cadence.
|
|
213
|
+
// Base 2_000 / cap 30_000 preserves the prior schedule's ballpark
|
|
214
|
+
// (2s, 4s, 8s, 16s, 30s capped) — first failure still has no
|
|
215
|
+
// extra wait via the `consecutiveFailures > 1` gate below.
|
|
216
|
+
const backoff = new Backoff({ baseMs: 2_000, capMs: 30_000, jitter: 0.5 });
|
|
217
|
+
while (this.isRunning) {
|
|
218
|
+
try {
|
|
219
|
+
const response = await this.client.getUpdates(this.pollState.getUpdatesBuf);
|
|
220
|
+
// Success — reset failure counter and backoff
|
|
221
|
+
consecutiveFailures = 0;
|
|
222
|
+
backoff.reset();
|
|
223
|
+
if (response.msgs?.length) {
|
|
224
|
+
log.debug({ count: response.msgs.length }, 'Received messages');
|
|
225
|
+
}
|
|
226
|
+
const isSuccess = response.ret === 0 || response.ret === undefined;
|
|
227
|
+
if (isSuccess) {
|
|
228
|
+
this.pollState.getUpdatesBuf = response.get_updates_buf;
|
|
229
|
+
if (response.msgs) {
|
|
230
|
+
for (const msg of response.msgs) {
|
|
231
|
+
this.handleIncomingMessage(msg).catch(err => log.error({ err: err instanceof Error ? err.message : String(err) }, 'Message handler error'));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (response.ret === ILINK_ERRORS.SESSION_EXPIRED) {
|
|
236
|
+
log.warn('WeChat session expired, attempting recovery...');
|
|
237
|
+
const recovered = await this.tryRefreshSession();
|
|
238
|
+
if (!recovered) {
|
|
239
|
+
log.error('WeChat session expired. Token refresh failed after retries — polling stopped.');
|
|
240
|
+
this.isRunning = false;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
log.info('Token refreshed successfully, resuming polling');
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
log.warn({ ret: response.ret }, 'Unexpected getUpdates response code');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
251
|
+
consecutiveFailures++;
|
|
252
|
+
log.error({ err: errMsg, consecutiveFailures }, 'Poll error');
|
|
253
|
+
// First failure: no extra wait, just the regular 1s tail sleep.
|
|
254
|
+
// From the second consecutive failure on, draw the next delay
|
|
255
|
+
// from the jittered exponential schedule (2s/4s/8s/16s/30s ±50%).
|
|
256
|
+
if (consecutiveFailures > 1) {
|
|
257
|
+
const delayMs = backoff.nextDelayMs();
|
|
258
|
+
log.warn({ delayMs }, `Backing off after ${consecutiveFailures} consecutive failures`);
|
|
259
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Periodic heartbeat: getconfig probe (no side effect on the
|
|
264
|
+
// long-poll cursor, so it's safe to run alongside the loop).
|
|
265
|
+
if (Date.now() - lastHeartbeatTime >= HEARTBEAT_INTERVAL_MS) {
|
|
266
|
+
lastHeartbeatTime = Date.now();
|
|
267
|
+
this.sendHeartbeat().catch(err => log.warn({ err: err instanceof Error ? err.message : String(err) }, 'Heartbeat error'));
|
|
268
|
+
// L14: piggyback context-token sweep on the heartbeat tick (1/min).
|
|
269
|
+
// Cheap full scan — bounded by active user count.
|
|
270
|
+
this.cleanupExpiredContextTokens();
|
|
271
|
+
}
|
|
272
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
273
|
+
}
|
|
274
|
+
this.pollState.isPolling = false;
|
|
275
|
+
log.info('Polling stopped');
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Attempt to recover from SESSION_EXPIRED. Tries a fresh getUpdates
|
|
279
|
+
* up to 3 times with linear backoff so a single network blip during
|
|
280
|
+
* recovery doesn't tear down the whole adapter (W-3).
|
|
281
|
+
*/
|
|
282
|
+
async tryRefreshSession() {
|
|
283
|
+
const MAX_ATTEMPTS = 3;
|
|
284
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
285
|
+
try {
|
|
286
|
+
const response = await this.client.getUpdates('');
|
|
287
|
+
if (response.ret === 0 || response.ret === undefined) {
|
|
288
|
+
this.pollState.getUpdatesBuf = response.get_updates_buf;
|
|
289
|
+
if (response.msgs) {
|
|
290
|
+
for (const msg of response.msgs) {
|
|
291
|
+
if (msg.from_user_id && msg.context_token) {
|
|
292
|
+
this.setContextToken(msg.from_user_id, msg.context_token);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
log.info({ attempt }, 'Session recovered after SESSION_EXPIRED');
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
log.warn({ attempt, ret: response.ret }, 'Session refresh attempt returned error');
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
log.warn({ attempt, err: err instanceof Error ? err.message : String(err) }, 'Session refresh attempt threw');
|
|
303
|
+
}
|
|
304
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
305
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 5_000)); // 5s, 10s
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Periodic keep-alive: probe getconfig instead of getUpdates so we don't
|
|
312
|
+
* race the polling loop on the same long-poll cursor (W-1).
|
|
313
|
+
*/
|
|
314
|
+
async sendHeartbeat() {
|
|
315
|
+
if (!this.client.hasCredentials())
|
|
316
|
+
return;
|
|
317
|
+
const ok = await this.client.pingConfig();
|
|
318
|
+
if (ok) {
|
|
319
|
+
log.debug('Heartbeat OK');
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
log.warn('Heartbeat failed (getconfig returned non-OK)');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async handleIncomingMessage(msg) {
|
|
326
|
+
log.debug({ messageId: msg.message_id, type: msg.message_type }, 'handleIncomingMessage');
|
|
327
|
+
if (!this.messageHandler) {
|
|
328
|
+
log.warn('No message handler registered');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (msg.message_type === 2) {
|
|
332
|
+
log.debug('Skipping bot message');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const msgId = String(msg.message_id);
|
|
336
|
+
if (msgId && this.processedMessages.has(msgId)) {
|
|
337
|
+
log.debug({ messageId: msgId }, 'Skipping duplicate message');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (msgId) {
|
|
341
|
+
this.processedMessages.set(msgId, Date.now());
|
|
342
|
+
this.cleanupProcessedMessages();
|
|
343
|
+
}
|
|
344
|
+
if (!msg.item_list?.length) {
|
|
345
|
+
log.debug('No item_list in message');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// Store context token for replies
|
|
349
|
+
if (msg.from_user_id && msg.context_token) {
|
|
350
|
+
this.setContextToken(msg.from_user_id, msg.context_token);
|
|
351
|
+
}
|
|
352
|
+
const textParts = [];
|
|
353
|
+
const userId = msg.from_user_id || 'unknown';
|
|
354
|
+
for (const item of msg.item_list) {
|
|
355
|
+
const part = await this.extractItemContent(item, msgId, userId);
|
|
356
|
+
if (part)
|
|
357
|
+
textParts.push(part);
|
|
358
|
+
}
|
|
359
|
+
if (!textParts.length) {
|
|
360
|
+
log.debug('No extractable content in message items');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const text = textParts.join('\n');
|
|
364
|
+
log.debug({
|
|
365
|
+
text: text.substring(0, 200),
|
|
366
|
+
fromUserId: msg.from_user_id,
|
|
367
|
+
contextToken: msg.context_token ? 'present' : 'missing',
|
|
368
|
+
itemTypes: msg.item_list.map(i => i.type),
|
|
369
|
+
}, 'Extracted message content');
|
|
370
|
+
const message = {
|
|
371
|
+
id: String(msg.message_id || Date.now()),
|
|
372
|
+
threadId: msg.group_id ? `room:${msg.group_id}` : `user:${msg.from_user_id}`,
|
|
373
|
+
userId,
|
|
374
|
+
text,
|
|
375
|
+
timestamp: new Date(msg.create_time_ms || Date.now()),
|
|
376
|
+
channelId: this.client.getCredentials()?.accountId || 'default',
|
|
377
|
+
};
|
|
378
|
+
const ctx = {
|
|
379
|
+
message,
|
|
380
|
+
platform: 'wechat',
|
|
381
|
+
channelId: this.client.getCredentials()?.accountId || 'default',
|
|
382
|
+
};
|
|
383
|
+
log.debug('Calling message handler');
|
|
384
|
+
await this.messageHandler(ctx);
|
|
385
|
+
log.debug('Message handler completed');
|
|
386
|
+
}
|
|
387
|
+
// ============================================
|
|
388
|
+
// Media Item Handlers
|
|
389
|
+
// ============================================
|
|
390
|
+
/**
|
|
391
|
+
* Extract displayable text content from a single message item. Text items
|
|
392
|
+
* return their text directly; media items are downloaded and surfaced as
|
|
393
|
+
* path markers that downstream agents can read.
|
|
394
|
+
*/
|
|
395
|
+
async extractItemContent(item, msgId, userId) {
|
|
396
|
+
switch (item.type) {
|
|
397
|
+
case ITEM_TYPE.TEXT:
|
|
398
|
+
return item.text_item?.text || null;
|
|
399
|
+
case ITEM_TYPE.IMAGE:
|
|
400
|
+
return this.handleImageItem(item, msgId, userId);
|
|
401
|
+
case ITEM_TYPE.VOICE:
|
|
402
|
+
return this.handleVoiceItem(item, msgId, userId);
|
|
403
|
+
case ITEM_TYPE.FILE:
|
|
404
|
+
return this.handleFileItem(item, msgId, userId);
|
|
405
|
+
case ITEM_TYPE.VIDEO:
|
|
406
|
+
return this.handleVideoItem(item, msgId, userId);
|
|
407
|
+
default:
|
|
408
|
+
log.debug({ type: item.type }, 'Ignoring unsupported item type');
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async handleImageItem(item, msgId, userId) {
|
|
413
|
+
if (!item.image_item)
|
|
414
|
+
return '[图片消息:无图片数据]';
|
|
415
|
+
const cdn = extractCdnMedia(item.image_item);
|
|
416
|
+
if (!cdn) {
|
|
417
|
+
log.warn({ event: 'wechat.media.image_no_cdn', msgId, image_item: item.image_item }, 'Image item has no downloadable CDN media');
|
|
418
|
+
return '[图片消息:无法下载]';
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
const ext = pickExtensionForImage(item.image_item);
|
|
422
|
+
const { path } = await downloadWechatMedia(this.client, cdn, userId, `${msgId}.${ext}`);
|
|
423
|
+
return `[图片附件:${path}]`;
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
427
|
+
log.warn({ event: 'wechat.media.image_failed', err: reason, msgId }, 'image download failed');
|
|
428
|
+
return `[图片附件下载失败:${reason}]`;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async handleVoiceItem(item, msgId, userId) {
|
|
432
|
+
if (!item.voice_item)
|
|
433
|
+
return '[语音消息:无语音数据]';
|
|
434
|
+
// WeChat may provide its own transcription via voice_item.text
|
|
435
|
+
if (item.voice_item.text) {
|
|
436
|
+
const dur = item.voice_item.playtime != null
|
|
437
|
+
? `${Math.round(item.voice_item.playtime / 1000)}s, `
|
|
438
|
+
: '';
|
|
439
|
+
return `[语音转写(${dur}微信识别):${item.voice_item.text}]`;
|
|
440
|
+
}
|
|
441
|
+
// No built-in transcription — download and run STT ourselves
|
|
442
|
+
const cdn = extractCdnMedia(item.voice_item);
|
|
443
|
+
if (!cdn) {
|
|
444
|
+
log.warn({ event: 'wechat.media.voice_no_cdn', msgId, voice_item: item.voice_item }, 'Voice item has no downloadable CDN media and no text');
|
|
445
|
+
return '[语音消息:无法下载且无转写文本]';
|
|
446
|
+
}
|
|
447
|
+
let savedPath = null;
|
|
448
|
+
try {
|
|
449
|
+
const ext = pickExtensionForVoice(item.voice_item);
|
|
450
|
+
const { path } = await downloadWechatMedia(this.client, cdn, userId, `${msgId}.${ext}`);
|
|
451
|
+
savedPath = path;
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
455
|
+
log.warn({ event: 'wechat.media.voice_download_failed', err: reason, msgId }, 'voice download failed');
|
|
456
|
+
return `[语音附件下载失败:${reason}]`;
|
|
457
|
+
}
|
|
458
|
+
if (detectProvider() === 'none') {
|
|
459
|
+
return `[语音附件未转写(未配置 OPENAI_API_KEY 或 IMHUB_WHISPERCPP_BIN):${savedPath}]`;
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
const result = await transcribe(savedPath, { language: 'zh' });
|
|
463
|
+
const dur = item.voice_item.playtime != null
|
|
464
|
+
? `${Math.round(item.voice_item.playtime / 1000)}s, `
|
|
465
|
+
: '';
|
|
466
|
+
return [
|
|
467
|
+
`[语音转写(${dur}provider=${result.provider}, ${result.elapsedMs}ms):`,
|
|
468
|
+
result.text || '(空)',
|
|
469
|
+
`源文件:${savedPath}]`,
|
|
470
|
+
].join('\n');
|
|
471
|
+
}
|
|
472
|
+
catch (err) {
|
|
473
|
+
const reason = err instanceof TranscribeError
|
|
474
|
+
? `${err.provider}: ${err.reason}`
|
|
475
|
+
: err instanceof Error ? err.message : String(err);
|
|
476
|
+
return `[语音转写失败(${reason})\n源文件:${savedPath}]`;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async handleFileItem(item, msgId, userId) {
|
|
480
|
+
if (!item.file_item)
|
|
481
|
+
return '[文件消息:无文件数据]';
|
|
482
|
+
const cdn = extractCdnMedia(item.file_item);
|
|
483
|
+
if (!cdn) {
|
|
484
|
+
log.warn({ event: 'wechat.media.file_no_cdn', msgId, file_item: item.file_item }, 'File item has no downloadable CDN media');
|
|
485
|
+
return `[文件消息:无法下载${item.file_item.file_name ? ` (${item.file_item.file_name})` : ''}]`;
|
|
486
|
+
}
|
|
487
|
+
const displayName = item.file_item.file_name || '未命名文件';
|
|
488
|
+
try {
|
|
489
|
+
const ext = pickExtensionForFile(item.file_item);
|
|
490
|
+
const filename = item.file_item.file_name || `${msgId}.${ext}`;
|
|
491
|
+
const safeName = filename.replace(/[/\\]/g, '_');
|
|
492
|
+
const { path, bytes } = await downloadWechatMedia(this.client, cdn, userId, safeName);
|
|
493
|
+
const sizeStr = bytes >= 1024 * 1024
|
|
494
|
+
? `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
495
|
+
: `${(bytes / 1024).toFixed(0)} KB`;
|
|
496
|
+
return `[文件附件:${path} (${displayName}, ${sizeStr})]`;
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
500
|
+
log.warn({ event: 'wechat.media.file_failed', err: reason, msgId }, 'file download failed');
|
|
501
|
+
return `[文件附件下载失败:${reason} (${displayName})]`;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async handleVideoItem(item, msgId, userId) {
|
|
505
|
+
if (!item.video_item)
|
|
506
|
+
return '[视频消息:无视频数据]';
|
|
507
|
+
const cdn = extractCdnMedia(item.video_item);
|
|
508
|
+
if (!cdn) {
|
|
509
|
+
log.warn({ event: 'wechat.media.video_no_cdn', msgId, video_item: item.video_item }, 'Video item has no downloadable CDN media');
|
|
510
|
+
return '[视频消息:无法下载]';
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const ext = pickExtensionForVideo(item.video_item);
|
|
514
|
+
const { path, bytes } = await downloadWechatMedia(this.client, cdn, userId, `${msgId}.${ext}`);
|
|
515
|
+
const dur = item.video_item.play_length != null
|
|
516
|
+
? `${item.video_item.play_length}s, `
|
|
517
|
+
: '';
|
|
518
|
+
const sizeStr = bytes >= 1024 * 1024
|
|
519
|
+
? `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
520
|
+
: `${(bytes / 1024).toFixed(0)} KB`;
|
|
521
|
+
return `[视频附件:${path} (${dur}${sizeStr})]`;
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
525
|
+
log.warn({ event: 'wechat.media.video_failed', err: reason, msgId }, 'video download failed');
|
|
526
|
+
return `[视频附件下载失败:${reason}]`;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// ============================================
|
|
530
|
+
// Context Token Management
|
|
531
|
+
// ============================================
|
|
532
|
+
getContextToken(userId) {
|
|
533
|
+
const now = Date.now();
|
|
534
|
+
const cached = this.contextTokens.get(userId);
|
|
535
|
+
if (cached) {
|
|
536
|
+
if (now - cached.timestamp > CONTEXT_TOKEN_TTL) {
|
|
537
|
+
this.contextTokens.delete(userId);
|
|
538
|
+
// Fall through to DB — last-resort same-user token might be even
|
|
539
|
+
// older, but the DB-backed cleanup will reject expired rows too.
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
return cached.contextToken;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// Map miss (or just-expired) → fall back to persistent store. This makes
|
|
546
|
+
// first-send-after-restart succeed when the user messaged before the
|
|
547
|
+
// restart and the token is still within its 30-min window.
|
|
548
|
+
const stored = loadContextToken(userId, CONTEXT_TOKEN_TTL, now);
|
|
549
|
+
if (!stored)
|
|
550
|
+
return null;
|
|
551
|
+
// Re-populate the Map so subsequent sends in this run skip the DB.
|
|
552
|
+
this.contextTokens.set(userId, {
|
|
553
|
+
userId: stored.userId,
|
|
554
|
+
contextToken: stored.token,
|
|
555
|
+
timestamp: stored.fetchedAt,
|
|
556
|
+
});
|
|
557
|
+
return stored.token;
|
|
558
|
+
}
|
|
559
|
+
setContextToken(userId, token) {
|
|
560
|
+
const now = Date.now();
|
|
561
|
+
this.contextTokens.set(userId, {
|
|
562
|
+
userId,
|
|
563
|
+
contextToken: token,
|
|
564
|
+
timestamp: now,
|
|
565
|
+
});
|
|
566
|
+
// Write-through. Best-effort: store calls fail-soft on SQLite issues.
|
|
567
|
+
persistContextToken(userId, token, now);
|
|
568
|
+
}
|
|
569
|
+
cleanupProcessedMessages() {
|
|
570
|
+
const now = Date.now();
|
|
571
|
+
for (const [msgId, timestamp] of this.processedMessages) {
|
|
572
|
+
if (now - timestamp > PROCESSED_MESSAGES_TTL) {
|
|
573
|
+
this.processedMessages.delete(msgId);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* L14: drop expired context-token entries. The original implementation
|
|
579
|
+
* only removed an entry on read (`getContextToken` checked TTL), so a
|
|
580
|
+
* userId that messaged once and never came back left its row in the
|
|
581
|
+
* Map indefinitely. Called from the polling loop at the same cadence
|
|
582
|
+
* as cleanupProcessedMessages so memory stays bounded under big group
|
|
583
|
+
* sizes / churning user populations.
|
|
584
|
+
*/
|
|
585
|
+
cleanupExpiredContextTokens() {
|
|
586
|
+
const now = Date.now();
|
|
587
|
+
for (const [userId, cached] of this.contextTokens) {
|
|
588
|
+
if (now - cached.timestamp > CONTEXT_TOKEN_TTL) {
|
|
589
|
+
this.contextTokens.delete(userId);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Mirror the cleanup into the persistent store so its row count stays
|
|
593
|
+
// bounded too. Cheap (indexed by fetched_at).
|
|
594
|
+
cleanupExpiredTokensInStore(CONTEXT_TOKEN_TTL, now);
|
|
595
|
+
}
|
|
596
|
+
// ============================================
|
|
597
|
+
// Credentials Persistence
|
|
598
|
+
// ============================================
|
|
599
|
+
async loadCredentials() {
|
|
600
|
+
try {
|
|
601
|
+
const data = await readFile(CREDENTIALS_FILE, 'utf-8');
|
|
602
|
+
// Defense in depth: nag in logs if the file isn't 0o600. We can't
|
|
603
|
+
// safely chmod it on the user's behalf at load time (concurrent ops,
|
|
604
|
+
// odd ownership) but the warning prompts an operator to fix legacy
|
|
605
|
+
// credentials files written before H9.
|
|
606
|
+
try {
|
|
607
|
+
const st = await stat(CREDENTIALS_FILE);
|
|
608
|
+
if (process.platform !== 'win32' && (st.mode & 0o077) !== 0) {
|
|
609
|
+
log.warn({
|
|
610
|
+
file: CREDENTIALS_FILE,
|
|
611
|
+
mode: (st.mode & 0o777).toString(8),
|
|
612
|
+
}, 'Credentials file is group/world readable — chmod 600 recommended');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch { /* stat failure is non-fatal */ }
|
|
616
|
+
return JSON.parse(data);
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async saveCredentials(credentials) {
|
|
623
|
+
const dir = join(CREDENTIALS_FILE, '..');
|
|
624
|
+
// Restrict the parent dir to the current user. recursive:true is a no-op
|
|
625
|
+
// when the dir already exists with looser perms — accept that compromise
|
|
626
|
+
// (we don't want to chmod existing dirs out from under the operator) and
|
|
627
|
+
// rely on the file-level 0o600 below as the primary line of defense.
|
|
628
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
629
|
+
await writeFile(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 0o600 });
|
|
630
|
+
log.info({ file: CREDENTIALS_FILE }, 'Credentials saved');
|
|
631
|
+
}
|
|
632
|
+
// ============================================
|
|
633
|
+
// Utilities
|
|
634
|
+
// ============================================
|
|
635
|
+
splitMessage(text, maxLength = 2000) {
|
|
636
|
+
if (text.length <= maxLength) {
|
|
637
|
+
return [text];
|
|
638
|
+
}
|
|
639
|
+
const chunks = [];
|
|
640
|
+
let remaining = text;
|
|
641
|
+
while (remaining.length > maxLength) {
|
|
642
|
+
// Try to split at newline
|
|
643
|
+
let splitPoint = remaining.lastIndexOf('\n', maxLength);
|
|
644
|
+
if (splitPoint < maxLength / 2) {
|
|
645
|
+
splitPoint = maxLength;
|
|
646
|
+
}
|
|
647
|
+
chunks.push(remaining.slice(0, splitPoint));
|
|
648
|
+
remaining = remaining.slice(splitPoint).trim();
|
|
649
|
+
}
|
|
650
|
+
if (remaining) {
|
|
651
|
+
chunks.push(remaining);
|
|
652
|
+
}
|
|
653
|
+
// Add continuation markers
|
|
654
|
+
if (chunks.length > 1) {
|
|
655
|
+
for (let i = 0; i < chunks.length - 1; i++) {
|
|
656
|
+
chunks[i] += '\n\n[continued...]';
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return chunks;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// Singleton instance
|
|
663
|
+
export const ilinkWeChatAdapter = new ILinkWeChatAdapter();
|
|
664
|
+
//# sourceMappingURL=ilink-adapter.js.map
|