@wu529778790/open-im 0.2.8 → 0.2.10

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
@@ -81,6 +81,7 @@ open-im run
81
81
  | `open-im init` | 初始化配置(首次使用或重新配置) |
82
82
  | `open-im start` | 后台启动服务 |
83
83
  | `open-im stop` | 停止服务 |
84
+ | `open-im restart` | 重启服务 |
84
85
 
85
86
  ### Telegram 机器人命令
86
87
 
package/dist/cli.js CHANGED
@@ -129,24 +129,8 @@ async function stopService() {
129
129
  await removePidFile();
130
130
  }
131
131
  }
132
- const args = process.argv.slice(2);
133
- if (args[0] === 'init') {
134
- // 手动触发配置
135
- console.log('\n━━━ open-im 配置向导 ━━━\n');
136
- const saved = await runInteractiveSetup();
137
- if (!saved) {
138
- console.log('配置未完成。');
139
- process.exit(1);
140
- }
141
- console.log('\n✅ 配置完成!现在可以运行以下命令启动服务:\n open-im start\n');
142
- }
143
- else if (args[0] === 'stop') {
144
- stopService().catch((err) => {
145
- console.error('停止服务时出错:', err);
146
- process.exit(1);
147
- });
148
- }
149
- else if (args[0] === 'start') {
132
+ // 启动服务(后台)
133
+ async function startService() {
150
134
  // 首先检查是否需要配置
151
135
  if (needsSetup()) {
152
136
  console.log('\n━━━ open-im 首次配置 ━━━\n');
@@ -186,6 +170,36 @@ else if (args[0] === 'start') {
186
170
  child.unref();
187
171
  console.log(`服务已在后台启动 (PID: ${child.pid})`);
188
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
+ }
189
203
  else if (args[0] === 'run' || args.length === 0) {
190
204
  // 前台运行(默认命令)
191
205
  console.log('\n🚀 正在前台启动 open-im 服务...\n');
@@ -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>;
@@ -69,7 +69,6 @@ export class CommandHandler {
69
69
  const workDir = this.deps.sessionManager.getWorkDir(userId);
70
70
  const convId = this.deps.sessionManager.getConvId(userId);
71
71
  const sessionId = this.deps.sessionManager.getSessionIdForConv(userId, convId);
72
- const record = this.deps.userCosts.get(userId);
73
72
  const lines = [
74
73
  '📊 状态:',
75
74
  '',
@@ -77,7 +76,6 @@ export class CommandHandler {
77
76
  `版本: ${version}`,
78
77
  `工作目录: ${workDir}`,
79
78
  `会话: ${sessionId ?? '无'}`,
80
- `费用: $${record?.totalCost.toFixed(4) ?? '0.0000'}`,
81
79
  ];
82
80
  await this.deps.sender.sendTextReply(chatId, lines.join('\n'));
83
81
  return true;
package/dist/config.js CHANGED
@@ -52,7 +52,7 @@ export function loadConfig() {
52
52
  ? parseInt(process.env.CLAUDE_TIMEOUT_MS, 10) || 600000
53
53
  : file.claudeTimeoutMs ?? 600000;
54
54
  if (aiCommand === 'claude') {
55
- if (isAbsolute(claudeCliPath) || claudeCliPath.includes('/')) {
55
+ if (isAbsolute(claudeCliPath) || claudeCliPath.includes('/') || claudeCliPath.includes('\\')) {
56
56
  try {
57
57
  accessSync(claudeCliPath, constants.F_OK | constants.X_OK);
58
58
  }
@@ -61,11 +61,31 @@ export function loadConfig() {
61
61
  }
62
62
  }
63
63
  else {
64
+ // 检查命令是否存在(Windows 用 where,Unix 用 which)
65
+ const checkCommand = process.platform === 'win32' ? 'where' : 'which';
64
66
  try {
65
- execFileSync('which', [claudeCliPath], { stdio: 'pipe' });
67
+ execFileSync(checkCommand, [claudeCliPath], { stdio: 'pipe' });
66
68
  }
67
69
  catch {
68
- throw new Error(`Claude CLI 在 PATH 中未找到: ${claudeCliPath}`);
70
+ const installGuide = [
71
+ '',
72
+ '━━━ Claude CLI 未安装 ━━━',
73
+ '',
74
+ 'open-im 需要 Claude Code CLI 才能运行。',
75
+ '',
76
+ '安装方法:',
77
+ '',
78
+ ' npm install -g @anthropic-ai/claude-code',
79
+ '',
80
+ '或者:',
81
+ ' 1. 访问 https://claude.ai/download',
82
+ ' 2. 下载并安装 Claude Code',
83
+ '',
84
+ '安装后重新运行:',
85
+ ' open-im run',
86
+ '',
87
+ ].join('\n');
88
+ throw new Error(installGuide);
69
89
  }
70
90
  }
71
91
  }
@@ -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,17 +56,25 @@ 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
- }
59
+ // 使用提示池,每轮显示不同的技巧
60
+ const USAGE_TIPS = [
61
+ '💡 提示:用 `/new` 开始全新会话',
62
+ '💡 可以用 `/cd <路径>` 切换工作目录',
63
+ '💡 `/pwd` 查看当前目录',
64
+ '💡 用 `/status` 查看运行状态',
65
+ '💡 支持发送图片让 AI 分析',
66
+ '💡 支持多行代码输入',
67
+ '⚠️ 上下文较长,建议 /new 开始新会话',
68
+ ];
66
69
  export function getContextWarning(totalTurns) {
67
- if (totalTurns >= 12)
68
- return '⚠️ 上下文较长,建议 /new 开始新会话';
69
- if (totalTurns >= 8)
70
- return `💡 对话已 ${totalTurns} 轮,可用 /compact 压缩`;
71
- return null;
70
+ // 6 轮开始显示提示
71
+ if (totalTurns < 6)
72
+ return null;
73
+ // 13 轮后一直显示警告
74
+ if (totalTurns >= 13) {
75
+ return USAGE_TIPS[USAGE_TIPS.length - 1];
76
+ }
77
+ // 根据轮数循环显示提示
78
+ const tipIndex = (totalTurns - 6) % USAGE_TIPS.length;
79
+ return USAGE_TIPS[tipIndex];
72
80
  }
@@ -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.8",
3
+ "version": "0.2.10",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",