@grinev/opencode-telegram-bot 0.16.1 → 0.18.0
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 +5 -0
- package/README.md +72 -43
- package/dist/app/start-bot-app.js +0 -2
- package/dist/attach/manager.js +66 -0
- package/dist/attach/service.js +166 -0
- package/dist/bot/commands/abort.js +0 -4
- package/dist/bot/commands/commands.js +13 -3
- package/dist/bot/commands/definitions.js +2 -0
- package/dist/bot/commands/new.js +10 -18
- package/dist/bot/commands/opencode-start.js +27 -24
- package/dist/bot/commands/opencode-stop.js +51 -23
- package/dist/bot/commands/projects.js +30 -6
- package/dist/bot/commands/sessions.js +19 -20
- package/dist/bot/commands/skills.js +376 -0
- package/dist/bot/commands/start.js +2 -0
- package/dist/bot/commands/status.js +21 -14
- package/dist/bot/commands/worktree.js +196 -0
- package/dist/bot/handlers/inline-menu.js +1 -0
- package/dist/bot/handlers/prompt.js +26 -31
- package/dist/bot/handlers/voice.js +8 -1
- package/dist/bot/index.js +90 -10
- package/dist/bot/middleware/interaction-guard.js +1 -1
- package/dist/bot/utils/busy-guard.js +3 -2
- package/dist/bot/utils/external-user-input.js +46 -0
- package/dist/bot/utils/switch-project.js +2 -0
- package/dist/config.js +2 -0
- package/dist/external-input/suppression.js +54 -0
- package/dist/git/worktree.js +158 -0
- package/dist/i18n/de.js +54 -1
- package/dist/i18n/en.js +54 -1
- package/dist/i18n/es.js +54 -1
- package/dist/i18n/fr.js +54 -1
- package/dist/i18n/ru.js +54 -1
- package/dist/i18n/zh.js +54 -1
- package/dist/interaction/guard.js +2 -1
- package/dist/opencode/events.js +13 -1
- package/dist/opencode/process.js +182 -0
- package/dist/pinned/manager.js +46 -61
- package/dist/project/manager.js +47 -32
- package/dist/runtime/bootstrap.js +119 -7
- package/dist/scheduled-task/executor.js +137 -7
- package/dist/settings/manager.js +9 -12
- package/dist/summary/aggregator.js +54 -9
- package/package.json +1 -1
- package/dist/process/manager.js +0 -273
- package/dist/process/types.js +0 -1
package/dist/i18n/fr.js
CHANGED
|
@@ -5,9 +5,11 @@ export const fr = {
|
|
|
5
5
|
"cmd.description.sessions": "Lister les sessions",
|
|
6
6
|
"cmd.description.tts": "Basculer les réponses audio",
|
|
7
7
|
"cmd.description.projects": "Lister les projets",
|
|
8
|
+
"cmd.description.worktree": "Changer de worktree git",
|
|
8
9
|
"cmd.description.task": "Créer une tâche planifiée",
|
|
9
10
|
"cmd.description.tasklist": "Afficher les tâches planifiées",
|
|
10
11
|
"cmd.description.commands": "Commandes personnalisées",
|
|
12
|
+
"cmd.description.skills": "Catalogue de skills",
|
|
11
13
|
"cmd.description.opencode_start": "Démarrer le serveur OpenCode",
|
|
12
14
|
"cmd.description.opencode_stop": "Arrêter le serveur OpenCode",
|
|
13
15
|
"cmd.description.help": "Aide",
|
|
@@ -33,7 +35,7 @@ export const fr = {
|
|
|
33
35
|
"inline.cancelled_callback": "Annulé",
|
|
34
36
|
"common.unknown": "inconnu",
|
|
35
37
|
"common.unknown_error": "erreur inconnue",
|
|
36
|
-
"start.welcome": "👋 Bienvenue dans OpenCode Telegram Bot !\n\nUtilisez les commandes :\n/projects — sélectionner un projet\n/sessions — liste des sessions\n/new — nouvelle session\n/task — tâche planifiée\n/tasklist — tâches planifiées\n/status — statut\n/help — aide\n\nUtilisez les boutons du bas pour choisir l'agent, le modèle et la variante.",
|
|
38
|
+
"start.welcome": "👋 Bienvenue dans OpenCode Telegram Bot !\n\nUtilisez les commandes :\n/projects — sélectionner un projet\n/sessions — liste des sessions\n/new — nouvelle session\n/commands — commandes personnalisées\n/skills — catalogue de skills\n/task — tâche planifiée\n/tasklist — tâches planifiées\n/status — statut\n/help — aide\n\nUtilisez les boutons du bas pour choisir l'agent, le modèle et la variante.",
|
|
37
39
|
"help.keyboard_hint": "💡 Utilisez les boutons du bas pour l'agent, le modèle, la variante et les actions de contexte.",
|
|
38
40
|
"help.text": "📖 **Aide**\n\n/status - Vérifier l'état du serveur\n/sessions - Liste des sessions\n/new - Créer une nouvelle session\n/help - Aide",
|
|
39
41
|
"bot.thinking": "💭 Réflexion en cours...",
|
|
@@ -46,6 +48,7 @@ export const fr = {
|
|
|
46
48
|
"bot.prompt_send_error": "Impossible d'envoyer la requête à OpenCode.",
|
|
47
49
|
"bot.session_error": "🔴 OpenCode a renvoyé une erreur : {message}",
|
|
48
50
|
"bot.session_retry": "🔁 {message}\n\nLe fournisseur renvoie la même erreur à chaque nouvelle tentative. Utilisez /abort pour arrêter.",
|
|
51
|
+
"bot.external_user_input": "Entrée utilisateur externe",
|
|
49
52
|
"bot.unknown_command": "⚠️ Commande inconnue : {command}. Utilisez /help pour voir les commandes disponibles.",
|
|
50
53
|
"bot.photo_downloading": "⏳ Téléchargement de la photo...",
|
|
51
54
|
"bot.photo_too_large": "⚠️ La photo est trop volumineuse (max {maxSizeMb}MB)",
|
|
@@ -73,6 +76,7 @@ export const fr = {
|
|
|
73
76
|
"status.tts.off": "Désactivées",
|
|
74
77
|
"status.agent_not_set": "non défini",
|
|
75
78
|
"status.project_selected": "Projet : {project}",
|
|
79
|
+
"status.worktree_selected": "Worktree : {worktree}",
|
|
76
80
|
"status.project_not_selected": "Projet : non sélectionné",
|
|
77
81
|
"status.project_hint": "Utilisez /projects pour sélectionner un projet",
|
|
78
82
|
"status.session_selected": "Session actuelle : {title}",
|
|
@@ -110,6 +114,17 @@ export const fr = {
|
|
|
110
114
|
"sessions.preview.title": "Messages récents :",
|
|
111
115
|
"sessions.preview.you": "Vous :",
|
|
112
116
|
"sessions.preview.agent": "Agent :",
|
|
117
|
+
"attach.project_not_selected": "🏗 Aucun projet sélectionné.\n\nSélectionnez d'abord un projet avec /projects.",
|
|
118
|
+
"attach.session_not_selected": "💬 Aucune session sélectionnée.\n\nSélectionnez d'abord une session avec /sessions.",
|
|
119
|
+
"attach.session_project_mismatch": "⚠️ La session sélectionnée ne correspond pas au projet actuel. Sélectionnez-la de nouveau via /sessions.",
|
|
120
|
+
"attach.connected": "✅ Connecté à la session : {title}",
|
|
121
|
+
"attach.already_connected": "ℹ️ Déjà connecté à la session : {title}",
|
|
122
|
+
"attach.status.idle_message": "Statut : idle. En attente de nouveaux événements.",
|
|
123
|
+
"attach.status.busy_message": "Statut : busy. Les nouveaux prompts sont temporairement bloqués.",
|
|
124
|
+
"attach.restored_question": "Une question en attente a été restaurée pour cette session.",
|
|
125
|
+
"attach.restored_permissions": "Demandes de permission en attente restaurées : {count}.",
|
|
126
|
+
"attach.disconnect_hint": "Pour vous déconnecter, passez simplement à une autre session ou à un autre projet.",
|
|
127
|
+
"attach.error": "🔴 Impossible de se connecter à la session actuelle.",
|
|
113
128
|
"new.project_not_selected": "🏗 Aucun projet n'est sélectionné.\n\nSélectionnez d'abord un projet avec /projects.",
|
|
114
129
|
"new.created": "✅ Nouvelle session créée : {title}",
|
|
115
130
|
"new.create_error": "🔴 Le serveur OpenCode est indisponible ou une erreur s'est produite lors de la création de la session.",
|
|
@@ -124,15 +139,20 @@ export const fr = {
|
|
|
124
139
|
"stop.error": "🔴 Impossible d'arrêter l'action.\n\nLe flux d'événements est arrêté, essayez /abort à nouveau.",
|
|
125
140
|
"opencode_start.already_running_managed": "⚠️ Le serveur OpenCode est déjà en cours d'exécution\n\nPID : {pid}\nTemps de fonctionnement : {seconds} secondes",
|
|
126
141
|
"opencode_start.already_running_external": "✅ Le serveur OpenCode est déjà en cours d'exécution en tant que processus externe\n\nVersion : {version}\n\nCe serveur n'a pas été démarré par le bot, donc /opencode-stop ne peut pas l'arrêter.",
|
|
142
|
+
"opencode_start.already_running": "✅ Le serveur OpenCode est déjà en cours d'exécution\n\nVersion : {version}",
|
|
143
|
+
"opencode_start.remote_configured": "⚠️ /opencode_start fonctionne uniquement avec un serveur OpenCode local.",
|
|
127
144
|
"opencode_start.starting": "🔄 Démarrage du serveur OpenCode...",
|
|
128
145
|
"opencode_start.start_error": "🔴 Impossible de démarrer le serveur OpenCode\n\nErreur : {error}\n\nVérifiez que l'interface en ligne de commande OpenCode est installée et disponible dans le PATH :\nopencode --version\nnpm install -g @opencode-ai/cli",
|
|
129
146
|
"opencode_start.started_not_ready": "⚠️ Le serveur OpenCode a démarré, mais ne répond pas encore\n\nPID : {pid}\n\nLe serveur est peut-être encore en cours de démarrage. Essayez /status dans quelques secondes.",
|
|
130
147
|
"opencode_start.success": "✅ Serveur OpenCode démarré avec succès\n\nPID : {pid}\nVersion : {version}",
|
|
131
148
|
"opencode_start.error": "🔴 Une erreur s'est produite lors du démarrage du serveur.\n\nConsultez les logs de l'application pour plus de détails.",
|
|
132
149
|
"opencode_stop.external_running": "⚠️ Le serveur OpenCode s'exécute comme processus externe\n\nCe serveur n'a pas été démarré via /opencode-start.\nArrêtez-le manuellement ou utilisez /status pour vérifier son état.",
|
|
150
|
+
"opencode_stop.remote_configured": "⚠️ /opencode_stop fonctionne uniquement avec un serveur OpenCode local.",
|
|
133
151
|
"opencode_stop.not_running": "⚠️ Le serveur OpenCode n'est pas en cours d'exécution",
|
|
152
|
+
"opencode_stop.pid_not_found": "⚠️ Le serveur OpenCode répond sur le port {port}, mais aucun processus local à arrêter n'a été trouvé.",
|
|
134
153
|
"opencode_stop.stopping": "🛑 Arrêt du serveur OpenCode...\n\nPID : {pid}",
|
|
135
154
|
"opencode_stop.stop_error": "🔴 Impossible d'arrêter le serveur OpenCode\n\nErreur : {error}",
|
|
155
|
+
"opencode_stop.still_running": "Le serveur répond toujours après la demande d'arrêt.",
|
|
136
156
|
"opencode_stop.success": "✅ Serveur OpenCode arrêté avec succès",
|
|
137
157
|
"opencode_stop.error": "🔴 Une erreur s'est produite lors de l'arrêt du serveur.\n\nConsultez les logs de l'application pour plus de détails.",
|
|
138
158
|
"agent.changed_callback": "Agent modifié : {name}",
|
|
@@ -222,7 +242,11 @@ export const fr = {
|
|
|
222
242
|
"pinned.default_session_title": "nouvelle session",
|
|
223
243
|
"pinned.unknown": "Inconnu",
|
|
224
244
|
"pinned.line.project": "Projet : {project}",
|
|
245
|
+
"pinned.line.worktree": "Worktree : {worktree}",
|
|
225
246
|
"pinned.line.model": "Modèle : {model}",
|
|
247
|
+
"pinned.line.attach": "Tracking : {status}",
|
|
248
|
+
"pinned.attach.status.idle": "actif, idle",
|
|
249
|
+
"pinned.attach.status.busy": "actif, busy",
|
|
226
250
|
"pinned.line.context": "Contexte : {used} / {limit} ({percent}%)",
|
|
227
251
|
"pinned.line.cost": "Coût : {cost} dépensé",
|
|
228
252
|
"subagent.header": "Sous-agent {agent} : {description}",
|
|
@@ -323,6 +347,23 @@ export const fr = {
|
|
|
323
347
|
"commands.button.next_page": "Suivant ➡️",
|
|
324
348
|
"commands.page_empty_callback": "Aucune commande sur cette page",
|
|
325
349
|
"commands.page_load_error_callback": "Impossible de charger cette page. Veuillez réessayer.",
|
|
350
|
+
"skills.select": "Choisissez un skill OpenCode :",
|
|
351
|
+
"skills.empty": "📭 Aucun skill OpenCode n'est disponible pour ce projet.",
|
|
352
|
+
"skills.fetch_error": "🔴 Impossible de charger les skills OpenCode.",
|
|
353
|
+
"skills.no_description": "Aucune description",
|
|
354
|
+
"skills.button.execute": "✅ Exécuter",
|
|
355
|
+
"skills.button.cancel": "❌ Annuler",
|
|
356
|
+
"skills.confirm": "Confirmez l'exécution du skill {skill}. Pour l'exécuter avec des arguments, envoyez-les dans un message.",
|
|
357
|
+
"skills.inactive_callback": "Ce menu de skills est inactif",
|
|
358
|
+
"skills.cancelled_callback": "Annulé",
|
|
359
|
+
"skills.execute_callback": "Utilisation du skill...",
|
|
360
|
+
"skills.executing_prefix": "⚡ Utilisation du skill :",
|
|
361
|
+
"skills.arguments_empty": "⚠️ Les arguments ne peuvent pas être vides. Envoyez du texte ou appuyez sur Exécuter.",
|
|
362
|
+
"skills.select_page": "Choisissez un skill OpenCode (page {page}) :",
|
|
363
|
+
"skills.button.prev_page": "⬅️ Précédent",
|
|
364
|
+
"skills.button.next_page": "Suivant ➡️",
|
|
365
|
+
"skills.page_empty_callback": "Aucun skill sur cette page",
|
|
366
|
+
"skills.page_load_error_callback": "Impossible de charger cette page. Veuillez réessayer.",
|
|
326
367
|
"cmd.description.rename": "Renommer la session actuelle",
|
|
327
368
|
"legacy.models.fetch_error": "🔴 Impossible de récupérer la liste des modèles. Vérifiez l'état du serveur avec /status.",
|
|
328
369
|
"legacy.models.empty": "📋 Aucun modèle disponible. Configurez les fournisseurs dans OpenCode.",
|
|
@@ -336,6 +377,18 @@ export const fr = {
|
|
|
336
377
|
"stt.error": "🔴 Impossible de reconnaître l'audio : {error}",
|
|
337
378
|
"stt.empty_result": "🎤 Aucune parole détectée dans le message audio.",
|
|
338
379
|
"cmd.description.open": "Ajouter un projet en parcourant les dossiers",
|
|
380
|
+
"worktree.branch_detached": "detached HEAD",
|
|
381
|
+
"worktree.select_with_current": "Sélectionnez un worktree :",
|
|
382
|
+
"worktree.project_not_selected": "🏗 Aucun projet sélectionné.\n\nSélectionnez d'abord un projet avec /projects.",
|
|
383
|
+
"worktree.not_git_repo": "🌿 Les git worktrees ne sont pas disponibles pour le projet actuel. Sélectionnez d'abord un dépôt git.",
|
|
384
|
+
"worktree.not_git_repo_callback": "Le projet actuel n'est pas un dépôt git",
|
|
385
|
+
"worktree.empty": "📭 Aucun git worktree trouvé pour le dépôt actuel.",
|
|
386
|
+
"worktree.fetch_error": "🔴 Impossible de charger les git worktrees.",
|
|
387
|
+
"worktree.page_empty_callback": "Aucun worktree sur cette page",
|
|
388
|
+
"worktree.selection_missing_callback": "Le worktree sélectionné n'est plus disponible",
|
|
389
|
+
"worktree.already_selected_callback": "Ce worktree est déjà sélectionné",
|
|
390
|
+
"worktree.selected": "✅ Worktree sélectionné : {worktree}\n\n📋 La session a été réinitialisée. Utilisez /sessions ou /new pour continuer.",
|
|
391
|
+
"worktree.select_error": "🔴 Impossible de sélectionner le worktree.",
|
|
339
392
|
"open.back": "⬆️ Remonter",
|
|
340
393
|
"open.roots": "📋 Retour aux racines",
|
|
341
394
|
"open.prev_page": "⬅️ Précédent",
|
package/dist/i18n/ru.js
CHANGED
|
@@ -5,9 +5,11 @@ export const ru = {
|
|
|
5
5
|
"cmd.description.sessions": "Список сессий",
|
|
6
6
|
"cmd.description.tts": "Переключить аудиоответы",
|
|
7
7
|
"cmd.description.projects": "Список проектов",
|
|
8
|
+
"cmd.description.worktree": "Переключить git worktree",
|
|
8
9
|
"cmd.description.task": "Создать задачу по расписанию",
|
|
9
10
|
"cmd.description.tasklist": "Список задач по расписанию",
|
|
10
11
|
"cmd.description.commands": "Пользовательские команды",
|
|
12
|
+
"cmd.description.skills": "Каталог скиллов",
|
|
11
13
|
"cmd.description.opencode_start": "Запустить OpenCode сервер",
|
|
12
14
|
"cmd.description.opencode_stop": "Остановить OpenCode сервер",
|
|
13
15
|
"cmd.description.help": "Справка",
|
|
@@ -33,7 +35,7 @@ export const ru = {
|
|
|
33
35
|
"inline.cancelled_callback": "Отменено",
|
|
34
36
|
"common.unknown": "неизвестна",
|
|
35
37
|
"common.unknown_error": "неизвестная ошибка",
|
|
36
|
-
"start.welcome": "👋 Добро пожаловать в OpenCode Telegram Bot!\n\nИспользуйте команды:\n/projects — выбрать проект\n/sessions — список сессий\n/new — новая сессия\n/task — задача по расписанию\n/tasklist — список задач по расписанию\n/status — статус\n/help — справка\n\nАгент, модель и вариант выбираются кнопками внизу.",
|
|
38
|
+
"start.welcome": "👋 Добро пожаловать в OpenCode Telegram Bot!\n\nИспользуйте команды:\n/projects — выбрать проект\n/sessions — список сессий\n/new — новая сессия\n/commands — пользовательские команды\n/skills — каталог скиллов\n/task — задача по расписанию\n/tasklist — список задач по расписанию\n/status — статус\n/help — справка\n\nАгент, модель и вариант выбираются кнопками внизу.",
|
|
37
39
|
"help.keyboard_hint": "💡 Агент, модель, вариант и действия с контекстом доступны через нижние кнопки клавиатуры.",
|
|
38
40
|
"help.text": "📖 **Справка**\n\n/status - Проверить статус сервера\n/sessions - Список сессий\n/new - Создать новую сессию\n/help - Справка",
|
|
39
41
|
"bot.thinking": "💭 Думаю...",
|
|
@@ -46,6 +48,7 @@ export const ru = {
|
|
|
46
48
|
"bot.prompt_send_error": "Не удалось отправить запрос в OpenCode.",
|
|
47
49
|
"bot.session_error": "🔴 OpenCode вернул ошибку: {message}",
|
|
48
50
|
"bot.session_retry": "🔁 {message}\n\nПровайдер возвращает одну и ту же ошибку при повторных запросах. Используйте /abort для остановки.",
|
|
51
|
+
"bot.external_user_input": "Внешний ввод пользователя",
|
|
49
52
|
"bot.unknown_command": "⚠️ Неизвестная команда: {command}. Используйте /help для списка команд.",
|
|
50
53
|
"bot.photo_downloading": "⏳ Скачиваю фото...",
|
|
51
54
|
"bot.photo_too_large": "⚠️ Фото слишком большое (макс. {maxSizeMb}МБ)",
|
|
@@ -73,6 +76,7 @@ export const ru = {
|
|
|
73
76
|
"status.tts.off": "Выкл",
|
|
74
77
|
"status.agent_not_set": "не установлен",
|
|
75
78
|
"status.project_selected": "Проект: {project}",
|
|
79
|
+
"status.worktree_selected": "Worktree: {worktree}",
|
|
76
80
|
"status.project_not_selected": "Проект: не выбран",
|
|
77
81
|
"status.project_hint": "Используйте /projects для выбора проекта",
|
|
78
82
|
"status.session_selected": "Текущая сессия: {title}",
|
|
@@ -110,6 +114,17 @@ export const ru = {
|
|
|
110
114
|
"sessions.preview.title": "Последние сообщения:",
|
|
111
115
|
"sessions.preview.you": "Вы:",
|
|
112
116
|
"sessions.preview.agent": "Агент:",
|
|
117
|
+
"attach.project_not_selected": "🏗 Проект не выбран.\n\nСначала выберите проект командой /projects.",
|
|
118
|
+
"attach.session_not_selected": "💬 Сессия не выбрана.\n\nСначала выберите сессию через /sessions.",
|
|
119
|
+
"attach.session_project_mismatch": "⚠️ Выбранная сессия не соответствует текущему проекту. Повторно выберите сессию через /sessions.",
|
|
120
|
+
"attach.connected": "✅ Подключился к сессии: {title}",
|
|
121
|
+
"attach.already_connected": "ℹ️ Уже подключен к сессии: {title}",
|
|
122
|
+
"attach.status.idle_message": "Статус: idle. Жду новые события.",
|
|
123
|
+
"attach.status.busy_message": "Статус: busy. Новые промты временно заблокированы.",
|
|
124
|
+
"attach.restored_question": "Восстановил ожидающий вопрос для этой сессии.",
|
|
125
|
+
"attach.restored_permissions": "Восстановил ожидающие запросы разрешений: {count}.",
|
|
126
|
+
"attach.disconnect_hint": "Чтобы отключиться, переключитесь на другую сессию или проект.",
|
|
127
|
+
"attach.error": "🔴 Не удалось подключиться к текущей сессии.",
|
|
113
128
|
"new.project_not_selected": "🏗 Проект не выбран.\n\nСначала выберите проект командой /projects.",
|
|
114
129
|
"new.created": "✅ Создана новая сессия: {title}",
|
|
115
130
|
"new.create_error": "🔴 OpenCode Server недоступен или произошла ошибка при создании сессии.",
|
|
@@ -124,15 +139,20 @@ export const ru = {
|
|
|
124
139
|
"stop.error": "🔴 Ошибка при прерывании действия.\n\nПоток событий остановлен, попробуйте /abort еще раз.",
|
|
125
140
|
"opencode_start.already_running_managed": "⚠️ OpenCode Server уже запущен\n\nPID: {pid}\nUptime: {seconds} секунд",
|
|
126
141
|
"opencode_start.already_running_external": "✅ OpenCode Server уже запущен внешним процессом\n\nВерсия: {version}\n\nЭтот сервер не был запущен через бота, поэтому команда /opencode-stop не сможет его остановить.",
|
|
142
|
+
"opencode_start.already_running": "✅ OpenCode Server уже запущен\n\nВерсия: {version}",
|
|
143
|
+
"opencode_start.remote_configured": "⚠️ /opencode_start работает только с локальным OpenCode Server.",
|
|
127
144
|
"opencode_start.starting": "🔄 Запускаю OpenCode Server...",
|
|
128
145
|
"opencode_start.start_error": "🔴 Не удалось запустить OpenCode Server\n\nОшибка: {error}\n\nПроверьте, что OpenCode CLI установлен и доступен в PATH:\nopencode --version\nnpm install -g @opencode-ai/cli",
|
|
129
146
|
"opencode_start.started_not_ready": "⚠️ OpenCode Server запущен, но не отвечает\n\nPID: {pid}\n\nСервер может запускаться. Попробуйте /status через несколько секунд.",
|
|
130
147
|
"opencode_start.success": "✅ OpenCode Server успешно запущен\n\nPID: {pid}\nВерсия: {version}",
|
|
131
148
|
"opencode_start.error": "🔴 Произошла ошибка при запуске сервера.\n\nПроверьте логи приложения для подробностей.",
|
|
132
149
|
"opencode_stop.external_running": "⚠️ OpenCode Server запущен внешним процессом\n\nЭтот сервер не был запущен через /opencode-start.\nОстановите его вручную или используйте /status для проверки состояния.",
|
|
150
|
+
"opencode_stop.remote_configured": "⚠️ /opencode_stop работает только с локальным OpenCode Server.",
|
|
133
151
|
"opencode_stop.not_running": "⚠️ OpenCode Server не запущен",
|
|
152
|
+
"opencode_stop.pid_not_found": "⚠️ OpenCode Server отвечает на порту {port}, но локальный процесс для остановки найти не удалось.",
|
|
134
153
|
"opencode_stop.stopping": "🛑 Останавливаю OpenCode Server...\n\nPID: {pid}",
|
|
135
154
|
"opencode_stop.stop_error": "🔴 Не удалось остановить OpenCode Server\n\nОшибка: {error}",
|
|
155
|
+
"opencode_stop.still_running": "Сервер все еще отвечает после запроса на остановку.",
|
|
136
156
|
"opencode_stop.success": "✅ OpenCode Server успешно остановлен",
|
|
137
157
|
"opencode_stop.error": "🔴 Произошла ошибка при остановке сервера.\n\nПроверьте логи приложения для подробностей.",
|
|
138
158
|
"agent.changed_callback": "Агент изменен: {name}",
|
|
@@ -222,7 +242,11 @@ export const ru = {
|
|
|
222
242
|
"pinned.default_session_title": "новая сессия",
|
|
223
243
|
"pinned.unknown": "Неизвестно",
|
|
224
244
|
"pinned.line.project": "Проект: {project}",
|
|
245
|
+
"pinned.line.worktree": "Worktree: {worktree}",
|
|
225
246
|
"pinned.line.model": "Модель: {model}",
|
|
247
|
+
"pinned.line.attach": "Tracking: {status}",
|
|
248
|
+
"pinned.attach.status.idle": "активен, idle",
|
|
249
|
+
"pinned.attach.status.busy": "активен, busy",
|
|
226
250
|
"pinned.line.context": "Контекст: {used} / {limit} ({percent}%)",
|
|
227
251
|
"pinned.line.cost": "Стоимость: {cost} потрачено",
|
|
228
252
|
"subagent.header": "Сабагент {agent}: {description}",
|
|
@@ -323,6 +347,23 @@ export const ru = {
|
|
|
323
347
|
"commands.button.next_page": "Вперёд ➡️",
|
|
324
348
|
"commands.page_empty_callback": "На этой странице нет команд",
|
|
325
349
|
"commands.page_load_error_callback": "Не удалось загрузить эту страницу. Пожалуйста, попробуйте снова.",
|
|
350
|
+
"skills.select": "Выберите скилл OpenCode:",
|
|
351
|
+
"skills.empty": "📭 Для этого проекта нет доступных скиллов OpenCode.",
|
|
352
|
+
"skills.fetch_error": "🔴 Не удалось загрузить список скиллов OpenCode.",
|
|
353
|
+
"skills.no_description": "Без описания",
|
|
354
|
+
"skills.button.execute": "✅ Выполнить",
|
|
355
|
+
"skills.button.cancel": "❌ Отмена",
|
|
356
|
+
"skills.confirm": "Подтвердите запуск скилла {skill}. Чтобы запустить его с аргументами, отправьте аргументы следующим сообщением.",
|
|
357
|
+
"skills.inactive_callback": "Это меню скиллов уже неактивно",
|
|
358
|
+
"skills.cancelled_callback": "Отменено",
|
|
359
|
+
"skills.execute_callback": "Использую скилл...",
|
|
360
|
+
"skills.executing_prefix": "⚡ Использую скилл:",
|
|
361
|
+
"skills.arguments_empty": "⚠️ Аргументы не могут быть пустыми. Отправьте текст или нажмите Выполнить.",
|
|
362
|
+
"skills.select_page": "Выберите скилл OpenCode (страница {page}):",
|
|
363
|
+
"skills.button.prev_page": "⬅️ Назад",
|
|
364
|
+
"skills.button.next_page": "Вперёд ➡️",
|
|
365
|
+
"skills.page_empty_callback": "На этой странице нет скиллов",
|
|
366
|
+
"skills.page_load_error_callback": "Не удалось загрузить эту страницу. Пожалуйста, попробуйте снова.",
|
|
326
367
|
"cmd.description.rename": "Переименовать текущую сессию",
|
|
327
368
|
"legacy.models.fetch_error": "🔴 Не удалось получить список моделей. Проверьте статус сервера /status.",
|
|
328
369
|
"legacy.models.empty": "📋 Нет доступных моделей. Настройте провайдеры через OpenCode.",
|
|
@@ -336,6 +377,18 @@ export const ru = {
|
|
|
336
377
|
"stt.error": "🔴 Не удалось распознать аудио: {error}",
|
|
337
378
|
"stt.empty_result": "🎤 В аудиосообщении не обнаружена речь.",
|
|
338
379
|
"cmd.description.open": "Добавить проект через обзор папок",
|
|
380
|
+
"worktree.branch_detached": "detached HEAD",
|
|
381
|
+
"worktree.select_with_current": "Выберите worktree:",
|
|
382
|
+
"worktree.project_not_selected": "🏗 Проект не выбран.\n\nСначала выберите проект командой /projects.",
|
|
383
|
+
"worktree.not_git_repo": "🌿 Git worktree недоступны для текущего проекта. Сначала выберите git-репозиторий.",
|
|
384
|
+
"worktree.not_git_repo_callback": "Текущий проект не является git-репозиторием",
|
|
385
|
+
"worktree.empty": "📭 Для текущего репозитория не найдено ни одного worktree.",
|
|
386
|
+
"worktree.fetch_error": "🔴 Не удалось загрузить git worktree.",
|
|
387
|
+
"worktree.page_empty_callback": "На этой странице нет worktree",
|
|
388
|
+
"worktree.selection_missing_callback": "Выбранный worktree больше недоступен",
|
|
389
|
+
"worktree.already_selected_callback": "Этот worktree уже выбран",
|
|
390
|
+
"worktree.selected": "✅ Выбран worktree: {worktree}\n\n📋 Сессия была сброшена. Используйте /sessions или /new для продолжения.",
|
|
391
|
+
"worktree.select_error": "🔴 Не удалось выбрать worktree.",
|
|
339
392
|
"open.back": "⬆️ Наверх",
|
|
340
393
|
"open.roots": "📋 К списку корней",
|
|
341
394
|
"open.prev_page": "⬅️ Назад",
|
package/dist/i18n/zh.js
CHANGED
|
@@ -5,9 +5,11 @@ export const zh = {
|
|
|
5
5
|
"cmd.description.sessions": "列出会话",
|
|
6
6
|
"cmd.description.tts": "切换语音回复",
|
|
7
7
|
"cmd.description.projects": "列出项目",
|
|
8
|
+
"cmd.description.worktree": "切换 git worktree",
|
|
8
9
|
"cmd.description.task": "创建定时任务",
|
|
9
10
|
"cmd.description.tasklist": "查看定时任务",
|
|
10
11
|
"cmd.description.commands": "自定义命令",
|
|
12
|
+
"cmd.description.skills": "技能目录",
|
|
11
13
|
"cmd.description.opencode_start": "启动 OpenCode 服务器",
|
|
12
14
|
"cmd.description.opencode_stop": "停止 OpenCode 服务器",
|
|
13
15
|
"cmd.description.help": "帮助",
|
|
@@ -33,7 +35,7 @@ export const zh = {
|
|
|
33
35
|
"inline.cancelled_callback": "已取消",
|
|
34
36
|
"common.unknown": "未知",
|
|
35
37
|
"common.unknown_error": "未知错误",
|
|
36
|
-
"start.welcome": "👋 欢迎使用 OpenCode Telegram Bot!\n\n可用命令:\n/projects — 选择项目\n/sessions — 会话列表\n/new — 新建会话\n/task — 定时任务\n/tasklist — 定时任务列表\n/status — 状态\n/help — 帮助\n\n请使用底部按钮选择 Agent、模型和变体。",
|
|
38
|
+
"start.welcome": "👋 欢迎使用 OpenCode Telegram Bot!\n\n可用命令:\n/projects — 选择项目\n/sessions — 会话列表\n/new — 新建会话\n/commands — 自定义命令\n/skills — 技能目录\n/task — 定时任务\n/tasklist — 定时任务列表\n/status — 状态\n/help — 帮助\n\n请使用底部按钮选择 Agent、模型和变体。",
|
|
37
39
|
"help.keyboard_hint": "💡 Agent、模型、变体和上下文操作请使用底部键盘按钮。",
|
|
38
40
|
"help.text": "📖 **帮助**\n\n/status - 查看服务器状态\n/sessions - 会话列表\n/new - 创建新会话\n/help - 帮助",
|
|
39
41
|
"bot.thinking": "💭 思考中...",
|
|
@@ -46,6 +48,7 @@ export const zh = {
|
|
|
46
48
|
"bot.prompt_send_error": "向 OpenCode 发送请求失败。",
|
|
47
49
|
"bot.session_error": "🔴 OpenCode 返回错误:{message}",
|
|
48
50
|
"bot.session_retry": "🔁 {message}\n\n提供方在重复重试时持续返回同一错误。使用 /abort 可停止。",
|
|
51
|
+
"bot.external_user_input": "外部用户输入",
|
|
49
52
|
"bot.unknown_command": "⚠️ 未知命令:{command}。使用 /help 查看可用命令。",
|
|
50
53
|
"bot.photo_downloading": "⏳ 正在下载照片...",
|
|
51
54
|
"bot.photo_too_large": "⚠️ 照片过大(最大 {maxSizeMb}MB)",
|
|
@@ -73,6 +76,7 @@ export const zh = {
|
|
|
73
76
|
"status.tts.off": "关闭",
|
|
74
77
|
"status.agent_not_set": "未设置",
|
|
75
78
|
"status.project_selected": "项目:{project}",
|
|
79
|
+
"status.worktree_selected": "Worktree:{worktree}",
|
|
76
80
|
"status.project_not_selected": "项目:未选择",
|
|
77
81
|
"status.project_hint": "使用 /projects 选择项目",
|
|
78
82
|
"status.session_selected": "当前会话:{title}",
|
|
@@ -110,6 +114,17 @@ export const zh = {
|
|
|
110
114
|
"sessions.preview.title": "最近消息:",
|
|
111
115
|
"sessions.preview.you": "你:",
|
|
112
116
|
"sessions.preview.agent": "代理:",
|
|
117
|
+
"attach.project_not_selected": "🏗 未选择项目。\n\n请先使用 /projects 选择一个项目。",
|
|
118
|
+
"attach.session_not_selected": "💬 未选择会话。\n\n请先使用 /sessions 选择一个会话。",
|
|
119
|
+
"attach.session_project_mismatch": "⚠️ 选中的会话与当前项目不匹配。请通过 /sessions 重新选择。",
|
|
120
|
+
"attach.connected": "✅ 已连接到会话:{title}",
|
|
121
|
+
"attach.already_connected": "ℹ️ 已经连接到会话:{title}",
|
|
122
|
+
"attach.status.idle_message": "状态:idle。正在等待新事件。",
|
|
123
|
+
"attach.status.busy_message": "状态:busy。新的提示词暂时被阻止。",
|
|
124
|
+
"attach.restored_question": "已恢复该会话的待处理问题。",
|
|
125
|
+
"attach.restored_permissions": "已恢复待处理权限请求:{count}。",
|
|
126
|
+
"attach.disconnect_hint": "如需断开连接,只需切换到其他会话或项目。",
|
|
127
|
+
"attach.error": "🔴 无法连接到当前会话。",
|
|
113
128
|
"new.project_not_selected": "🏗 未选择项目。\n\n请先使用 /projects 选择一个项目。",
|
|
114
129
|
"new.created": "✅ 新会话已创建:{title}",
|
|
115
130
|
"new.create_error": "🔴 OpenCode 服务器不可用,或创建会话时发生错误。",
|
|
@@ -124,15 +139,20 @@ export const zh = {
|
|
|
124
139
|
"stop.error": "🔴 停止操作失败。\n\n事件流已停止,请再次尝试 /abort。",
|
|
125
140
|
"opencode_start.already_running_managed": "⚠️ OpenCode 服务器已在运行\n\nPID:{pid}\n运行时间:{seconds} 秒",
|
|
126
141
|
"opencode_start.already_running_external": "✅ OpenCode 服务器正作为外部进程运行\n\n版本:{version}\n\n该服务器不是由机器人启动,因此 /opencode-stop 无法停止它。",
|
|
142
|
+
"opencode_start.already_running": "✅ OpenCode 服务器已在运行\n\n版本:{version}",
|
|
143
|
+
"opencode_start.remote_configured": "⚠️ /opencode_start 仅适用于本地 OpenCode 服务器。",
|
|
127
144
|
"opencode_start.starting": "🔄 正在启动 OpenCode 服务器...",
|
|
128
145
|
"opencode_start.start_error": "🔴 启动 OpenCode 服务器失败\n\n错误:{error}\n\n请检查 OpenCode CLI 已安装且在 PATH 中可用:\nopencode --version\nnpm install -g @opencode-ai/cli",
|
|
129
146
|
"opencode_start.started_not_ready": "⚠️ OpenCode 服务器已启动,但未响应\n\nPID:{pid}\n\n服务器可能仍在启动中。几秒后试试 /status。",
|
|
130
147
|
"opencode_start.success": "✅ OpenCode 服务器启动成功\n\nPID:{pid}\n版本:{version}",
|
|
131
148
|
"opencode_start.error": "🔴 启动服务器时发生错误。\n\n请查看应用日志了解详情。",
|
|
132
149
|
"opencode_stop.external_running": "⚠️ OpenCode 服务器正作为外部进程运行\n\n该服务器不是通过 /opencode-start 启动的。\n请手动停止它,或使用 /status 检查状态。",
|
|
150
|
+
"opencode_stop.remote_configured": "⚠️ /opencode_stop 仅适用于本地 OpenCode 服务器。",
|
|
133
151
|
"opencode_stop.not_running": "⚠️ OpenCode 服务器未运行",
|
|
152
|
+
"opencode_stop.pid_not_found": "⚠️ OpenCode 服务器正在端口 {port} 上响应,但未找到可停止的本地进程。",
|
|
134
153
|
"opencode_stop.stopping": "🛑 正在停止 OpenCode 服务器...\n\nPID:{pid}",
|
|
135
154
|
"opencode_stop.stop_error": "🔴 停止 OpenCode 服务器失败\n\n错误:{error}",
|
|
155
|
+
"opencode_stop.still_running": "停止请求后服务器仍在响应。",
|
|
136
156
|
"opencode_stop.success": "✅ OpenCode 服务器已成功停止",
|
|
137
157
|
"opencode_stop.error": "🔴 停止服务器时发生错误。\n\n请查看应用日志了解详情。",
|
|
138
158
|
"agent.changed_callback": "Agent 已更改:{name}",
|
|
@@ -222,7 +242,11 @@ export const zh = {
|
|
|
222
242
|
"pinned.default_session_title": "新会话",
|
|
223
243
|
"pinned.unknown": "未知",
|
|
224
244
|
"pinned.line.project": "项目: {project}",
|
|
245
|
+
"pinned.line.worktree": "Worktree: {worktree}",
|
|
225
246
|
"pinned.line.model": "模型: {model}",
|
|
247
|
+
"pinned.line.attach": "Tracking: {status}",
|
|
248
|
+
"pinned.attach.status.idle": "active, idle",
|
|
249
|
+
"pinned.attach.status.busy": "active, busy",
|
|
226
250
|
"pinned.line.context": "上下文: {used} / {limit} ({percent}%)",
|
|
227
251
|
"pinned.line.cost": "费用: {cost}",
|
|
228
252
|
"subagent.header": "子代理 {agent}: {description}",
|
|
@@ -323,6 +347,23 @@ export const zh = {
|
|
|
323
347
|
"commands.button.next_page": "下一页 ➡️",
|
|
324
348
|
"commands.page_empty_callback": "这一页没有命令",
|
|
325
349
|
"commands.page_load_error_callback": "无法加载此页面。请重试。",
|
|
350
|
+
"skills.select": "请选择一个 OpenCode 技能:",
|
|
351
|
+
"skills.empty": "📭 当前项目没有可用的 OpenCode 技能。",
|
|
352
|
+
"skills.fetch_error": "🔴 加载 OpenCode 技能失败。",
|
|
353
|
+
"skills.no_description": "无描述",
|
|
354
|
+
"skills.button.execute": "✅ 执行",
|
|
355
|
+
"skills.button.cancel": "❌ 取消",
|
|
356
|
+
"skills.confirm": "请确认执行技能 {skill}。若需带参数执行,请发送一条包含参数的消息。",
|
|
357
|
+
"skills.inactive_callback": "该技能菜单已失效",
|
|
358
|
+
"skills.cancelled_callback": "已取消",
|
|
359
|
+
"skills.execute_callback": "正在使用技能...",
|
|
360
|
+
"skills.executing_prefix": "⚡ 使用技能:",
|
|
361
|
+
"skills.arguments_empty": "⚠️ 参数不能为空。请发送文本或点击执行。",
|
|
362
|
+
"skills.select_page": "请选择一个 OpenCode 技能(第 {page} 页):",
|
|
363
|
+
"skills.button.prev_page": "⬅️ 上一页",
|
|
364
|
+
"skills.button.next_page": "下一页 ➡️",
|
|
365
|
+
"skills.page_empty_callback": "这一页没有技能",
|
|
366
|
+
"skills.page_load_error_callback": "无法加载此页面。请重试。",
|
|
326
367
|
"cmd.description.rename": "重命名当前会话",
|
|
327
368
|
"legacy.models.fetch_error": "🔴 获取模型列表失败。请使用 /status 检查服务器状态。",
|
|
328
369
|
"legacy.models.empty": "📋 没有可用模型。请在 OpenCode 中配置 providers。",
|
|
@@ -336,6 +377,18 @@ export const zh = {
|
|
|
336
377
|
"stt.error": "🔴 识别音频失败:{error}",
|
|
337
378
|
"stt.empty_result": "🎤 音频消息中未检测到语音。",
|
|
338
379
|
"cmd.description.open": "通过浏览目录添加项目",
|
|
380
|
+
"worktree.branch_detached": "detached HEAD",
|
|
381
|
+
"worktree.select_with_current": "选择一个 worktree:",
|
|
382
|
+
"worktree.project_not_selected": "🏗 尚未选择项目。\n\n请先使用 /projects 选择一个项目。",
|
|
383
|
+
"worktree.not_git_repo": "🌿 当前项目不支持 git worktree。请先选择一个 git 仓库。",
|
|
384
|
+
"worktree.not_git_repo_callback": "当前项目不是 git 仓库",
|
|
385
|
+
"worktree.empty": "📭 当前仓库未找到任何 git worktree。",
|
|
386
|
+
"worktree.fetch_error": "🔴 加载 git worktree 失败。",
|
|
387
|
+
"worktree.page_empty_callback": "此页面没有 worktree",
|
|
388
|
+
"worktree.selection_missing_callback": "所选 worktree 已不可用",
|
|
389
|
+
"worktree.already_selected_callback": "该 worktree 已被选中",
|
|
390
|
+
"worktree.selected": "✅ 已选择 worktree:{worktree}\n\n📋 会话已重置。请使用 /sessions 或 /new 继续。",
|
|
391
|
+
"worktree.select_error": "🔴 选择 worktree 失败。",
|
|
339
392
|
"open.back": "⬆️ 上级",
|
|
340
393
|
"open.roots": "📋 返回根目录",
|
|
341
394
|
"open.prev_page": "⬅️ 上一页",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { interactionManager } from "./manager.js";
|
|
2
2
|
import { allowsBusyInteraction, isBusyAllowedCommand } from "./busy.js";
|
|
3
3
|
import { foregroundSessionState } from "../scheduled-task/foreground-state.js";
|
|
4
|
+
import { attachManager } from "../attach/manager.js";
|
|
4
5
|
function normalizeIncomingCommand(text) {
|
|
5
6
|
const trimmed = text.trim();
|
|
6
7
|
if (!trimmed.startsWith("/")) {
|
|
@@ -83,7 +84,7 @@ function isAllowedTaskCallback(ctx, state) {
|
|
|
83
84
|
export function resolveInteractionGuardDecision(ctx) {
|
|
84
85
|
const state = interactionManager.getSnapshot();
|
|
85
86
|
const { inputType, command } = classifyIncomingInput(ctx);
|
|
86
|
-
const isBusy = foregroundSessionState.isBusy();
|
|
87
|
+
const isBusy = foregroundSessionState.isBusy() || attachManager.isBusy();
|
|
87
88
|
if (state && interactionManager.isExpired()) {
|
|
88
89
|
interactionManager.clear("expired");
|
|
89
90
|
return createBlockDecision(inputType, state, "expired", command, isBusy);
|
package/dist/opencode/events.js
CHANGED
|
@@ -8,6 +8,7 @@ let eventCallback = null;
|
|
|
8
8
|
let isListening = false;
|
|
9
9
|
let activeDirectory = null;
|
|
10
10
|
let streamAbortController = null;
|
|
11
|
+
let listenerGeneration = 0;
|
|
11
12
|
function getReconnectDelayMs(attempt) {
|
|
12
13
|
const exponentialDelay = RECONNECT_BASE_DELAY_MS * Math.pow(2, Math.max(0, attempt - 1));
|
|
13
14
|
return Math.min(exponentialDelay, RECONNECT_MAX_DELAY_MS);
|
|
@@ -44,6 +45,7 @@ export async function subscribeToEvents(directory, callback) {
|
|
|
44
45
|
activeDirectory = null;
|
|
45
46
|
}
|
|
46
47
|
const controller = new AbortController();
|
|
48
|
+
const generation = ++listenerGeneration;
|
|
47
49
|
activeDirectory = directory;
|
|
48
50
|
eventCallback = callback;
|
|
49
51
|
isListening = true;
|
|
@@ -70,7 +72,16 @@ export async function subscribeToEvents(directory, callback) {
|
|
|
70
72
|
// Use setImmediate to avoid blocking the event loop
|
|
71
73
|
// and let grammY process incoming Telegram updates
|
|
72
74
|
const callbackSnapshot = eventCallback;
|
|
73
|
-
setImmediate(() =>
|
|
75
|
+
setImmediate(() => {
|
|
76
|
+
if (streamAbortController !== controller ||
|
|
77
|
+
controller.signal.aborted ||
|
|
78
|
+
!isListening ||
|
|
79
|
+
activeDirectory !== directory ||
|
|
80
|
+
listenerGeneration !== generation) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
callbackSnapshot(event);
|
|
84
|
+
});
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
eventStream = null;
|
|
@@ -130,6 +141,7 @@ export async function subscribeToEvents(directory, callback) {
|
|
|
130
141
|
}
|
|
131
142
|
}
|
|
132
143
|
export function stopEventListening() {
|
|
144
|
+
listenerGeneration++;
|
|
133
145
|
streamAbortController?.abort();
|
|
134
146
|
streamAbortController = null;
|
|
135
147
|
isListening = false;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
const DEFAULT_OPENCODE_PORT = 4096;
|
|
5
|
+
const PROCESS_EXIT_POLL_MS = 100;
|
|
6
|
+
function isLocalHostname(hostname) {
|
|
7
|
+
return ["localhost", "127.0.0.1", "::1", "0.0.0.0"].includes(hostname.toLowerCase());
|
|
8
|
+
}
|
|
9
|
+
export function resolveLocalOpencodeTarget(apiUrl) {
|
|
10
|
+
try {
|
|
11
|
+
const parsedUrl = new URL(apiUrl);
|
|
12
|
+
if (!isLocalHostname(parsedUrl.hostname)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const port = parsedUrl.port ? Number.parseInt(parsedUrl.port, 10) : DEFAULT_OPENCODE_PORT;
|
|
16
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
host: parsedUrl.hostname,
|
|
21
|
+
port,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function parsePid(value) {
|
|
29
|
+
const pid = Number.parseInt(value.trim(), 10);
|
|
30
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
31
|
+
}
|
|
32
|
+
function parseSocketPort(value) {
|
|
33
|
+
const trimmedValue = value.trim();
|
|
34
|
+
const match = trimmedValue.match(/:(\d+)$/);
|
|
35
|
+
if (!match) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const port = Number.parseInt(match[1], 10);
|
|
39
|
+
return Number.isInteger(port) && port > 0 ? port : null;
|
|
40
|
+
}
|
|
41
|
+
export function findWindowsListeningPidInNetstat(stdout, port) {
|
|
42
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
43
|
+
const trimmedLine = line.trim();
|
|
44
|
+
if (!trimmedLine) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const columns = trimmedLine.split(/\s+/);
|
|
48
|
+
const localAddress = columns[1] ?? "";
|
|
49
|
+
const localPort = parseSocketPort(localAddress);
|
|
50
|
+
if (localPort !== port) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const pid = parsePid(columns[columns.length - 1] ?? "");
|
|
54
|
+
if (pid !== null) {
|
|
55
|
+
return pid;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
export function findUnixListeningPidInSs(stdout, port) {
|
|
61
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
62
|
+
const trimmedLine = line.trim();
|
|
63
|
+
if (!trimmedLine) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const columns = trimmedLine.split(/\s+/);
|
|
67
|
+
const localAddress = columns[3] ?? "";
|
|
68
|
+
const localPort = parseSocketPort(localAddress);
|
|
69
|
+
if (localPort !== port) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const pidMatch = trimmedLine.match(/pid=(\d+)/);
|
|
73
|
+
const pid = pidMatch ? parsePid(pidMatch[1]) : null;
|
|
74
|
+
if (pid !== null) {
|
|
75
|
+
return pid;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
async function findWindowsServerPid(port) {
|
|
81
|
+
try {
|
|
82
|
+
const { stdout } = await execAsync("netstat -ano | findstr LISTENING");
|
|
83
|
+
return findWindowsListeningPidInNetstat(stdout, port);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function parseUnixPidList(stdout) {
|
|
90
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
91
|
+
const pid = parsePid(line);
|
|
92
|
+
if (pid !== null) {
|
|
93
|
+
return pid;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
async function findUnixServerPid(port) {
|
|
99
|
+
try {
|
|
100
|
+
const { stdout } = await execAsync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`);
|
|
101
|
+
const pid = parseUnixPidList(stdout);
|
|
102
|
+
if (pid !== null) {
|
|
103
|
+
return pid;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Fall back to ss when lsof is unavailable.
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const { stdout } = await execAsync("ss -ltnp");
|
|
111
|
+
return findUnixListeningPidInSs(stdout, port);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function findServerPid(port) {
|
|
118
|
+
return process.platform === "win32" ? findWindowsServerPid(port) : findUnixServerPid(port);
|
|
119
|
+
}
|
|
120
|
+
function isProcessAlive(pid) {
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, 0);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
130
|
+
const startTime = Date.now();
|
|
131
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
132
|
+
if (!isProcessAlive(pid)) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, PROCESS_EXIT_POLL_MS));
|
|
136
|
+
}
|
|
137
|
+
return !isProcessAlive(pid);
|
|
138
|
+
}
|
|
139
|
+
async function killWindowsProcess(pid, timeoutMs) {
|
|
140
|
+
try {
|
|
141
|
+
await execAsync(`taskkill /PID ${pid} /T`);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Continue with forced stop if the process is still alive.
|
|
145
|
+
}
|
|
146
|
+
if (await waitForProcessExit(pid, timeoutMs)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await execAsync(`taskkill /F /PID ${pid} /T`);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return !isProcessAlive(pid);
|
|
154
|
+
}
|
|
155
|
+
return waitForProcessExit(pid, timeoutMs);
|
|
156
|
+
}
|
|
157
|
+
async function killUnixProcess(pid, timeoutMs) {
|
|
158
|
+
try {
|
|
159
|
+
process.kill(pid, "SIGTERM");
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return !isProcessAlive(pid);
|
|
163
|
+
}
|
|
164
|
+
if (await waitForProcessExit(pid, timeoutMs)) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
process.kill(pid, "SIGKILL");
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return !isProcessAlive(pid);
|
|
172
|
+
}
|
|
173
|
+
return waitForProcessExit(pid, timeoutMs);
|
|
174
|
+
}
|
|
175
|
+
export async function killServerProcess(pid, timeoutMs = 5000) {
|
|
176
|
+
if (!isProcessAlive(pid)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return process.platform === "win32"
|
|
180
|
+
? killWindowsProcess(pid, timeoutMs)
|
|
181
|
+
: killUnixProcess(pid, timeoutMs);
|
|
182
|
+
}
|