@wu529778790/open-im 0.2.7 → 0.2.9

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/README.md CHANGED
@@ -19,24 +19,69 @@ open-im 是一个轻量级的 IM 桥接工具,让你通过 Telegram 就能使
19
19
  ### 方式一:npx(无需安装)
20
20
 
21
21
  ```bash
22
- npx @wu529778790/open-im start
22
+ npx @wu529778790/open-im run
23
23
  ```
24
24
 
25
25
  ### 方式二:全局安装(推荐常用用户)
26
26
 
27
27
  ```bash
28
28
  npm i @wu529778790/open-im -g
29
- open-im start
29
+ open-im run
30
30
  ```
31
31
 
32
32
  首次运行会引导你完成配置,30 秒即可搞定。
33
33
 
34
+ 如果配置引导未出现,可以手动运行:
35
+
36
+ ```bash
37
+ npx @wu529778790/open-im init
38
+ ```
39
+
40
+ ## ⚙️ 配置说明
41
+
42
+ 配置文件位置:`~/.open-im/config.json`
43
+
44
+ 配置文件示例:
45
+
46
+ ```json
47
+ {
48
+ "telegramBotToken": "你的Bot Token(从 @BotFather 获取)",
49
+ "allowedUserIds": ["你的Telegram用户ID"],
50
+ "claudeWorkDir": "/path/to/your/work/dir",
51
+ "claudeSkipPermissions": true,
52
+ "aiCommand": "claude"
53
+ }
54
+ ```
55
+
56
+ 获取 Telegram Bot Token:
57
+ 1. 在 Telegram 中搜索 @BotFather
58
+ 2. 发送 `/newbot` 创建新机器人
59
+ 3. 按提示设置机器人名称
60
+ 4. BotFather 会返回 Token,格式如:`123456789:ABCdefGHIjklMNOpqrsTUVwxyz`
61
+
62
+ 获取 Telegram 用户 ID(可选):
63
+ 1. 在 Telegram 中搜索 @userinfobot
64
+ 2. 发送任意消息
65
+ 3. 机器人会返回你的用户 ID
66
+ 4. 如不设置,则所有人都可以使用你的机器人
67
+
68
+ **或者通过环境变量配置:**
69
+
70
+ ```bash
71
+ export TELEGRAM_BOT_TOKEN="你的Bot Token"
72
+ export ALLOWED_USER_IDS="用户ID1,用户ID2"
73
+ open-im run
74
+ ```
75
+
34
76
  ## 📖 常用命令
35
77
 
36
78
  | 命令 | 说明 |
37
79
  |------|------|
38
- | `open-im start` | 启动服务(后台运行) |
80
+ | `open-im` / `open-im run` | 前台运行(首次使用会引导配置) |
81
+ | `open-im init` | 初始化配置(首次使用或重新配置) |
82
+ | `open-im start` | 后台启动服务 |
39
83
  | `open-im stop` | 停止服务 |
84
+ | `open-im restart` | 重启服务 |
40
85
 
41
86
  ### Telegram 机器人命令
42
87
 
@@ -59,7 +104,7 @@ open-im start
59
104
 
