opencode-telegram-group-topics-bot 0.11.2
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/.env.example +74 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/agent/manager.js +60 -0
- package/dist/agent/types.js +26 -0
- package/dist/app/start-bot-app.js +47 -0
- package/dist/bot/commands/abort.js +116 -0
- package/dist/bot/commands/commands.js +389 -0
- package/dist/bot/commands/constants.js +20 -0
- package/dist/bot/commands/definitions.js +25 -0
- package/dist/bot/commands/help.js +27 -0
- package/dist/bot/commands/models.js +38 -0
- package/dist/bot/commands/new.js +247 -0
- package/dist/bot/commands/opencode-start.js +85 -0
- package/dist/bot/commands/opencode-stop.js +44 -0
- package/dist/bot/commands/projects.js +304 -0
- package/dist/bot/commands/rename.js +173 -0
- package/dist/bot/commands/sessions.js +491 -0
- package/dist/bot/commands/start.js +67 -0
- package/dist/bot/commands/status.js +138 -0
- package/dist/bot/constants.js +49 -0
- package/dist/bot/handlers/agent.js +127 -0
- package/dist/bot/handlers/context.js +125 -0
- package/dist/bot/handlers/document.js +65 -0
- package/dist/bot/handlers/inline-menu.js +124 -0
- package/dist/bot/handlers/model.js +152 -0
- package/dist/bot/handlers/permission.js +281 -0
- package/dist/bot/handlers/prompt.js +263 -0
- package/dist/bot/handlers/question.js +285 -0
- package/dist/bot/handlers/variant.js +147 -0
- package/dist/bot/handlers/voice.js +173 -0
- package/dist/bot/index.js +945 -0
- package/dist/bot/message-patterns.js +4 -0
- package/dist/bot/middleware/auth.js +30 -0
- package/dist/bot/middleware/interaction-guard.js +80 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/scope.js +222 -0
- package/dist/bot/telegram-constants.js +3 -0
- package/dist/bot/telegram-rate-limiter.js +263 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/bot/utils/file-download.js +91 -0
- package/dist/bot/utils/keyboard.js +85 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +57 -0
- package/dist/bot/utils/session-error-filter.js +34 -0
- package/dist/bot/utils/topic-link.js +29 -0
- package/dist/cli/args.js +98 -0
- package/dist/cli.js +80 -0
- package/dist/config.js +103 -0
- package/dist/i18n/de.js +330 -0
- package/dist/i18n/en.js +330 -0
- package/dist/i18n/es.js +330 -0
- package/dist/i18n/index.js +102 -0
- package/dist/i18n/ru.js +330 -0
- package/dist/i18n/zh.js +330 -0
- package/dist/index.js +28 -0
- package/dist/interaction/cleanup.js +24 -0
- package/dist/interaction/constants.js +25 -0
- package/dist/interaction/guard.js +100 -0
- package/dist/interaction/manager.js +113 -0
- package/dist/interaction/types.js +1 -0
- package/dist/keyboard/manager.js +115 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/model/capabilities.js +62 -0
- package/dist/model/manager.js +257 -0
- package/dist/model/types.js +24 -0
- package/dist/opencode/client.js +13 -0
- package/dist/opencode/events.js +159 -0
- package/dist/opencode/prompt-submit-error.js +101 -0
- package/dist/permission/manager.js +92 -0
- package/dist/permission/types.js +1 -0
- package/dist/pinned/manager.js +405 -0
- package/dist/pinned/types.js +1 -0
- package/dist/process/manager.js +273 -0
- package/dist/process/types.js +1 -0
- package/dist/project/manager.js +88 -0
- package/dist/question/manager.js +186 -0
- package/dist/question/types.js +1 -0
- package/dist/rename/manager.js +64 -0
- package/dist/runtime/bootstrap.js +350 -0
- package/dist/runtime/mode.js +74 -0
- package/dist/runtime/paths.js +37 -0
- package/dist/runtime/process-error-handlers.js +24 -0
- package/dist/session/cache-manager.js +455 -0
- package/dist/session/manager.js +87 -0
- package/dist/settings/manager.js +283 -0
- package/dist/stt/client.js +64 -0
- package/dist/summary/aggregator.js +625 -0
- package/dist/summary/formatter.js +417 -0
- package/dist/summary/tool-message-batcher.js +277 -0
- package/dist/topic/colors.js +8 -0
- package/dist/topic/constants.js +10 -0
- package/dist/topic/manager.js +161 -0
- package/dist/topic/title-constants.js +2 -0
- package/dist/topic/title-format.js +10 -0
- package/dist/topic/title-sync.js +17 -0
- package/dist/utils/error-format.js +29 -0
- package/dist/utils/logger.js +175 -0
- package/dist/utils/safe-background-task.js +33 -0
- package/dist/variant/manager.js +103 -0
- package/dist/variant/types.js +1 -0
- package/package.json +76 -0
package/dist/i18n/zh.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
export const zh = {
|
|
2
|
+
"cmd.description.status": "服务器和会话状态",
|
|
3
|
+
"cmd.description.new": "创建新会话",
|
|
4
|
+
"cmd.description.abort": "中止当前操作",
|
|
5
|
+
"cmd.description.stop": "停止当前操作",
|
|
6
|
+
"cmd.description.sessions": "列出会话",
|
|
7
|
+
"cmd.description.projects": "列出项目",
|
|
8
|
+
"cmd.description.commands": "自定义命令",
|
|
9
|
+
"cmd.description.model": "选择模型",
|
|
10
|
+
"cmd.description.agent": "选择代理模式",
|
|
11
|
+
"cmd.description.cleanup": "关闭过期话题",
|
|
12
|
+
"cmd.description.opencode_start": "启动 OpenCode 服务器",
|
|
13
|
+
"cmd.description.opencode_stop": "停止 OpenCode 服务器",
|
|
14
|
+
"cmd.description.help": "帮助",
|
|
15
|
+
"callback.unknown_command": "未知命令",
|
|
16
|
+
"callback.processing_error": "处理错误",
|
|
17
|
+
"error.load_agents": "❌ 加载代理列表失败",
|
|
18
|
+
"error.load_models": "❌ 加载模型列表失败",
|
|
19
|
+
"error.load_variants": "❌ 加载变体列表失败",
|
|
20
|
+
"error.context_button": "❌ 处理上下文按钮失败",
|
|
21
|
+
"error.generic": "🔴 出现了一些问题。",
|
|
22
|
+
"interaction.blocked.expired": "⚠️ 此交互已过期。请重新开始。",
|
|
23
|
+
"interaction.blocked.expected_callback": "⚠️ 此步骤请使用内联按钮,或点击取消。",
|
|
24
|
+
"interaction.blocked.expected_text": "⚠️ 此步骤请发送一条文本消息。",
|
|
25
|
+
"interaction.blocked.expected_command": "⚠️ 此步骤请发送一条命令。",
|
|
26
|
+
"interaction.blocked.command_not_allowed": "⚠️ 当前步骤不可用此命令。",
|
|
27
|
+
"interaction.blocked.finish_current": "⚠️ 请先完成当前交互(回答或取消),然后再打开其他菜单。",
|
|
28
|
+
"inline.blocked.expected_choice": "⚠️ 请使用内联按钮选择一个选项,或点击取消。",
|
|
29
|
+
"inline.blocked.command_not_allowed": "⚠️ 内联菜单激活期间不可用此命令。",
|
|
30
|
+
"question.blocked.expected_answer": "⚠️ 请使用按钮、自定义回答或取消来回答当前问题。",
|
|
31
|
+
"question.blocked.command_not_allowed": "⚠️ 在当前问答流程完成之前不可用此命令。",
|
|
32
|
+
"inline.button.cancel": "❌ 取消",
|
|
33
|
+
"inline.inactive_callback": "此菜单已失效",
|
|
34
|
+
"inline.cancelled_callback": "已取消",
|
|
35
|
+
"common.unknown": "未知",
|
|
36
|
+
"common.unknown_error": "未知错误",
|
|
37
|
+
"start.welcome": "👋 欢迎使用 OpenCode Telegram Group Topics Bot!\n\n可用命令:\n/projects — 选择项目\n/sessions — 会话列表\n/new — 新建会话\n/status — 状态\n/help — 帮助\n\n请使用底部按钮选择模式、模型和变体。",
|
|
38
|
+
"help.keyboard_hint": "💡 代理模式、模型、变体和上下文操作请使用底部键盘按钮。",
|
|
39
|
+
"help.text": "📖 **帮助**\n\n/status - 查看服务器状态\n/sessions - 会话列表\n/new - 创建新会话\n/help - 帮助",
|
|
40
|
+
"bot.thinking": "💭 思考中...",
|
|
41
|
+
"bot.project_not_selected": "🏗 未选择项目。\n\n请先使用 /projects 选择一个项目。",
|
|
42
|
+
"bot.creating_session": "🔄 正在创建新会话...",
|
|
43
|
+
"bot.create_session_error": "🔴 创建会话失败。请重试 /new,或使用 /status 检查服务器状态。",
|
|
44
|
+
"bot.session_created": "✅ 会话已创建:{title}",
|
|
45
|
+
"bot.session_busy": "⏳ 你上一条请求还在执行,所以这条新请求没有启动。\n\n原因:OpenCode 在同一个会话里一次只允许一个运行中的任务。\n建议:先等待当前回复;如果看起来卡住了,先用 /abort,再重新发送消息。",
|
|
46
|
+
"bot.session_reset_project_mismatch": "⚠️ 活动会话与所选项目不匹配,因此已重置。使用 /sessions 选择一个会话,或 /new 创建新会话。",
|
|
47
|
+
"bot.prompt_send_error": "⚠️ 这条消息没有成功发送到 OpenCode。\n\n可能原因:机器人与 OpenCode 服务器之间出现了临时连接问题。\n建议:请重发一次;如果持续出现,请运行 /status 检查 OpenCode 是否可达。",
|
|
48
|
+
"bot.prompt_send_error_session_not_found": "⚠️ 这条消息发送失败,因为当前活动会话已不可用。\n\n原因:该会话可能已被重置、切换或删除。\n建议:先用 /sessions 选择会话,或用 /new 创建新会话,然后再重发消息。",
|
|
49
|
+
"bot.session_error": "🔴 OpenCode 返回错误:{message}",
|
|
50
|
+
"bot.session_retry": "🔁 {message}\n\n提供方在重复重试时持续返回同一错误。使用 /abort 可停止。",
|
|
51
|
+
"bot.unknown_command": "⚠️ 未知命令:{command}。使用 /help 查看可用命令。",
|
|
52
|
+
"bot.photo_downloading": "⏳ 正在下载照片...",
|
|
53
|
+
"bot.photo_too_large": "⚠️ 照片过大(最大 {maxSizeMb}MB)",
|
|
54
|
+
"bot.photo_model_no_image": "⚠️ 当前模型不支持图像输入。将仅发送文本。",
|
|
55
|
+
"bot.photo_download_error": "🔴 下载照片失败",
|
|
56
|
+
"bot.photo_no_caption": "💡 提示:添加说明文字以描述你希望对这张照片做什么。",
|
|
57
|
+
"bot.file_downloading": "⏳ 正在下载文件...",
|
|
58
|
+
"bot.file_too_large": "⚠️ 文件过大(最大 {maxSizeMb}MB)",
|
|
59
|
+
"bot.file_download_error": "🔴 下载文件失败",
|
|
60
|
+
"bot.model_no_pdf": "⚠️ 当前模型不支持PDF输入。将仅发送文本。",
|
|
61
|
+
"bot.text_file_too_large": "⚠️ 文本文件过大(最大 {maxSizeKb}KB)",
|
|
62
|
+
"status.header_running": "🟢 **OpenCode 服务器正在运行**",
|
|
63
|
+
"status.health.healthy": "健康",
|
|
64
|
+
"status.health.unhealthy": "不健康",
|
|
65
|
+
"status.line.health": "状态:{health}",
|
|
66
|
+
"status.line.version": "版本:{version}",
|
|
67
|
+
"status.line.managed_yes": "由机器人管理:是",
|
|
68
|
+
"status.line.managed_no": "由机器人管理:否",
|
|
69
|
+
"status.line.pid": "PID:{pid}",
|
|
70
|
+
"status.line.uptime_sec": "运行时间:{seconds} 秒",
|
|
71
|
+
"status.line.mode": "模式:{mode}",
|
|
72
|
+
"status.line.model": "模型:{model}",
|
|
73
|
+
"status.agent_not_set": "未设置",
|
|
74
|
+
"status.project_selected": "🏗 项目:{project}",
|
|
75
|
+
"status.project_not_selected": "🏗 项目:未选择",
|
|
76
|
+
"status.project_hint": "使用 /projects 选择项目",
|
|
77
|
+
"status.session_selected": "📋 当前会话:{title}",
|
|
78
|
+
"status.session_not_selected": "📋 当前会话:未选择",
|
|
79
|
+
"status.session_hint": "使用 /sessions 选择一个会话,或 /new 创建",
|
|
80
|
+
"status.server_unavailable": "🔴 OpenCode 服务器不可用\n\n使用 /opencode_start 启动服务器。",
|
|
81
|
+
"projects.empty": "📭 未找到项目。\n\n在 OpenCode 中打开一个目录并至少创建一个会话,然后它会出现在这里。",
|
|
82
|
+
"projects.select": "请选择一个项目:",
|
|
83
|
+
"projects.select_with_current": "请选择一个项目:\n\n当前:🏗 {project}",
|
|
84
|
+
"projects.page_indicator": "第 {current}/{total} 页",
|
|
85
|
+
"projects.prev_page": "⬅️ 上一页",
|
|
86
|
+
"projects.next_page": "下一页 ➡️",
|
|
87
|
+
"projects.fetch_error": "🔴 OpenCode 服务器不可用,或加载项目时发生错误。",
|
|
88
|
+
"projects.page_load_error": "无法加载此页面。请重试。",
|
|
89
|
+
"projects.selected": "✅ 已选择项目:{project}\n\n📋 会话已重置。请在此项目中使用 /sessions 或 /new。",
|
|
90
|
+
"projects.select_error": "🔴 选择项目失败。",
|
|
91
|
+
"projects.locked.topic_scope": "⚠️ 此话题已绑定自己的项目/会话范围。请仅在 General 中、创建话题前切换项目。",
|
|
92
|
+
"projects.locked.group_project": "⚠️ 此群组已配置为项目:{project}。若要处理其他仓库,请创建新群组。",
|
|
93
|
+
"projects.locked.callback": "此群组已锁定项目切换。",
|
|
94
|
+
"sessions.project_not_selected": "🏗 未选择项目。\n\n请先使用 /projects 选择一个项目。",
|
|
95
|
+
"sessions.empty": "📭 未找到会话。\n\n使用 /new 创建新会话。",
|
|
96
|
+
"sessions.select": "请选择一个会话:",
|
|
97
|
+
"sessions.select_page": "请选择一个会话(第 {page} 页):",
|
|
98
|
+
"sessions.fetch_error": "🔴 OpenCode 服务器不可用,或加载会话时发生错误。",
|
|
99
|
+
"sessions.select_project_first": "🔴 未选择项目。使用 /projects。",
|
|
100
|
+
"sessions.page_empty_callback": "这一页没有会话",
|
|
101
|
+
"sessions.page_load_error_callback": "无法加载此页面。请重试。",
|
|
102
|
+
"sessions.button.prev_page": "⬅️ 上一页",
|
|
103
|
+
"sessions.button.next_page": "下一页 ➡️",
|
|
104
|
+
"sessions.topic_locked": "⚠️ 此话题已绑定当前会话。请在 General 话题中使用 /new 创建新话题。",
|
|
105
|
+
"sessions.general_overview": "话题会话概览:",
|
|
106
|
+
"sessions.general_item": "• {topic}(线程 #{thread})- {status}",
|
|
107
|
+
"sessions.general_empty": "尚无会话话题。请使用 /new 创建。",
|
|
108
|
+
"sessions.bound_topic_link": "🔗 此会话的话题链接:{url}",
|
|
109
|
+
"sessions.created_topic_link": "✅ 已为此会话创建话题:{url}",
|
|
110
|
+
"sessions.loading_context": "⏳ 正在加载上下文和最新消息...",
|
|
111
|
+
"sessions.selected": "✅ 已选择会话:{title}",
|
|
112
|
+
"sessions.select_error": "🔴 选择会话失败。",
|
|
113
|
+
"sessions.preview.empty": "没有最近消息。",
|
|
114
|
+
"sessions.preview.title": "最近消息:",
|
|
115
|
+
"sessions.preview.you": "你:",
|
|
116
|
+
"sessions.preview.agent": "代理:",
|
|
117
|
+
"new.project_not_selected": "🏗 未选择项目。\n\n请先使用 /projects 选择一个项目。",
|
|
118
|
+
"new.created": "✅ 新会话已创建:{title}",
|
|
119
|
+
"new.topic_only_in_general": "⚠️ 请在 General 话题中运行 /new 以创建专用会话话题。",
|
|
120
|
+
"new.requires_forum_general": "⚠️ /new 仅可在启用论坛的超级群 General 话题中使用。",
|
|
121
|
+
"new.topic_created": "✅ 会话话题已就绪:{title}",
|
|
122
|
+
"new.general_created": "✅ 已创建新的 OpenCode 会话和群组话题。",
|
|
123
|
+
"new.topic_create_error": "🔴 创建会话话题失败。请检查论坛权限后重试。",
|
|
124
|
+
"new.topic_create_no_rights": "🔴 我无法在该群组创建论坛话题。请给机器人授予管理话题(Manage Topics)权限后再执行 /new。",
|
|
125
|
+
"new.general_open_link": "🔗 打开话题:{url}",
|
|
126
|
+
"new.create_error": "🔴 OpenCode 服务器不可用,或创建会话时发生错误。",
|
|
127
|
+
"cleanup.topic_use_general": "⚠️ 请在 General 话题中运行 /cleanup。",
|
|
128
|
+
"cleanup.requires_forum_general": "⚠️ /cleanup 仅在启用论坛的超级群 General 话题中可用。",
|
|
129
|
+
"cleanup.no_topics": "✅ 没有需要清理的会话话题。",
|
|
130
|
+
"cleanup.result": "🧹 清理完成。检查:{inspected},关闭:{closed},跳过:{skipped},失败:{failed}。",
|
|
131
|
+
"stop.no_active_session": "🛑 代理尚未启动\n\n使用 /new 创建会话,或通过 /sessions 选择一个。",
|
|
132
|
+
"stop.in_progress": "🛑 已停止事件流,正在发送中止信号...\n\n等待代理停止。",
|
|
133
|
+
"stop.warn_unconfirmed": "⚠️ 事件流已停止,但服务器未确认中止。\n\n检查 /status,并在几秒后重试 /abort。",
|
|
134
|
+
"stop.warn_maybe_finished": "⚠️ 事件流已停止,但代理可能已完成。",
|
|
135
|
+
"stop.success": "✅ 代理操作已中断。本次运行的后续消息将不再发送。",
|
|
136
|
+
"stop.warn_still_busy": "⚠️ 信号已发送,但代理仍在忙。\n\n事件流已禁用,因此不会发送中间消息。",
|
|
137
|
+
"stop.warn_timeout": "⚠️ 中止请求超时。\n\n事件流已禁用,请在几秒后重试 /abort。",
|
|
138
|
+
"stop.warn_local_only": "⚠️ 已在本地停止事件流,但服务器端中止失败。",
|
|
139
|
+
"stop.error": "🔴 停止操作失败。\n\n事件流已停止,请再次尝试 /abort。",
|
|
140
|
+
"opencode_start.already_running_managed": "⚠️ OpenCode 服务器已在运行\n\nPID:{pid}\n运行时间:{seconds} 秒",
|
|
141
|
+
"opencode_start.already_running_external": "✅ OpenCode 服务器正作为外部进程运行\n\n版本:{version}\n\n该服务器不是由机器人启动,因此 /opencode-stop 无法停止它。",
|
|
142
|
+
"opencode_start.starting": "🔄 正在启动 OpenCode 服务器...",
|
|
143
|
+
"opencode_start.start_error": "🔴 启动 OpenCode 服务器失败\n\n错误:{error}\n\n请检查 OpenCode CLI 已安装且在 PATH 中可用:\n`opencode --version`\n`npm install -g @opencode-ai/cli`",
|
|
144
|
+
"opencode_start.started_not_ready": "⚠️ OpenCode 服务器已启动,但未响应\n\nPID:{pid}\n\n服务器可能仍在启动中。几秒后试试 /status。",
|
|
145
|
+
"opencode_start.success": "✅ OpenCode 服务器启动成功\n\nPID:{pid}\n版本:{version}",
|
|
146
|
+
"opencode_start.error": "🔴 启动服务器时发生错误。\n\n请查看应用日志了解详情。",
|
|
147
|
+
"opencode_stop.external_running": "⚠️ OpenCode 服务器正作为外部进程运行\n\n该服务器不是通过 /opencode-start 启动的。\n请手动停止它,或使用 /status 检查状态。",
|
|
148
|
+
"opencode_stop.not_running": "⚠️ OpenCode 服务器未运行",
|
|
149
|
+
"opencode_stop.stopping": "🛑 正在停止 OpenCode 服务器...\n\nPID:{pid}",
|
|
150
|
+
"opencode_stop.stop_error": "🔴 停止 OpenCode 服务器失败\n\n错误:{error}",
|
|
151
|
+
"opencode_stop.success": "✅ OpenCode 服务器已成功停止",
|
|
152
|
+
"opencode_stop.error": "🔴 停止服务器时发生错误。\n\n请查看应用日志了解详情。",
|
|
153
|
+
"agent.changed_callback": "模式已更改:{name}",
|
|
154
|
+
"agent.changed_message": "✅ 模式已切换为:{name}",
|
|
155
|
+
"agent.change_error_callback": "切换模式失败",
|
|
156
|
+
"agent.menu.current": "当前模式:{name}\n\n请选择模式:",
|
|
157
|
+
"agent.menu.select": "请选择工作模式:",
|
|
158
|
+
"agent.menu.empty": "⚠️ 没有可用的代理",
|
|
159
|
+
"agent.menu.error": "🔴 获取代理列表失败",
|
|
160
|
+
"model.changed_callback": "模型已更改:{name}",
|
|
161
|
+
"model.changed_message": "✅ 模型已切换为:{name}",
|
|
162
|
+
"model.change_error_callback": "切换模型失败",
|
|
163
|
+
"model.menu.empty": "⚠️ 没有可用模型",
|
|
164
|
+
"model.menu.select": "请选择模型:",
|
|
165
|
+
"model.menu.current": "当前模型:{name}\n\n请选择模型:",
|
|
166
|
+
"model.menu.favorites_title": "⭐ 收藏(可在 OpenCode CLI 中将模型加入收藏)",
|
|
167
|
+
"model.menu.favorites_empty": "— 列表为空。",
|
|
168
|
+
"model.menu.recent_title": "🕘 最近使用",
|
|
169
|
+
"model.menu.recent_empty": "— 列表为空。",
|
|
170
|
+
"model.menu.favorites_hint": "ℹ️ 可在 OpenCode CLI 中将模型加入收藏,使其显示在列表顶部。",
|
|
171
|
+
"model.menu.error": "🔴 获取模型列表失败",
|
|
172
|
+
"variant.model_not_selected_callback": "错误:未选择模型",
|
|
173
|
+
"variant.changed_callback": "变体已更改:{name}",
|
|
174
|
+
"variant.changed_message": "✅ 变体已切换为:{name}",
|
|
175
|
+
"variant.change_error_callback": "切换变体失败",
|
|
176
|
+
"variant.select_model_first": "⚠️ 请先选择一个模型",
|
|
177
|
+
"variant.menu.empty": "⚠️ 没有可用变体",
|
|
178
|
+
"variant.menu.current": "当前变体:{name}\n\n请选择变体:",
|
|
179
|
+
"variant.menu.error": "🔴 获取变体列表失败",
|
|
180
|
+
"context.button.confirm": "✅ 是的,压缩上下文",
|
|
181
|
+
"context.no_active_session": "⚠️ 没有活动会话。使用 /new 创建会话",
|
|
182
|
+
"context.confirm_text": '📊 会话 "{title}" 的上下文压缩\n\n这会通过移除历史中的旧消息来减少上下文占用。当前任务不会被中断。\n\n继续?',
|
|
183
|
+
"context.general_not_available": "⚠️ 上下文压缩仅在会话话题中可用,General 中不可用。",
|
|
184
|
+
"context.general_not_available_callback": "请先打开一个会话话题。",
|
|
185
|
+
"context.callback_session_not_found": "未找到会话",
|
|
186
|
+
"context.callback_compacting": "正在压缩上下文...",
|
|
187
|
+
"context.progress": "⏳ 正在压缩上下文...",
|
|
188
|
+
"context.error": "❌ 上下文压缩失败",
|
|
189
|
+
"context.success": "✅ 上下文压缩成功",
|
|
190
|
+
"permission.inactive_callback": "权限请求已失效",
|
|
191
|
+
"permission.processing_error_callback": "处理错误",
|
|
192
|
+
"permission.no_active_request_callback": "错误:没有活动请求",
|
|
193
|
+
"permission.reply.once": "仅允许一次",
|
|
194
|
+
"permission.reply.always": "始终允许",
|
|
195
|
+
"permission.reply.reject": "已拒绝",
|
|
196
|
+
"permission.send_reply_error": "❌ 发送权限回复失败",
|
|
197
|
+
"permission.blocked.expected_reply": "⚠️ 请先使用上方按钮回答权限请求。",
|
|
198
|
+
"permission.blocked.command_not_allowed": "⚠️ 在你回答权限请求之前不可用此命令。",
|
|
199
|
+
"permission.header": "{emoji} **权限请求:{name}**\n\n",
|
|
200
|
+
"permission.button.allow": "✅ 允许一次",
|
|
201
|
+
"permission.button.always": "🔓 始终允许",
|
|
202
|
+
"permission.button.reject": "❌ 拒绝",
|
|
203
|
+
"permission.name.bash": "Bash",
|
|
204
|
+
"permission.name.edit": "编辑",
|
|
205
|
+
"permission.name.write": "写入",
|
|
206
|
+
"permission.name.read": "读取",
|
|
207
|
+
"permission.name.webfetch": "Web 获取",
|
|
208
|
+
"permission.name.websearch": "Web 搜索",
|
|
209
|
+
"permission.name.glob": "文件搜索",
|
|
210
|
+
"permission.name.grep": "内容搜索",
|
|
211
|
+
"permission.name.list": "列出目录",
|
|
212
|
+
"permission.name.task": "任务",
|
|
213
|
+
"permission.name.lsp": "LSP",
|
|
214
|
+
"question.inactive_callback": "投票已失效",
|
|
215
|
+
"question.processing_error_callback": "处理错误",
|
|
216
|
+
"question.select_one_required_callback": "请至少选择一个选项",
|
|
217
|
+
"question.enter_custom_callback": "请发送你的自定义回答",
|
|
218
|
+
"question.cancelled": "❌ 投票已取消",
|
|
219
|
+
"question.answer_already_received": "已收到答案,请稍候...",
|
|
220
|
+
"question.completed_no_answers": "✅ 投票完成(无答案)",
|
|
221
|
+
"question.no_active_project": "❌ 没有活动项目",
|
|
222
|
+
"question.no_active_request": "❌ 没有活动请求",
|
|
223
|
+
"question.send_answers_error": "❌ 向代理发送答案失败",
|
|
224
|
+
"question.multi_hint": "\n*你可以选择多个选项*",
|
|
225
|
+
"question.button.submit": "✅ 完成",
|
|
226
|
+
"question.button.custom": "🔤 自定义回答",
|
|
227
|
+
"question.button.cancel": "❌ 取消",
|
|
228
|
+
"question.use_custom_button_first": '⚠️ 要发送文本,请先点击当前问题的 "自定义回答" 按钮。',
|
|
229
|
+
"question.summary.title": "✅ 投票已完成!\n\n",
|
|
230
|
+
"question.summary.question": "问题 {index}:\n{question}\n\n",
|
|
231
|
+
"question.summary.answer": "回答:\n{answer}\n\n",
|
|
232
|
+
"keyboard.agent_mode": "{emoji} {name} 模式",
|
|
233
|
+
"keyboard.context": "📊 {used} / {limit} ({percent}%)",
|
|
234
|
+
"keyboard.context_empty": "📊 控制面板",
|
|
235
|
+
"keyboard.general_defaults": "新会话默认项:",
|
|
236
|
+
"keyboard.general_defaults_info": "你在此群组中设置的这些默认项会应用到新创建的会话:\n• Agent\n• Model\n• Variant",
|
|
237
|
+
"keyboard.variant": "💭 {name}",
|
|
238
|
+
"keyboard.variant_default": "💡 默认",
|
|
239
|
+
"keyboard.updated": "⌨️ 键盘已更新",
|
|
240
|
+
"keyboard.dm.status": "/status",
|
|
241
|
+
"keyboard.dm.help": "/help",
|
|
242
|
+
"keyboard.dm.opencode_start": "/opencode_start",
|
|
243
|
+
"keyboard.dm.opencode_stop": "/opencode_stop",
|
|
244
|
+
"pinned.default_session_title": "新会话",
|
|
245
|
+
"pinned.unknown": "未知",
|
|
246
|
+
"pinned.line.project": "项目: {project}",
|
|
247
|
+
"pinned.line.model": "模型: {model}",
|
|
248
|
+
"pinned.line.context": "上下文: {used} / {limit} ({percent}%)",
|
|
249
|
+
"pinned.files.title": "文件({count}):",
|
|
250
|
+
"pinned.files.item": " {path}{diff}",
|
|
251
|
+
"pinned.files.more": " ... 还有 {count} 个",
|
|
252
|
+
"tool.todo.overflow": "*(还有 {count} 个任务)*",
|
|
253
|
+
"tool.file_header.write": "写入文件/路径: {path}\n============================================================\n\n",
|
|
254
|
+
"tool.file_header.edit": "编辑文件/路径: {path}\n============================================================\n\n",
|
|
255
|
+
"runtime.wizard.ask_token": "请输入 Telegram 机器人 token(从 @BotFather 获取)。\n> ",
|
|
256
|
+
"runtime.wizard.ask_language": "请选择界面语言。\n输入列表中的语言编号或 locale code。\n按 Enter 保持默认语言:{defaultLocale}\n{options}\n> ",
|
|
257
|
+
"runtime.wizard.language_invalid": "请输入列表中的语言编号或受支持的 locale code。\n",
|
|
258
|
+
"runtime.wizard.language_selected": "已选择语言:{language}\n",
|
|
259
|
+
"runtime.wizard.token_required": "必须提供 token。请重试。\n",
|
|
260
|
+
"runtime.wizard.token_invalid": "token 看起来无效(期望格式 <id>:<secret>)。请重试。\n",
|
|
261
|
+
"runtime.wizard.ask_user_id": "请输入你的 Telegram User ID(可从 @userinfobot 获取)。\n> ",
|
|
262
|
+
"runtime.wizard.user_id_invalid": "请输入一个正整数(> 0)。\n",
|
|
263
|
+
"runtime.wizard.ask_api_url": "请输入 OpenCode API URL(可选)。\n按 Enter 使用默认值:{defaultUrl}\n> ",
|
|
264
|
+
"runtime.wizard.ask_server_username": "请输入 OpenCode 服务器用户名(可选)。\n按 Enter 使用默认值:{defaultUsername}\n> ",
|
|
265
|
+
"runtime.wizard.ask_server_password": "请输入 OpenCode 服务器密码(可选,输入隐藏)。\n按 Enter 跳过。\n> ",
|
|
266
|
+
"runtime.wizard.api_url_invalid": "请输入有效 URL(http/https),或按 Enter 使用默认值。\n",
|
|
267
|
+
"runtime.wizard.start": "OpenCode Telegram Group Topics Bot 设置。\n",
|
|
268
|
+
"runtime.wizard.saved": "配置已保存:\n- {envPath}\n- {settingsPath}\n",
|
|
269
|
+
"runtime.wizard.not_configured_starting": "应用尚未配置。正在启动向导...\n",
|
|
270
|
+
"runtime.wizard.tty_required": "交互式向导需要 TTY 终端。请在交互式 shell 中运行 `opencode-telegram-group-topics-bot config`。",
|
|
271
|
+
"rename.no_session": "⚠️ 没有活动会话。请先创建或选择一个会话。",
|
|
272
|
+
"rename.prompt": "📝 请输入会话的新标题:\n\n当前:{title}",
|
|
273
|
+
"rename.empty_title": "⚠️ 标题不能为空。",
|
|
274
|
+
"rename.success": "✅ 会话已重命名为:{title}",
|
|
275
|
+
"rename.error": "🔴 重命名会话失败。",
|
|
276
|
+
"rename.cancelled": "❌ 重命名已取消。",
|
|
277
|
+
"rename.inactive_callback": "重命名请求已失效",
|
|
278
|
+
"rename.inactive": "⚠️ 重命名请求未激活。请再次运行 /rename。",
|
|
279
|
+
"rename.blocked.expected_name": "⚠️ 请以文本输入新会话名称,或在重命名消息中点击取消。",
|
|
280
|
+
"rename.blocked.command_not_allowed": "⚠️ 重命名等待新名称期间不可用此命令。",
|
|
281
|
+
"rename.button.cancel": "❌ 取消",
|
|
282
|
+
"commands.select": "请选择一个 OpenCode 命令:",
|
|
283
|
+
"commands.empty": "📭 当前项目没有可用的 OpenCode 命令。",
|
|
284
|
+
"commands.fetch_error": "🔴 加载 OpenCode 命令失败。",
|
|
285
|
+
"commands.no_description": "无描述",
|
|
286
|
+
"commands.button.execute": "✅ 执行",
|
|
287
|
+
"commands.button.cancel": "❌ 取消",
|
|
288
|
+
"commands.confirm": "请确认执行命令 {command}。若需带参数执行,请发送一条包含参数的消息。",
|
|
289
|
+
"commands.inactive_callback": "该命令菜单已失效",
|
|
290
|
+
"commands.cancelled_callback": "已取消",
|
|
291
|
+
"commands.execute_callback": "正在执行命令...",
|
|
292
|
+
"commands.executing_prefix": "⚡ 执行命令:",
|
|
293
|
+
"commands.arguments_empty": "⚠️ 参数不能为空。请发送文本或点击执行。",
|
|
294
|
+
"commands.execute_error": "🔴 执行 OpenCode 命令失败。",
|
|
295
|
+
"cmd.description.rename": "重命名当前会话",
|
|
296
|
+
"cli.usage": "用法:\n opencode-telegram-group-topics-bot [start] [--mode sources|installed]\n opencode-telegram-group-topics-bot status\n opencode-telegram-group-topics-bot stop\n opencode-telegram-group-topics-bot config [--mode sources|installed]\n\n说明:\n - 不带命令时默认执行 `start`\n - `config` 默认使用 `installed` 模式,除非显式传入 `--mode sources`",
|
|
297
|
+
"cli.placeholder.status": "命令 `status` 目前是占位符。真实状态检查将会在 service 层(阶段 5)添加。",
|
|
298
|
+
"cli.placeholder.stop": "命令 `stop` 目前是占位符。真实的后台进程停止将会在 service 层(阶段 5)添加。",
|
|
299
|
+
"cli.placeholder.unavailable": "命令不可用。",
|
|
300
|
+
"cli.error.prefix": "CLI 错误:{message}",
|
|
301
|
+
"cli.args.unknown_command": "未知命令:{value}",
|
|
302
|
+
"cli.args.mode_requires_value": "选项 --mode 需要一个值:sources|installed",
|
|
303
|
+
"cli.args.invalid_mode": "无效的 --mode 值:{value}。期望 sources|installed",
|
|
304
|
+
"cli.args.unknown_option": "未知选项:{value}",
|
|
305
|
+
"cli.args.mode_only_start": "选项 --mode 仅支持 start 和 config 命令",
|
|
306
|
+
"legacy.models.fetch_error": "🔴 获取模型列表失败。请使用 /status 检查服务器状态。",
|
|
307
|
+
"legacy.models.empty": "📋 没有可用模型。请在 OpenCode 中配置 providers。",
|
|
308
|
+
"legacy.models.header": "📋 **可用模型:**\n\n",
|
|
309
|
+
"legacy.models.no_provider_models": " ⚠️ 没有可用模型\n",
|
|
310
|
+
"legacy.models.env_hint": "💡 在 .env 中使用该模型:\n",
|
|
311
|
+
"legacy.models.error": "🔴 加载模型列表时发生错误。",
|
|
312
|
+
"stt.recognizing": "🎤 正在识别音频...",
|
|
313
|
+
"stt.recognized": "🎤 识别结果:\n{text}",
|
|
314
|
+
"stt.not_configured": "🎤 语音识别尚未配置。\n\n在 .env 中设置 STT_API_URL 和 STT_API_KEY 以启用。",
|
|
315
|
+
"stt.error": "🔴 识别音频失败:{error}",
|
|
316
|
+
"stt.empty_result": "🎤 音频消息中未检测到语音。",
|
|
317
|
+
"start.welcome_dm": "👋 私聊模式仅支持机器人/服务器状态与控制命令。\n\n请在群组话题线程中进行项目与会话工作。",
|
|
318
|
+
"status.global_overview": "📈 全局概览",
|
|
319
|
+
"status.global_projects": "项目数:{count}",
|
|
320
|
+
"status.global_sessions": "会话数:{count}",
|
|
321
|
+
"dm.restricted.command": "⚠️ 私聊中已禁用会话控制命令。请使用群组话题线程进行项目/会话工作。",
|
|
322
|
+
"dm.restricted.prompt": "⚠️ 私聊中已禁用任务输入。请使用群组话题线程运行 OpenCode 任务。",
|
|
323
|
+
"help.dm.title": "私聊控制命令",
|
|
324
|
+
"help.dm.command_start": "显示私聊模式说明",
|
|
325
|
+
"help.dm.hint": "请使用群组话题线程进行项目/会话工作。",
|
|
326
|
+
"status.dm.title": "私聊状态概览",
|
|
327
|
+
"status.dm.hint": "请使用群组话题线程运行 OpenCode 会话。",
|
|
328
|
+
"group.general.prompts_disabled": "⚠️ General 话题中已禁用任务输入。请使用 /new 创建专用会话话题。",
|
|
329
|
+
"topic.unbound": "⚠️ 此话题尚未绑定会话。请前往 General 话题并执行 /new。",
|
|
330
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { resolveRuntimeMode, setRuntimeMode } from "./runtime/mode.js";
|
|
2
|
+
import { installProcessErrorHandlers } from "./runtime/process-error-handlers.js";
|
|
3
|
+
const EXIT_RUNTIME_ERROR = 1;
|
|
4
|
+
const EXIT_INVALID_ARGS = 2;
|
|
5
|
+
async function main() {
|
|
6
|
+
installProcessErrorHandlers();
|
|
7
|
+
const modeResult = resolveRuntimeMode({
|
|
8
|
+
defaultMode: "sources",
|
|
9
|
+
argv: process.argv.slice(2),
|
|
10
|
+
});
|
|
11
|
+
if (modeResult.error) {
|
|
12
|
+
process.stderr.write(`${modeResult.error}\n`);
|
|
13
|
+
process.exit(EXIT_INVALID_ARGS);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
setRuntimeMode(modeResult.mode);
|
|
17
|
+
const { startBotApp } = await import("./app/start-bot-app.js");
|
|
18
|
+
await startBotApp();
|
|
19
|
+
}
|
|
20
|
+
void main().catch((error) => {
|
|
21
|
+
if (error instanceof Error) {
|
|
22
|
+
process.stderr.write(`Failed to start bot: ${error.message}\n`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
process.stderr.write(`Failed to start bot: ${String(error)}\n`);
|
|
26
|
+
}
|
|
27
|
+
process.exit(EXIT_RUNTIME_ERROR);
|
|
28
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { permissionManager } from "../permission/manager.js";
|
|
2
|
+
import { questionManager } from "../question/manager.js";
|
|
3
|
+
import { renameManager } from "../rename/manager.js";
|
|
4
|
+
import { interactionManager } from "./manager.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
export function clearAllInteractionState(reason, scopeKey = "global") {
|
|
7
|
+
const questionActive = questionManager.isActive(scopeKey);
|
|
8
|
+
const permissionActive = permissionManager.isActive(scopeKey);
|
|
9
|
+
const renameActive = renameManager.isWaitingForName(scopeKey);
|
|
10
|
+
const interactionSnapshot = interactionManager.getSnapshot(scopeKey);
|
|
11
|
+
questionManager.clear(scopeKey);
|
|
12
|
+
permissionManager.clear(scopeKey);
|
|
13
|
+
renameManager.clear(scopeKey);
|
|
14
|
+
interactionManager.clear(reason, scopeKey);
|
|
15
|
+
const hasAnyActiveState = questionActive || permissionActive || renameActive || interactionSnapshot !== null;
|
|
16
|
+
const message = `[InteractionCleanup] Cleared state: reason=${reason}, ` +
|
|
17
|
+
`questionActive=${questionActive}, permissionActive=${permissionActive}, ` +
|
|
18
|
+
`renameActive=${renameActive}, interactionKind=${interactionSnapshot?.kind || "none"}`;
|
|
19
|
+
if (hasAnyActiveState) {
|
|
20
|
+
logger.info(message);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
logger.debug(message);
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const INTERACTION_CLEAR_REASON = {
|
|
2
|
+
MANUAL: "manual",
|
|
3
|
+
STATE_REPLACED: "state_replaced",
|
|
4
|
+
EXPIRED: "expired",
|
|
5
|
+
QUESTION_ERROR: "question_error",
|
|
6
|
+
QUESTION_REPLACED_BY_NEW_POLL: "question_replaced_by_new_poll",
|
|
7
|
+
BOT_STARTUP: "bot_startup",
|
|
8
|
+
CALLBACK_HANDLER_ERROR: "callback_handler_error",
|
|
9
|
+
MESSAGE_HANDLER_ERROR: "message_handler_error",
|
|
10
|
+
SESSION_MISMATCH_RESET: "session_mismatch_reset",
|
|
11
|
+
SESSION_CREATED: "session_created",
|
|
12
|
+
SESSION_SWITCHED: "session_switched",
|
|
13
|
+
STOP_COMMAND: "stop_command",
|
|
14
|
+
RENAME_CANCEL_INACTIVE: "rename_cancel_inactive",
|
|
15
|
+
RENAME_CANCELLED: "rename_cancelled",
|
|
16
|
+
RENAME_MISSING_SESSION_INFO: "rename_missing_session_info",
|
|
17
|
+
RENAME_COMPLETED: "rename_completed",
|
|
18
|
+
PROJECT_SWITCHED: "project_switched",
|
|
19
|
+
PROJECT_SELECT_ERROR: "project_select_error",
|
|
20
|
+
SESSION_SELECT_ERROR: "session_select_error",
|
|
21
|
+
PERMISSION_NO_PENDING_REQUESTS: "permission_no_pending_requests",
|
|
22
|
+
PERMISSION_INACTIVE_CALLBACK: "permission_inactive_callback",
|
|
23
|
+
PERMISSION_INVALID_RUNTIME_CONTEXT: "permission_invalid_runtime_context",
|
|
24
|
+
PERMISSION_REPLIED: "permission_replied",
|
|
25
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { interactionManager } from "./manager.js";
|
|
2
|
+
import { getScopeKeyFromContext } from "../bot/scope.js";
|
|
3
|
+
import { INTERACTION_CLEAR_REASON } from "./constants.js";
|
|
4
|
+
function normalizeIncomingCommand(text) {
|
|
5
|
+
const trimmed = text.trim();
|
|
6
|
+
if (!trimmed.startsWith("/")) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const token = trimmed.split(/\s+/)[0];
|
|
10
|
+
const withoutMention = token.split("@")[0].toLowerCase();
|
|
11
|
+
if (withoutMention.length <= 1) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return withoutMention;
|
|
15
|
+
}
|
|
16
|
+
function classifyIncomingInput(ctx) {
|
|
17
|
+
if (ctx.callbackQuery?.data) {
|
|
18
|
+
return { inputType: "callback" };
|
|
19
|
+
}
|
|
20
|
+
const text = ctx.message?.text;
|
|
21
|
+
if (typeof text === "string") {
|
|
22
|
+
const command = normalizeIncomingCommand(text);
|
|
23
|
+
if (command) {
|
|
24
|
+
return { inputType: "command", command };
|
|
25
|
+
}
|
|
26
|
+
return { inputType: "text" };
|
|
27
|
+
}
|
|
28
|
+
// Photo, voice, audio, and other non-text messages are classified as "other"
|
|
29
|
+
if (ctx.message?.photo) {
|
|
30
|
+
return { inputType: "other" };
|
|
31
|
+
}
|
|
32
|
+
return { inputType: "other" };
|
|
33
|
+
}
|
|
34
|
+
function getExpectedInputBlockReason(expectedInput) {
|
|
35
|
+
switch (expectedInput) {
|
|
36
|
+
case "callback":
|
|
37
|
+
return "expected_callback";
|
|
38
|
+
case "command":
|
|
39
|
+
return "expected_command";
|
|
40
|
+
case "text":
|
|
41
|
+
case "mixed":
|
|
42
|
+
return "expected_text";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function createAllowDecision(inputType, state, command) {
|
|
46
|
+
return {
|
|
47
|
+
allow: true,
|
|
48
|
+
inputType,
|
|
49
|
+
state,
|
|
50
|
+
command,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function createBlockDecision(inputType, state, reason, command) {
|
|
54
|
+
return {
|
|
55
|
+
allow: false,
|
|
56
|
+
inputType,
|
|
57
|
+
state,
|
|
58
|
+
reason,
|
|
59
|
+
command,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function isAllowedRenameCancelCallback(ctx, state) {
|
|
63
|
+
return (state.kind === "rename" &&
|
|
64
|
+
state.expectedInput === "text" &&
|
|
65
|
+
ctx.callbackQuery?.data === "rename:cancel");
|
|
66
|
+
}
|
|
67
|
+
export function resolveInteractionGuardDecision(ctx) {
|
|
68
|
+
const scopeKey = getScopeKeyFromContext(ctx);
|
|
69
|
+
const state = interactionManager.getSnapshot(scopeKey);
|
|
70
|
+
const { inputType, command } = classifyIncomingInput(ctx);
|
|
71
|
+
if (!state) {
|
|
72
|
+
return createAllowDecision(inputType, null, command);
|
|
73
|
+
}
|
|
74
|
+
if (interactionManager.isExpired(scopeKey)) {
|
|
75
|
+
interactionManager.clear(INTERACTION_CLEAR_REASON.EXPIRED, scopeKey);
|
|
76
|
+
return createBlockDecision(inputType, state, "expired", command);
|
|
77
|
+
}
|
|
78
|
+
if (inputType === "command") {
|
|
79
|
+
if (command === "/start") {
|
|
80
|
+
return createAllowDecision(inputType, state, command);
|
|
81
|
+
}
|
|
82
|
+
if (command && state.allowedCommands.includes(command)) {
|
|
83
|
+
return createAllowDecision(inputType, state, command);
|
|
84
|
+
}
|
|
85
|
+
return createBlockDecision(inputType, state, "command_not_allowed", command);
|
|
86
|
+
}
|
|
87
|
+
if (state.expectedInput === "mixed") {
|
|
88
|
+
if (inputType === "callback" || inputType === "text") {
|
|
89
|
+
return createAllowDecision(inputType, state, command);
|
|
90
|
+
}
|
|
91
|
+
return createBlockDecision(inputType, state, "expected_text", command);
|
|
92
|
+
}
|
|
93
|
+
if (inputType === "callback" && isAllowedRenameCancelCallback(ctx, state)) {
|
|
94
|
+
return createAllowDecision(inputType, state, command);
|
|
95
|
+
}
|
|
96
|
+
if (state.expectedInput === inputType) {
|
|
97
|
+
return createAllowDecision(inputType, state, command);
|
|
98
|
+
}
|
|
99
|
+
return createBlockDecision(inputType, state, getExpectedInputBlockReason(state.expectedInput), command);
|
|
100
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { INTERACTION_CLEAR_REASON } from "./constants.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
export const DEFAULT_ALLOWED_INTERACTION_COMMANDS = ["/help", "/status", "/abort"];
|
|
4
|
+
function normalizeCommand(command) {
|
|
5
|
+
const trimmed = command.trim().toLowerCase();
|
|
6
|
+
if (!trimmed) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
10
|
+
const withoutMention = withSlash.split("@")[0];
|
|
11
|
+
if (withoutMention.length <= 1) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return withoutMention;
|
|
15
|
+
}
|
|
16
|
+
function normalizeAllowedCommands(commands) {
|
|
17
|
+
if (commands === undefined) {
|
|
18
|
+
return [...DEFAULT_ALLOWED_INTERACTION_COMMANDS];
|
|
19
|
+
}
|
|
20
|
+
const normalized = new Set();
|
|
21
|
+
for (const command of commands) {
|
|
22
|
+
const value = normalizeCommand(command);
|
|
23
|
+
if (value) {
|
|
24
|
+
normalized.add(value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return Array.from(normalized);
|
|
28
|
+
}
|
|
29
|
+
function cloneState(state) {
|
|
30
|
+
return {
|
|
31
|
+
...state,
|
|
32
|
+
allowedCommands: [...state.allowedCommands],
|
|
33
|
+
metadata: { ...state.metadata },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
class InteractionManager {
|
|
37
|
+
stateByScope = new Map();
|
|
38
|
+
start(options, scopeKey = "global") {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
let expiresAt = null;
|
|
41
|
+
const currentState = this.stateByScope.get(scopeKey) ?? null;
|
|
42
|
+
if (currentState) {
|
|
43
|
+
this.clear(INTERACTION_CLEAR_REASON.STATE_REPLACED, scopeKey);
|
|
44
|
+
}
|
|
45
|
+
if (typeof options.expiresInMs === "number") {
|
|
46
|
+
expiresAt = now + options.expiresInMs;
|
|
47
|
+
}
|
|
48
|
+
const nextState = {
|
|
49
|
+
kind: options.kind,
|
|
50
|
+
expectedInput: options.expectedInput,
|
|
51
|
+
allowedCommands: normalizeAllowedCommands(options.allowedCommands),
|
|
52
|
+
metadata: options.metadata ? { ...options.metadata } : {},
|
|
53
|
+
createdAt: now,
|
|
54
|
+
expiresAt,
|
|
55
|
+
};
|
|
56
|
+
this.stateByScope.set(scopeKey, nextState);
|
|
57
|
+
logger.info(`[InteractionManager] Started interaction: kind=${nextState.kind}, expectedInput=${nextState.expectedInput}, allowedCommands=${nextState.allowedCommands.join(",") || "none"}`);
|
|
58
|
+
return cloneState(nextState);
|
|
59
|
+
}
|
|
60
|
+
get(scopeKey = "global") {
|
|
61
|
+
const state = this.stateByScope.get(scopeKey) ?? null;
|
|
62
|
+
if (!state) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return cloneState(state);
|
|
66
|
+
}
|
|
67
|
+
getSnapshot(scopeKey = "global") {
|
|
68
|
+
return this.get(scopeKey);
|
|
69
|
+
}
|
|
70
|
+
isActive(scopeKey = "global") {
|
|
71
|
+
return this.stateByScope.has(scopeKey);
|
|
72
|
+
}
|
|
73
|
+
isExpired(scopeKey = "global", referenceTimeMs = Date.now()) {
|
|
74
|
+
const state = this.stateByScope.get(scopeKey) ?? null;
|
|
75
|
+
if (!state || state.expiresAt === null) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return referenceTimeMs >= state.expiresAt;
|
|
79
|
+
}
|
|
80
|
+
transition(options, scopeKey = "global") {
|
|
81
|
+
const currentState = this.stateByScope.get(scopeKey) ?? null;
|
|
82
|
+
if (!currentState) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const nextState = {
|
|
87
|
+
...currentState,
|
|
88
|
+
kind: options.kind ?? currentState.kind,
|
|
89
|
+
expectedInput: options.expectedInput ?? currentState.expectedInput,
|
|
90
|
+
allowedCommands: options.allowedCommands !== undefined
|
|
91
|
+
? normalizeAllowedCommands(options.allowedCommands)
|
|
92
|
+
: [...currentState.allowedCommands],
|
|
93
|
+
metadata: options.metadata ? { ...options.metadata } : { ...currentState.metadata },
|
|
94
|
+
expiresAt: options.expiresInMs === undefined
|
|
95
|
+
? currentState.expiresAt
|
|
96
|
+
: options.expiresInMs === null
|
|
97
|
+
? null
|
|
98
|
+
: now + options.expiresInMs,
|
|
99
|
+
};
|
|
100
|
+
this.stateByScope.set(scopeKey, nextState);
|
|
101
|
+
logger.debug(`[InteractionManager] Transitioned interaction: kind=${nextState.kind}, expectedInput=${nextState.expectedInput}, allowedCommands=${nextState.allowedCommands.join(",") || "none"}`);
|
|
102
|
+
return cloneState(nextState);
|
|
103
|
+
}
|
|
104
|
+
clear(reason = "manual", scopeKey = "global") {
|
|
105
|
+
const state = this.stateByScope.get(scopeKey) ?? null;
|
|
106
|
+
if (!state) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
logger.info(`[InteractionManager] Cleared interaction: reason=${reason}, kind=${state.kind}, expectedInput=${state.expectedInput}`);
|
|
110
|
+
this.stateByScope.delete(scopeKey);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export const interactionManager = new InteractionManager();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|