@goodandready/dsh-messenger-gateway 0.3.20 → 0.3.21

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/lib/index.js CHANGED
@@ -266,7 +266,7 @@ export function apply(ctx, config) {
266
266
  const started = turnStarts.get(String(session.id))
267
267
  turnStarts.delete(String(session.id))
268
268
  const dur = started ? Math.round((Date.now() - started) / 1000) : null
269
- const title = kind === 'task_done' ? 'Задача завершена' : 'Ошибка агента'
269
+ const title = kind === 'task_done' ? 'Task completed' : 'Agent error'
270
270
  const lines = [
271
271
  `【${title}】`,
272
272
  `session: ${session.id}`,
@@ -0,0 +1,114 @@
1
+ // Canonical English locale dictionary
2
+ export const en = {
3
+ // Command descriptions
4
+ 'cmd.help': 'Show commands help',
5
+ 'cmd.new': 'Start new session',
6
+ 'cmd.stop': 'Interrupt current turn',
7
+ 'cmd.model': 'Interactive model selector',
8
+ 'cmd.role': 'Switch agent persona or role',
9
+ 'cmd.rewind': 'Rewind last N turns',
10
+ 'cmd.fork': 'Fork session into new branch',
11
+ 'cmd.export': 'Export history to Markdown',
12
+ 'cmd.skills': 'List active tools & skills',
13
+ 'cmd.files': 'Workspace file explorer',
14
+ 'cmd.get': 'Download file from workspace',
15
+ 'cmd.remind': 'Set a timer or reminder',
16
+ 'cmd.status': 'Gateway and model status',
17
+ 'cmd.top': 'System resources (RAM, uptime)',
18
+ 'cmd.keyboard': 'Toggle quick action buttons',
19
+ 'cmd.voice': 'Voice reply mode',
20
+ 'cmd.tts': 'Toggle speech in this chat',
21
+ 'cmd.mute': 'Mute notifications in this chat',
22
+ 'cmd.unmute': 'Unmute notifications in this chat',
23
+ 'cmd.whoami': 'Show your messenger user ID',
24
+ 'cmd.pair': 'Approve pairing code',
25
+ 'cmd.sethome': 'Set primary notification home',
26
+ 'cmd.setalert': 'Set alert notification target',
27
+ 'cmd.bind': 'Bind preset or persona to forum topic',
28
+ 'cmd.preset': 'Show or switch agent preset',
29
+ 'cmd.cron': 'Schedule recurring autonomous report',
30
+
31
+ // Bot replies & statuses
32
+ 'msg.start': 'Gateway connected. Send messages to the agent. /help for commands.',
33
+ 'msg.steer_added': '↪️ Added to current turn',
34
+ 'msg.turn_stopped': 'Turn interrupted.',
35
+ 'msg.no_active_session': 'No active session found.',
36
+ 'msg.unknown_command': 'Unknown command {cmd}. /help',
37
+ 'msg.not_allowed': 'Access denied. Ask the owner to whitelist your ID.',
38
+ 'msg.pairing_requested': 'Access required.\nYour ID: {userId}\nPairing code: {code}\n\nThe owner must send to bot:\n/pair {code}',
39
+ 'msg.pairing_rate_limit': 'Pairing code already issued. Please wait or ask owner for /pair.',
40
+ 'msg.pairing_approved': '✅ User {userId} successfully approved!',
41
+ 'msg.pairing_invalid': 'Invalid or expired pairing code.',
42
+ 'msg.muted_on': 'Notifications in this chat: muted (/unmute)',
43
+ 'msg.muted_off': 'Notifications in this chat: enabled',
44
+ 'msg.no_response': '(no response)',
45
+ 'msg.agent_error': 'Agent error: {code}: {message}',
46
+ 'msg.exception': 'Internal error: {message}',
47
+
48
+ // Photo & media
49
+ 'photo.received_one': 'Photo received. Send a question — e.g. "what is in this photo?"',
50
+ 'photo.received_many': 'Received {count} attachments. Send your question.',
51
+ 'photo.hint_photo': '[Photo attached]',
52
+ 'photo.hint_photos': '[{count} photos attached]',
53
+ 'doc.hint': '[Document attached: {name}]',
54
+
55
+ // Interactive buttons & ask
56
+ 'ask.confirm_title': '⚠️ Confirmation Required\nTool: <code>{tool}</code>{reason}',
57
+ 'ask.allow_once': '✅ Allow Once',
58
+ 'ask.allow_session': '🛡️ Allow for Session',
59
+ 'ask.deny': '❌ Deny',
60
+ 'ask.chose': 'Chose: {choice}',
61
+ 'ask.done': 'Done',
62
+ 'ask.cancel': 'Cancel',
63
+ 'ask.canceled': 'Canceled',
64
+ 'ask.expired': 'Interaction expired. Please try again.',
65
+ 'ask.other_chat': 'Button belongs to another chat',
66
+
67
+ // Model selector
68
+ 'model.title': '🤖 <b>Choose Provider:</b>',
69
+ 'model.current': 'Current: <code>{current}</code>',
70
+ 'model.provider_models': '🤖 <b>Provider:</b> <code>{provider}</code>\nSelect model (page {page}/{total}):',
71
+ 'model.no_models': 'No models available for this provider',
72
+ 'model.switched': '✅ Model successfully switched to: <b>{provider}/{model}</b>',
73
+
74
+ // Personas & Presets
75
+ 'persona.title': '🎭 <b>Available Roles & Personas:</b>',
76
+ 'persona.usage': 'Switch role: <code>/role coder</code> (or /role reset)',
77
+ 'persona.reset': '🎭 Role reset to default (Default).',
78
+ 'persona.switched': '🎭 Role changed to: {icon} <b>{name}</b>\n{description}',
79
+ 'persona.unknown': 'Unknown role "{target}". List: /role list',
80
+ 'persona.bound_topic': '📌 Topic bound to {kind}: <b>{name}</b>',
81
+
82
+ // Builtin persona descriptions
83
+ 'persona.default.name': 'Default',
84
+ 'persona.default.desc': 'General assistant, concise and balanced',
85
+ 'persona.coder.name': 'Developer',
86
+ 'persona.coder.desc': 'Senior developer, code focus without fluff',
87
+ 'persona.writer.name': 'Editor',
88
+ 'persona.writer.desc': 'Copywriter, clean text and clear tone',
89
+ 'persona.analyst.name': 'Analyst',
90
+ 'persona.analyst.desc': 'Structured decomposition and data analysis',
91
+ 'persona.concise.name': 'Brief',
92
+ 'persona.concise.desc': 'Ultra-short replies, 1-2 sentences',
93
+
94
+ // File explorer & export
95
+ 'files.title': '📁 <b>Workspace:</b> <code>{path}</code>\nTotal files: {count}\n\n',
96
+ 'files.empty': '📁 Directory is empty: <code>{path}</code>',
97
+ 'files.download_hint': '\nDownload file: <code>/get &lt;path&gt;</code>',
98
+ 'files.not_found': 'File not found: {path}',
99
+ 'export.title': '📄 Dialog export ({count} messages):',
100
+ 'export.empty': 'Session is empty, nothing to export.',
101
+
102
+ // Reminders & Cron
103
+ 'remind.scheduled': '⏰ Reminder set for {time} (in {duration}):\n{text}',
104
+ 'remind.invalid': 'Invalid time format. Example: /remind 10m check deploy',
105
+ 'remind.prefix': '⏰ <b>[Reminder]</b>\n{text}',
106
+ 'cron.scheduled': '⏱️ Autonomous cron task scheduled: <code>{id}</code>\nSchedule: <code>{schedule}</code>\nPrompt: {prompt}',
107
+ 'cron.list_title': '⏱️ <b>Scheduled Cron Tasks:</b>\n',
108
+ 'cron.none': 'No active cron tasks.',
109
+ 'cron.cancelled': '✅ Cron task cancelled: <code>{id}</code>',
110
+ 'cron.prefix': '⏱️ <b>[Scheduled Report: {id}]</b>\n',
111
+
112
+ // Mirror & Forum
113
+ 'mirror.created': '🪞 <b>[DSH Session Mirror]</b>\nSession: <code>{sessionId}</code>\nTitle: <b>{title}</b>\nMessages in this topic are synchronized with DSH.',
114
+ }
@@ -0,0 +1,21 @@
1
+ import { en } from './en.js'
2
+ import { zh } from './zh.js'
3
+
4
+ export const locales = { en, zh }
5
+
6
+ export function getLocaleDictionary(locale = 'en') {
7
+ const norm = String(locale || '').toLowerCase()
8
+ if (norm.startsWith('zh')) return zh
9
+ return en
10
+ }
11
+
12
+ export function t(key, params = {}, locale = 'en') {
13
+ const dict = getLocaleDictionary(locale)
14
+ let template = dict[key] || en[key] || key
15
+ for (const [k, v] of Object.entries(params)) {
16
+ template = template.replaceAll(`{${k}}`, String(v))
17
+ }
18
+ return template
19
+ }
20
+
21
+ export { en, zh }
@@ -0,0 +1,114 @@
1
+ // Canonical Chinese (zh) locale dictionary
2
+ export const zh = {
3
+ // Command descriptions
4
+ 'cmd.help': '显示帮助信息',
5
+ 'cmd.new': '开始新会话',
6
+ 'cmd.stop': '中断当前回答',
7
+ 'cmd.model': '交互式模型选择器',
8
+ 'cmd.role': '切换智能体角色/人设',
9
+ 'cmd.rewind': '回退最近N轮对话',
10
+ 'cmd.fork': '将会话派生到新分支',
11
+ 'cmd.export': '导出历史记录为Markdown',
12
+ 'cmd.skills': '列出当前可用工具与技能',
13
+ 'cmd.files': '工作区文件浏览器',
14
+ 'cmd.get': '从工作区下载文件',
15
+ 'cmd.remind': '设置提醒或定时器',
16
+ 'cmd.status': '查看网关与模型状态',
17
+ 'cmd.top': '查看系统资源(内存、运行时间)',
18
+ 'cmd.keyboard': '开关快捷操作键盘',
19
+ 'cmd.voice': '设置语音回复模式',
20
+ 'cmd.tts': '在此聊天开关语音朗读',
21
+ 'cmd.mute': '在此聊天静音通知',
22
+ 'cmd.unmute': '在此聊天恢复通知',
23
+ 'cmd.whoami': '显示你的用户ID',
24
+ 'cmd.pair': '批准配对申请码',
25
+ 'cmd.sethome': '设置主通知频道',
26
+ 'cmd.setalert': '设置告警通知目标',
27
+ 'cmd.bind': '绑定预设或人设到论坛话题',
28
+ 'cmd.preset': '查看或切换智能体预设',
29
+ 'cmd.cron': '配置定时周期性任务报告',
30
+
31
+ // Bot replies & statuses
32
+ 'msg.start': '网关已连接。请向智能体发送消息。发送 /help 查看命令。',
33
+ 'msg.steer_added': '↪️ 已追加到当前回答',
34
+ 'msg.turn_stopped': '回答已中断。',
35
+ 'msg.no_active_session': '未找到活跃会话。',
36
+ 'msg.unknown_command': '未知命令 {cmd}。发送 /help',
37
+ 'msg.not_allowed': '访问受限。请联系管理员将您的ID加入白名单。',
38
+ 'msg.pairing_requested': '需要访问配对。\n您的ID: {userId}\n配对码: {code}\n\n管理员请向机器人发送:\n/pair {code}',
39
+ 'msg.pairing_rate_limit': '配对码已发出,请稍候或联系管理员。',
40
+ 'msg.pairing_approved': '✅ 用户 {userId} 已成功授权!',
41
+ 'msg.pairing_invalid': '配对码无效或已过期。',
42
+ 'msg.muted_on': '此聊天通知:已静音 (/unmute)',
43
+ 'msg.muted_off': '此聊天通知:已开启',
44
+ 'msg.no_response': '(无回复)',
45
+ 'msg.agent_error': '智能体错误: {code}: {message}',
46
+ 'msg.exception': '内部处理异常: {message}',
47
+
48
+ // Photo & media
49
+ 'photo.received_one': '已收到图片。请输入您的问题 — 例如:“图片中有什么?”',
50
+ 'photo.received_many': '已收到 {count} 个附件。请输入您的问题。',
51
+ 'photo.hint_photo': '[附加图片]',
52
+ 'photo.hint_photos': '[附加了 {count} 张图片]',
53
+ 'doc.hint': '[附加文件: {name}]',
54
+
55
+ // Interactive buttons & ask
56
+ 'ask.confirm_title': '⚠️ 需要授权确认\n工具: <code>{tool}</code>{reason}',
57
+ 'ask.allow_once': '✅ 允许单次',
58
+ 'ask.allow_session': '🛡️ 本会话始终允许',
59
+ 'ask.deny': '❌ 拒绝',
60
+ 'ask.chose': '已选择: {choice}',
61
+ 'ask.done': '完成',
62
+ 'ask.cancel': '取消',
63
+ 'ask.canceled': '已取消',
64
+ 'ask.expired': '交互已超时,请重新发起。',
65
+ 'ask.other_chat': '该按钮属于其他聊天',
66
+
67
+ // Model selector
68
+ 'model.title': '🤖 <b>选择服务商:</b>',
69
+ 'model.current': '当前: <code>{current}</code>',
70
+ 'model.provider_models': '🤖 <b>服务商:</b> <code>{provider}</code>\n请选择模型 (第 {page}/{total} 页):',
71
+ 'model.no_models': '该服务商暂无可用的模型',
72
+ 'model.switched': '✅ 模型已成功切换至: <b>{provider}/{model}</b>',
73
+
74
+ // Personas & Presets
75
+ 'persona.title': '🎭 <b>可用角色与人设:</b>',
76
+ 'persona.usage': '切换角色: <code>/role coder</code> (或 /role reset)',
77
+ 'persona.reset': '🎭 角色已重置为默认 (Default)。',
78
+ 'persona.switched': '🎭 角色已切换为: {icon} <b>{name}</b>\n{description}',
79
+ 'persona.unknown': '未知角色 "{target}"。列表请见: /role list',
80
+ 'persona.bound_topic': '📌 话题已绑定到{kind}: <b>{name}</b>',
81
+
82
+ // Builtin persona descriptions
83
+ 'persona.default.name': '默认',
84
+ 'persona.default.desc': '通用智能助手,回答平衡清晰',
85
+ 'persona.coder.name': '开发者',
86
+ 'persona.coder.desc': '资深全栈工程师,直奔代码核心',
87
+ 'persona.writer.name': '文案编辑',
88
+ 'persona.writer.desc': '专业编辑润色,文笔优美流畅',
89
+ 'persona.analyst.name': '分析师',
90
+ 'persona.analyst.desc': '结构化逻辑拆解,深度数据分析',
91
+ 'persona.concise.name': '极简',
92
+ 'persona.concise.desc': '极速短促回复,1-2句话答复',
93
+
94
+ // File explorer & export
95
+ 'files.title': '📁 <b>工作区路径:</b> <code>{path}</code>\n文件总计: {count}\n\n',
96
+ 'files.empty': '📁 目录为空: <code>{path}</code>',
97
+ 'files.download_hint': '\n下载文件命令: <code>/get &lt;path&gt;</code>',
98
+ 'files.not_found': '未找到文件: {path}',
99
+ 'export.title': '📄 对话导出 ({count} 条消息):',
100
+ 'export.empty': '会话为空,无消息可导出。',
101
+
102
+ // Reminders & Cron
103
+ 'remind.scheduled': '⏰ 提醒已设置在 {time} ({duration} 后):\n{text}',
104
+ 'remind.invalid': '时间格式错误。例如: /remind 10m 检查构建',
105
+ 'remind.prefix': '⏰ <b>[提醒]</b>\n{text}',
106
+ 'cron.scheduled': '⏱️ 定时任务已配置: <code>{id}</code>\n周期表达式: <code>{schedule}</code>\n指令: {prompt}',
107
+ 'cron.list_title': '⏱️ <b>当前定时任务列表:</b>\n',
108
+ 'cron.none': '暂无活动的定时任务。',
109
+ 'cron.cancelled': '✅ 定时任务已取消: <code>{id}</code>',
110
+ 'cron.prefix': '⏱️ <b>[定时报告: {id}]</b>\n',
111
+
112
+ // Mirror & Forum
113
+ 'mirror.created': '🪞 <b>[DSH 会话镜像]</b>\n会话ID: <code>{sessionId}</code>\n标题: <b>{title}</b>\n此话题消息与 DSH 双向同步。',
114
+ }
package/lib/models.js CHANGED
@@ -114,7 +114,7 @@ export function buildModelsKeyboard(providerId, models, currentModel, page = 0,
114
114
  if (navRow.length) rows.push(navRow)
115
115
 
116
116
  // Back row
117
- rows.push([{ text: '🔙 Назад к провайдерам', callback_data: 'mdl:back' }])
117
+ rows.push([{ text: '🔙 Back to providers', callback_data: 'mdl:back' }])
118
118
 
119
119
  return { inline_keyboard: rows, page: curPage, totalPages }
120
120
  }
package/lib/personas.js CHANGED
@@ -6,51 +6,58 @@ export const BUILTIN_PERSONAS = {
6
6
  id: 'default',
7
7
  name: 'Default',
8
8
  icon: '🤖',
9
- description: 'Стандартный универсальный ассистент',
9
+ description: 'General assistant, concise and balanced',
10
10
  instruction: '',
11
11
  },
12
12
  coder: {
13
13
  id: 'coder',
14
14
  name: 'Senior Developer',
15
15
  icon: '💻',
16
- description: 'Опытный разработчик: чистый код, архитектура, минимум лишних слов',
16
+ description: 'Senior developer: clean code, architecture, minimum fluff',
17
17
  instruction: 'You are an expert senior software engineer. Provide high-quality, production-ready code with best practices, proper error handling, and concise explanations.',
18
18
  },
19
19
  architect: {
20
20
  id: 'architect',
21
21
  name: 'System Architect',
22
22
  icon: '📐',
23
- description: 'Системный архитектор: проектирование, масштабируемость, компромиссы',
23
+ description: 'System architect: design, scalability, modularity and trade-offs',
24
24
  instruction: 'You are a principal system architect. Focus on high-level architecture, scalability, security, trade-offs, modular design, and clear diagrams.',
25
25
  },
26
26
  reviewer: {
27
27
  id: 'reviewer',
28
28
  name: 'Code Reviewer',
29
29
  icon: '🔍',
30
- description: 'Строгий код-ревьюер: поиск багов, безопасность, краевые случаи',
30
+ description: 'Rigorous code reviewer: edge cases, security and maintainability',
31
31
  instruction: 'You are a meticulous code reviewer. Analyze code for bugs, edge cases, security vulnerabilities, performance bottlenecks, and maintainability.',
32
32
  },
33
33
  writer: {
34
34
  id: 'writer',
35
35
  name: 'Tech Writer',
36
36
  icon: '📝',
37
- description: 'Технический писатель: понятные тексты, структура, документация',
37
+ description: 'Technical writer: clear text, structure and thorough docs',
38
38
  instruction: 'You are a professional technical writer and editor. Structure information clearly with clean formatting, intuitive language, and thorough documentation.',
39
39
  },
40
40
  translator: {
41
41
  id: 'translator',
42
42
  name: 'Translator',
43
43
  icon: '🌐',
44
- description: 'Переводчик: точный перевод с сохранением терминологии',
44
+ description: 'Translator: accurate translation with technical terminology preserved',
45
45
  instruction: 'You are an expert translator and localization specialist. Translate accurately while preserving technical context, nuance, and terminology.',
46
46
  },
47
47
  concise: {
48
48
  id: 'concise',
49
49
  name: 'Concise',
50
50
  icon: '⚡',
51
- description: 'Лаконичный режим: краткие и емкие ответы без воды',
51
+ description: 'Ultra-concise mode: direct, dense answers without filler',
52
52
  instruction: 'Be extremely concise. Answer directly in 1-3 sentences or short bullet points without unnecessary filler or pleasantries.',
53
53
  },
54
+ analyst: {
55
+ id: 'analyst',
56
+ name: 'Analyst',
57
+ icon: '📊',
58
+ description: 'Data & business analyst: decomposition, metrics and logic',
59
+ instruction: 'You are a senior data and systems analyst. Focus on structured problem breakdown, data interpretation, metrics, and methodical decision making.',
60
+ },
54
61
  }
55
62
 
56
63
  export function getPersona(id) {
@@ -64,9 +71,16 @@ export function listPersonas() {
64
71
  }
65
72
 
66
73
  export function createPersonaStore(filePath) {
67
- let cache = {}
74
+ let cache = { personas: {}, presets: {} }
68
75
  try {
69
- cache = JSON.parse(readFileSync(filePath, 'utf8'))
76
+ const raw = JSON.parse(readFileSync(filePath, 'utf8'))
77
+ if (raw && typeof raw === 'object') {
78
+ if (raw.personas || raw.presets) {
79
+ cache = { personas: raw.personas || {}, presets: raw.presets || {} }
80
+ } else {
81
+ cache = { personas: raw, presets: {} }
82
+ }
83
+ }
70
84
  } catch {}
71
85
 
72
86
  function save() {
@@ -76,18 +90,57 @@ export function createPersonaStore(filePath) {
76
90
  } catch {}
77
91
  }
78
92
 
93
+ function makeKey(chatId, threadId = 0) {
94
+ const tid = Number(threadId) || 0
95
+ return tid > 0 ? `${chatId}:${tid}` : String(chatId)
96
+ }
97
+
79
98
  return {
80
- get(chatId) {
99
+ get(chatId, threadId = 0) {
81
100
  if (chatId === undefined || chatId === null) return 'default'
82
- return cache[String(chatId)] || 'default'
101
+ const key = makeKey(chatId, threadId)
102
+ if (cache.personas[key]) return cache.personas[key]
103
+ // Fallback to chat-level persona if in thread
104
+ const baseKey = String(chatId)
105
+ return cache.personas[baseKey] || 'default'
83
106
  },
84
- set(chatId, personaId) {
107
+ set(chatId, personaId, threadId = 0) {
85
108
  if (chatId === undefined || chatId === null) return
109
+ const key = makeKey(chatId, threadId)
86
110
  const valid = getPersona(personaId)
87
111
  if (valid && valid.id !== 'default') {
88
- cache[String(chatId)] = valid.id
112
+ cache.personas[key] = valid.id
113
+ } else {
114
+ delete cache.personas[key]
115
+ }
116
+ save()
117
+ },
118
+ getPersonaForChat(chatId, threadId = 0) {
119
+ return this.get(chatId, threadId)
120
+ },
121
+ getPreset(chatId, threadId = 0) {
122
+ if (chatId === undefined || chatId === null) return null
123
+ const key = makeKey(chatId, threadId)
124
+ if (cache.presets[key]) return cache.presets[key]
125
+ const baseKey = String(chatId)
126
+ return cache.presets[baseKey] || null
127
+ },
128
+ setPreset(chatId, a, b) {
129
+ if (chatId === undefined || chatId === null) return
130
+ let presetName, threadId
131
+ if (typeof a === 'number' || (typeof a === 'string' && !isNaN(Number(a)) && b !== undefined && isNaN(Number(b)))) {
132
+ threadId = a
133
+ presetName = b
134
+ } else {
135
+ presetName = a
136
+ threadId = b || 0
137
+ }
138
+ const key = makeKey(chatId, threadId)
139
+ const clean = String(presetName || '').trim()
140
+ if (clean && clean !== 'default' && clean !== 'null') {
141
+ cache.presets[key] = clean
89
142
  } else {
90
- delete cache[String(chatId)]
143
+ delete cache.presets[key]
91
144
  }
92
145
  save()
93
146
  },
package/lib/photos.js CHANGED
@@ -26,6 +26,6 @@ export function photoOnlyHint(attachments, userText) {
26
26
  if (String(userText || '').trim()) return ''
27
27
  const photos = attachments.filter((a) => a.kind === 'photo')
28
28
  if (!photos.length) return ''
29
- if (photos.length === 1) return '[Пользователь отправил фото]'
30
- return `[Пользователь отправил ${photos.length} фото]`
29
+ if (photos.length === 1) return '[User attached a photo]'
30
+ return `[User attached ${photos.length} photos]`
31
31
  }