60
105
  ```bash
61
106
  # npx(无需安装)
62
- npx @wu529778790/open-im start
107
+ npx @wu529778790/open-im run
63
108
 
64
109
  # npm 全局安装
65
110
  npm i @wu529778790/open-im -g
@@ -74,3 +119,59 @@ pnpm i @wu529778790/open-im -g
74
119
  ## 📝 License
75
120
 
76
121
  [MIT](LICENSE)
122
+
123
+ ## 🔧 故障排除
124
+
125
+ ### Q: 首次运行没有配置引导?
126
+
127
+ 如果配置引导没有出现,尝试以下方法:
128
+
129
+ 1. **手动运行配置命令:**
130
+ ```bash
131
+ npx @wu529778790/open-im init
132
+ ```
133
+
134
+ 2. **检查是否已有配置文件:**
135
+ ```bash
136
+ cat ~/.open-im/config.json
137
+ ```
138
+
139
+ 3. **手动创建配置文件:**
140
+ ```bash
141
+ mkdir -p ~/.open-im
142
+ cat > ~/.open-im/config.json << 'EOF'
143
+ {
144
+ "telegramBotToken": "你的Bot Token",
145
+ "allowedUserIds": ["你的Telegram用户ID"],
146
+ "claudeWorkDir": "$(pwd)",
147
+ "claudeSkipPermissions": true,
148
+ "aiCommand": "claude"
149
+ }
150
+ EOF
151
+ ```
152
+
153
+ ### Q: 启动后服务立即退出?
154
+
155
+ 可能是配置文件无效,检查配置:
156
+
157
+ ```bash
158
+ # 查看日志
159
+ tail -f ~/.open-im/logs/*.log
160
+
161
+ # 重新配置
162
+ rm ~/.open-im/config.json
163
+ npx @wu529778790/open-im run
164
+ ```
165
+
166
+ ### Q: 如何获取 Telegram Bot Token?
167
+
168
+ 1. 在 Telegram 中搜索 @BotFather
169
+ 2. 发送 `/newbot` 创建新机器人
170
+ 3. 按提示设置机器人名称
171
+ 4. BotFather 会返回 Token,格式如:`123456789:ABCdefGHIjklMNOpqrsTUVwxyz`
172
+
173
+ ### Q: 如何获取 Telegram 用户 ID?
174
+
175
+ 1. 在 Telegram 中搜索 @userinfobot
176
+ 2. 点击"START"或发送任意消息
177
+ 3. 机器人会返回你的用户 ID(数字)
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { main } from './index.js';
2
+ import { main, needsSetup, runInteractiveSetup } from './index.js';
3
+ import { loadConfig } from './config.js';
3
4
  import { spawn, execFileSync } from 'node:child_process';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { dirname, join } from 'node:path';
@@ -128,14 +129,28 @@ async function stopService() {
128
129
  await removePidFile();
129
130
  }
130
131
  }
131
- const args = process.argv.slice(2);
132
- if (args[0] === 'stop') {
133
- stopService().catch((err) => {
134
- console.error('停止服务时出错:', err);
132
+ // 启动服务(后台)
133
+ async function startService() {
134
+ // 首先检查是否需要配置
135
+ if (needsSetup()) {
136
+ console.log('\n━━━ open-im 首次配置 ━━━\n');
137
+ console.log('检测到未配置,需要先完成配置才能启动服务\n');
138
+ const saved = await runInteractiveSetup();
139
+ if (!saved) {
140
+ console.log('配置未完成,取消启动。');
141
+ process.exit(1);
142
+ }
143
+ console.log('');
144
+ }
145
+ // 验证配置是否有效(避免有配置文件但缺少必要字段的情况)
146
+ try {
147
+ loadConfig();
148
+ }
149
+ catch (err) {
150
+ console.error('配置无效或缺少必要字段:', err instanceof Error ? err.message : err);
151
+ console.log('\n请运行以下命令重新配置:\n npx @wu529778790/open-im\n');
135
152
  process.exit(1);
136
- });
137
- }
138
- else if (args[0] === 'start') {
153
+ }
139
154
  // 获取当前工作目录
140
155
  const currentDir = process.cwd();
141
156
  // 更新配置中的工作目录
@@ -155,8 +170,49 @@ else if (args[0] === 'start') {
155
170
  child.unref();
156
171
  console.log(`服务已在后台启动 (PID: ${child.pid})`);
157
172
  }
173
+ const args = process.argv.slice(2);
174
+ if (args[0] === 'init') {
175
+ // 手动触发配置
176
+ console.log('\n━━━ open-im 配置向导 ━━━\n');
177
+ const saved = await runInteractiveSetup();
178
+ if (!saved) {
179
+ console.log('配置未完成。');
180
+ process.exit(1);
181
+ }
182
+ console.log('\n✅ 配置完成!现在可以运行以下命令启动服务:\n open-im start\n');
183
+ }
184
+ else if (args[0] === 'stop') {
185
+ stopService().catch((err) => {
186
+ console.error('停止服务时出错:', err);
187
+ process.exit(1);
188
+ });
189
+ }
190
+ else if (args[0] === 'restart') {
191
+ console.log('正在重启服务...\n');
192
+ await stopService().catch((err) => {
193
+ console.error('停止服务时出错:', err);
194
+ });
195
+ // 等待进程完全退出
196
+ await new Promise(resolve => setTimeout(resolve, 1000));
197
+ console.log('\n正在重新启动服务...\n');
198
+ await startService();
199
+ }
200
+ else if (args[0] === 'start') {
201
+ await startService();
202
+ }
203
+ else if (args[0] === 'run' || args.length === 0) {
204
+ // 前台运行(默认命令)
205
+ console.log('\n🚀 正在前台启动 open-im 服务...\n');
206
+ console.log('💡 提示:按 Ctrl+C 可随时停止服务\n');
207
+ main().catch((err) => {
208
+ console.error(err);
209
+ process.exit(1);
210
+ });
211
+ }
158
212
  else {
159
- // 默认启动(兼容直接运行 open-im)
213
+ // 兼容旧版本,无参数时也运行
214
+ console.log('\n🚀 正在前台启动 open-im 服务...\n');
215
+ console.log('💡 提示:按 Ctrl+C 可随时停止服务\n');
160
216
  main().catch((err) => {
161
217
  console.error(err);
162
218
  process.exit(1);
@@ -1,8 +1,8 @@
1
1
  import type { Config } from '../config.js';
2
2
  import type { SessionManager } from '../session/session-manager.js';
3
3
  import type { RequestQueue } from '../queue/request-queue.js';
4
- import type { ThreadContext, CostRecord } from '../shared/types.js';
5
- export type { ThreadContext, CostRecord };
4
+ import type { ThreadContext } from '../shared/types.js';
5
+ export type { ThreadContext };
6
6
  export interface MessageSender {
7
7
  sendTextReply(chatId: string, text: string, threadCtx?: ThreadContext): Promise<void>;
8
8
  }
@@ -11,7 +11,6 @@ export interface CommandHandlerDeps {
11
11
  sessionManager: SessionManager;
12
12
  requestQueue: RequestQueue;
13
13
  sender: MessageSender;
14
- userCosts: Map<string, CostRecord>;
15
14
  getRunningTasksSize: () => number;
16
15
  }
17
16
  export type ClaudeRequestHandler = (userId: string, chatId: string, prompt: string, workDir: string, convId?: string, threadCtx?: ThreadContext, replyToMessageId?: string) => Promise<void>;
@@ -39,19 +39,24 @@ export class CommandHandler {
39
39
  '📋 可用命令:',
40
40
  '',
41
41
  '/help - 显示帮助',
42
- '/new - 开始新会话',
42
+ '/new - 开始新会话(AI 上下文重置)',
43
43
  '/status - 显示状态',
44
44
  '/cd <路径> - 切换工作目录',
45
45
  '/pwd - 当前工作目录',
46
46
  '/allow (/y) - 允许权限请求',
47
47
  '/deny (/n) - 拒绝权限请求',
48
+ '',
49
+ '💡 提示:清除聊天历史请点击 Telegram 右上角 ⋮ → 清除历史',
48
50
  ].join('\n');
49
51
  await this.deps.sender.sendTextReply(chatId, help);
50
52
  return true;
51
53
  }
52
54
  async handleNew(chatId, userId) {
53
55
  const ok = this.deps.sessionManager.newSession(userId);
54
- await this.deps.sender.sendTextReply(chatId, ok ? '✅ 已开始新会话。' : '当前没有活动会话。');
56
+ await this.deps.sender.sendTextReply(chatId, ok
57
+ ? '✅ AI 会话已重置,下一条消息将使用全新上下文。\n\n' +
58
+ '💡 提示:如需清除本对话的历史消息,请点击 Telegram 聊天右上角 ⋮ → 清除历史'
59
+ : '当前没有活动会话。');
55
60
  return true;
56
61
  }
57
62
  async handlePwd(chatId, userId) {
@@ -64,7 +69,6 @@ export class CommandHandler {
64
69
  const workDir = this.deps.sessionManager.getWorkDir(userId);
65
70
  const convId = this.deps.sessionManager.getConvId(userId);
66
71
  const sessionId = this.deps.sessionManager.getSessionIdForConv(userId, convId);
67
- const record = this.deps.userCosts.get(userId);
68
72
  const lines = [
69
73
  '📊 状态:',
70
74
  '',
@@ -72,7 +76,6 @@ export class CommandHandler {
72
76
  `版本: ${version}`,
73
77
  `工作目录: ${workDir}`,
74
78
  `会话: ${sessionId ?? '无'}`,
75
- `费用: $${record?.totalCost.toFixed(4) ?? '0.0000'}`,
76
79
  ];
77
80
  await this.deps.sender.sendTextReply(chatId, lines.join('\n'));
78
81
  return true;
@@ -84,7 +87,9 @@ export class CommandHandler {
84
87
  }
85
88
  try {
86
89
  const resolved = await this.deps.sessionManager.setWorkDir(userId, dir);
87
- await this.deps.sender.sendTextReply(chatId, `工作目录已切换到: ${resolved}\n会话已重置。`);
90
+ await this.deps.sender.sendTextReply(chatId, `📁 工作目录已切换到: ${resolved}\n\n` +
91
+ `🔄 AI 会话已重置,下一条消息将使用全新上下文。\n` +
92
+ `💡 提示:如需清除本对话的历史消息,请点击 Telegram 聊天右上角 ⋮ → 清除历史`);
88
93
  }
89
94
  catch (err) {
90
95
  await this.deps.sender.sendTextReply(chatId, err instanceof Error ? err.message : String(err));
package/dist/index.d.ts CHANGED
@@ -1 +1,4 @@
1
+ import { needsSetup } from './config.js';
2
+ import { runInteractiveSetup } from './setup.js';
3
+ export { needsSetup, runInteractiveSetup };
1
4
  export declare function main(): Promise<void>;
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { loadConfig, needsSetup } from './config.js';
2
2
  import { runInteractiveSetup } from './setup.js';
3
+ // 导出供 cli.ts 使用
4
+ export { needsSetup, runInteractiveSetup };
3
5
  import { initTelegram, stopTelegram } from './telegram/client.js';
4
6
  import { setupTelegramHandlers } from './telegram/event-handler.js';
5
7
  import { sendTextReply } from './telegram/message-sender.js';
@@ -72,17 +74,23 @@ export async function main() {
72
74
  ].join('\n');
73
75
  await sendLifecycleNotification('telegram', startupMsg).catch(() => { });
74
76
  const startedAt = Date.now();
77
+ // 防止重复发送关闭通知
78
+ let shutdownNotificationSent = false;
75
79
  const shutdown = async () => {
76
80
  log.info('Shutting down...');
77
81
  const uptimeSec = Math.floor((Date.now() - startedAt) / 1000);
78
82
  const m = Math.floor(uptimeSec / 60);
79
- try {
80
- await sendLifecycleNotification('telegram', `🔴 open-im 服务正在关闭...\n运行时长: ${m}分钟`);
81
- // 等待消息发送完成
82
- await new Promise(resolve => setTimeout(resolve, 500));
83
- }
84
- catch (err) {
85
- log.debug('Failed to send shutdown notification:', err);
83
+ // 只发送一次通知
84
+ if (!shutdownNotificationSent) {
85
+ shutdownNotificationSent = true;
86
+ try {
87
+ await sendLifecycleNotification('telegram', `🔴 open-im 服务正在关闭...\n运行时长: ${m}分钟`);
88
+ // 等待消息发送完成
89
+ await new Promise(resolve => setTimeout(resolve, 500));
90
+ }
91
+ catch (err) {
92
+ log.debug('Failed to send shutdown notification:', err);
93
+ }
86
94
  }
87
95
  // 清理停止标记文件
88
96
  await clearStopSignal();
@@ -68,12 +68,17 @@ export class SessionManager {
68
68
  const realPath = await this.resolveAndValidate(currentDir, workDir);
69
69
  const s = this.sessions.get(userId);
70
70
  if (s) {
71
+ const oldConvId = s.activeConvId;
71
72
  if (s.activeConvId && s.sessionId) {
72
73
  this.convSessionMap.set(`${userId}:${s.activeConvId}`, s.sessionId);
73
74
  }
74
75
  s.workDir = realPath;
75
76
  s.sessionId = undefined;
76
77
  s.activeConvId = randomBytes(4).toString('hex');
78
+ // 清除旧的 convSessionMap 中的映射
79
+ if (oldConvId) {
80
+ this.convSessionMap.delete(`${userId}:${oldConvId}`);
81
+ }
77
82
  }
78
83
  else {
79
84
  this.sessions.set(userId, {
@@ -82,19 +87,26 @@ export class SessionManager {
82
87
  });
83
88
  }
84
89
  this.flushSync();
85
- log.info(`WorkDir changed for user ${userId}: ${realPath}`);
90
+ log.info(`WorkDir changed for user ${userId}: ${realPath}, oldConvId=${s?.activeConvId}`);
86
91
  return realPath;
87
92
  }
88
93
  newSession(userId) {
89
94
  const s = this.sessions.get(userId);
90
95
  if (s) {
96
+ const oldSessionId = s.sessionId;
97
+ const oldConvId = s.activeConvId;
91
98
  if (s.activeConvId && s.sessionId) {
92
99
  this.convSessionMap.set(`${userId}:${s.activeConvId}`, s.sessionId);
93
100
  }
94
101
  s.sessionId = undefined;
95
102
  s.activeConvId = randomBytes(4).toString('hex');
96
103
  s.totalTurns = 0;
104
+ // 清除旧的 convSessionMap 中的映射,防止恢复旧的 sessionId
105
+ if (oldConvId) {
106
+ this.convSessionMap.delete(`${userId}:${oldConvId}`);
107
+ }
97
108
  this.flushSync();
109
+ log.info(`New session for user ${userId}: oldConvId=${oldConvId}, oldSessionId=${oldSessionId}, newConvId=${s.activeConvId}, sessionId=undefined`);
98
110
  return true;
99
111
  }
100
112
  return false;
@@ -4,11 +4,9 @@
4
4
  import type { Config } from '../config.js';
5
5
  import type { SessionManager } from '../session/session-manager.js';
6
6
  import type { ToolAdapter } from '../adapters/tool-adapter.interface.js';
7
- import type { CostRecord } from './types.js';
8
7
  export interface TaskDeps {
9
8
  config: Config;
10
9
  sessionManager: SessionManager;
11
- userCosts: Map<string, CostRecord>;
12
10
  }
13
11
  export interface TaskContext {
14
12
  userId: string;
@@ -1,19 +1,13 @@
1
1
  /**
2
2
  * 共享 AI 任务执行层 - 支持多 ToolAdapter
3
3
  */
4
- import { formatToolStats, formatToolCallNotification, trackCost, getContextWarning, } from './utils.js';
4
+ import { formatToolStats, formatToolCallNotification, getContextWarning, } from './utils.js';
5
5
  import { createLogger } from '../logger.js';
6
6
  const log = createLogger('AITask');
7
7
  function buildCompletionNote(result, sessionManager, ctx) {
8
8
  const toolInfo = formatToolStats(result.toolStats, result.numTurns);
9
9
  const parts = [];
10
- if (result.cost > 0) {
11
- parts.push(`耗时 ${(result.durationMs / 1000).toFixed(1)}s`);
12
- parts.push(`费用 $${result.cost.toFixed(4)}`);
13
- }
14
- else {
15
- parts.push('完成');
16
- }
10
+ parts.push(`耗时 ${(result.durationMs / 1000).toFixed(1)}s`);
17
11
  if (toolInfo)
18
12
  parts.push(toolInfo);
19
13
  if (result.model)
@@ -27,7 +21,7 @@ function buildCompletionNote(result, sessionManager, ctx) {
27
21
  return parts.join(' | ');
28
22
  }
29
23
  export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
30
- const { config, sessionManager, userCosts } = deps;
24
+ const { config, sessionManager } = deps;
31
25
  return new Promise((resolve) => {
32
26
  let lastUpdateTime = 0;
33
27
  let pendingUpdate = null;
@@ -125,8 +119,6 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
125
119
  pendingUpdate = null;
126
120
  }
127
121
  const note = buildCompletionNote(result, sessionManager, ctx);
128
- log.info(`Task completed for user ${ctx.userId}: cost=$${result.cost.toFixed(4)}`);
129
- trackCost(userCosts, ctx.userId, result.cost, result.durationMs);
130
122
  const finalContent = result.accumulated || result.result || '(无输出)';
131
123
  try {
132
124
  await platformAdapter.sendComplete(finalContent, note, thinkingText || undefined);
@@ -2,8 +2,3 @@ export interface ThreadContext {
2
2
  rootMessageId: string;
3
3
  threadId: string;
4
4
  }
5
- export interface CostRecord {
6
- totalCost: number;
7
- totalDurationMs: number;
8
- requestCount: number;
9
- }
@@ -1,7 +1,5 @@
1
- import type { CostRecord } from './types.js';
2
1
  export declare function truncateText(text: string, maxLen: number): string;
3
2
  export declare function splitLongContent(text: string, maxLen: number): string[];
4
3
  export declare function formatToolStats(toolStats: Record<string, number>, numTurns: number): string;
5
4
  export declare function formatToolCallNotification(toolName: string, toolInput?: Record<string, unknown>): string;
6
- export declare function trackCost(userCosts: Map<string, CostRecord>, userId: string, cost: number, durationMs: number): void;
7
5
  export declare function getContextWarning(totalTurns: number): string | null;
@@ -56,13 +56,6 @@ export function formatToolCallNotification(toolName, toolInput) {
56
56
  detail = ` → ${toolInput.file_path}`;
57
57
  return `${emoji} ${toolName}${detail}`;
58
58
  }
59
- export function trackCost(userCosts, userId, cost, durationMs) {
60
- const r = userCosts.get(userId) ?? { totalCost: 0, totalDurationMs: 0, requestCount: 0 };
61
- r.totalCost += cost;
62
- r.totalDurationMs += durationMs;
63
- r.requestCount += 1;
64
- userCosts.set(userId, r);
65
- }
66
59
  export function getContextWarning(totalTurns) {
67
60
  if (totalTurns >= 12)
68
61
  return '⚠️ 上下文较长,建议 /new 开始新会话';
@@ -27,7 +27,6 @@ async function downloadTelegramPhoto(bot, fileId) {
27
27
  export function setupTelegramHandlers(bot, config, sessionManager) {
28
28
  const accessControl = new AccessControl(config.allowedUserIds);
29
29
  const requestQueue = new RequestQueue();
30
- const userCosts = new Map();
31
30
  const runningTasks = new Map();
32
31
  const stopTaskCleanup = startTaskCleanup(runningTasks);
33
32
  const dedup = new MessageDedup();
@@ -36,7 +35,6 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
36
35
  sessionManager,
37
36
  requestQueue,
38
37
  sender: { sendTextReply },
39
- userCosts,
40
38
  getRunningTasksSize: () => runningTasks.size,
41
39
  });
42
40
  registerPermissionSender('telegram', {});
@@ -59,7 +57,7 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
59
57
  }
60
58
  const stopTyping = startTypingLoop(chatId);
61
59
  const taskKey = `${userId}:${msgId}`;
62
- await runAITask({ config, sessionManager, userCosts }, { userId, chatId, workDir, sessionId, convId, platform: 'telegram', taskKey }, prompt, toolAdapter, {
60
+ await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'telegram', taskKey }, prompt, toolAdapter, {
63
61
  throttleMs: THROTTLE_MS,
64
62
  streamUpdate: (content, toolNote) => {
65
63
  const note = toolNote ? '输出中...\n' + toolNote : '输出中...';
@@ -45,8 +45,9 @@ export async function sendThinkingMessage(chatId, replyToMessageId, toolId = 'cl
45
45
  message_id: Number(replyToMessageId),
46
46
  };
47
47
  }
48
- const msg = await bot.telegram.sendMessage(Number(chatId), formatMessage('正在思考...', 'thinking', '请稍候', toolId), { ...extra, parse_mode: 'Markdown' });
49
- await bot.telegram.editMessageText(Number(chatId), msg.message_id, undefined, formatMessage('正在思考...', 'thinking', '请稍候', toolId), { reply_markup: buildStopKeyboard(msg.message_id), parse_mode: 'Markdown' });
48
+ // 初始消息使用纯文本,避免 Markdown 解析问题
49
+ const msg = await bot.telegram.sendMessage(Number(chatId), formatMessage('正在思考...', 'thinking', '请稍候', toolId), extra);
50
+ await bot.telegram.editMessageText(Number(chatId), msg.message_id, undefined, formatMessage('正在思考...', 'thinking', '请稍候', toolId), { reply_markup: buildStopKeyboard(msg.message_id) });
50
51
  return String(msg.message_id);
51
52
  }
52
53
  export async function updateMessage(chatId, messageId, content, status, note, toolId = 'claude') {
@@ -55,8 +56,11 @@ export async function updateMessage(chatId, messageId, content, status, note, to
55
56
  if (status === 'thinking' || status === 'streaming') {
56
57
  opts.reply_markup = buildStopKeyboard(Number(messageId));
57
58
  }
59
+ // 流式输出时使用纯文本,避免 Markdown 解析导致内容减少
60
+ // 只在完成时应用 Markdown 格式
61
+ const shouldParseMarkdown = status === 'done' || status === 'error';
58
62
  try {
59
- await bot.telegram.editMessageText(Number(chatId), Number(messageId), undefined, formatMessage(content, status, note, toolId), { ...opts, parse_mode: 'Markdown' });
63
+ await bot.telegram.editMessageText(Number(chatId), Number(messageId), undefined, formatMessage(content, status, note, toolId), { ...opts, parse_mode: shouldParseMarkdown ? 'Markdown' : undefined });
60
64
  }
61
65
  catch (err) {
62
66
  if (err && typeof err === 'object' && 'message' in err && String(err.message).includes('not modified')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",