@wu529778790/open-im 1.5.2-beta.4 → 1.5.2-beta.5

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
@@ -8,8 +8,7 @@
8
8
  - 多 AI 工具:支持 Claude、Codex、Cursor
9
9
  - 流式输出:实时回传 AI 回复与工具执行进度
10
10
  - 会话隔离:每个用户独立维护本地会话,`/new` 可重置
11
- - 权限模式:支持 `ask`、`accept-edits`、`plan`、`yolo`
12
- - 常用命令:支持 `/help`、`/mode`、`/new`、`/cd`、`/pwd`、`/status`
11
+ - 常用命令:支持 `/help`、`/new`、`/cd`、`/pwd`、`/status`
13
12
 
14
13
  ## 环境要求
15
14
 
@@ -185,26 +184,16 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
185
184
  说明:钉钉当前采用“Stream Mode 收消息 + OpenAPI 发送消息”的混合模式。
186
185
 
187
186
  - 会话内普通文本回复默认走 `sessionWebhook`
188
- - 如果配置了 `cardTemplateId` / `DINGTALK_CARD_TEMPLATE_ID`,AI 回复会升级为单条 `ai_card` 流式更新(`prepare / update / finish`)
187
+ - 若配置了 `cardTemplateId`,会尝试 AI 助理 `prepare/update/finish` 流式卡片;失败则 fallback 为普通文本(自定义机器人/普通群场景下互动卡片 API 报 param.error,暂不支持单条流式更新)
189
188
  - 启动/关闭通知会发给最近一次已互动的钉钉会话;如果服务冷启动后还没有任何钉钉会话互动过,则没有可用目标可发
190
189
 
191
- 钉钉 AI 卡片模板需至少兼容以下字段,建议模板按这些 key 取值:
192
-
193
- - `title`
194
- - `content`
195
- - `note`
196
- - `toolName`
197
- - `status`
198
- - `flowStatus`
199
- - `displayText`
190
+ 钉钉 AI 卡片模板:已适配官方「搜索结果卡片」模板,使用变量 `lastMessage`、`content`、`resources`、`users`、`flowStatus`。若使用该模板,无需修改模板即可实现流式更新。
200
191
 
201
192
  ## IM 内命令
202
193
 
203
194
  | 命令 | 说明 |
204
195
  | ---- | ---- |
205
196
  | `/help` | 显示帮助 |
206
- | `/mode` | 飞书显示卡片,Telegram 显示按钮,其它平台(含钉钉)显示文本模式列表 |
207
- | `/mode <模式>` | 直接切换:`ask` / `accept-edits` / `plan` / `yolo` |
208
197
  | `/new` | 开始新会话 |
209
198
  | `/status` | 显示 AI 工具、版本、会话目录、会话 ID |
210
199
  | `/cd <路径>` | 切换会话目录 |
@@ -212,17 +201,6 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
212
201
  | `/allow` `/y` | 允许权限请求 |
213
202
  | `/deny` `/n` | 拒绝权限请求 |
214
203
 
215
- ### 权限模式
216
-
217
- 与 Claude Code 官方命名保持一致,参考 [permissions](https://code.claude.com/docs/en/permissions):
218
-
219
- | 模式 | Claude 名 | 说明 |
220
- | ---- | --------- | ---- |
221
- | `ask` | `default` | 首次使用工具时询问 |
222
- | `accept-edits` | `acceptEdits` | 自动允许编辑 |
223
- | `plan` | `plan` | 只读分析,不执行命令、不改文件 |
224
- | `yolo` | `bypassPermissions` | 跳过所有权限确认 |
225
-
226
204
  ## 故障排除
227
205
 
228
206
  **Telegram 无响应**:检查网络,必要时在 Telegram 平台配置中添加 `"proxy": "http://127.0.0.1:7890"` 或设置 `TELEGRAM_PROXY`。
@@ -233,7 +211,7 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
233
211
 
234
212
  **钉钉无法回复**:确认应用已启用机器人 Stream Mode,并检查 `DINGTALK_CLIENT_ID`、`DINGTALK_CLIENT_SECRET` 或 `platforms.dingtalk` 配置是否正确。
235
213
 
236
- **钉钉没有流式更新**:如果未配置 `DINGTALK_CARD_TEMPLATE_ID`(或 `platforms.dingtalk.cardTemplateId`),会自动退回普通文本回复;配置 AI 卡片模板后才会启用单条流式更新。
214
+ **钉钉没有流式更新**:prepare 失败时 fallback 为普通文本回复。自定义机器人/普通群场景下,AI 助理和互动卡片 API 均不可用,仅支持单条文本回复。
237
215
 
238
216
  **Cursor 报 `Authentication required`**:先执行 `agent login`,或在 `env` 中设置 `CURSOR_API_KEY`。
239
217
 
@@ -2,7 +2,4 @@ import type { Config } from '../config.js';
2
2
  import type { ToolAdapter } from './tool-adapter.interface.js';
3
3
  export declare function initAdapters(config: Config): void;
4
4
  export declare function getAdapter(aiCommand: string): ToolAdapter | undefined;
5
- /**
6
- * Cleanup all adapter resources.
7
- */
8
5
  export declare function cleanupAdapters(): void;
@@ -14,7 +14,7 @@ export function initAdapters(config) {
14
14
  console.log('🚀 使用标准 Claude 适配器');
15
15
  adapters.set('claude', new ClaudeAdapter(config.claudeCliPath, {
16
16
  useProcessPool: true,
17
- idleTimeoutMs: 2 * 60 * 1000, // 2 minutes
17
+ idleTimeoutMs: 2 * 60 * 1000,
18
18
  }));
19
19
  }
20
20
  }
@@ -30,11 +30,8 @@ export function initAdapters(config) {
30
30
  export function getAdapter(aiCommand) {
31
31
  return adapters.get(aiCommand);
32
32
  }
33
- /**
34
- * Cleanup all adapter resources.
35
- */
36
33
  export function cleanupAdapters() {
37
34
  ClaudeAdapter.destroy();
38
- ClaudeSDKAdapter.destroy(); // 清理 SDK 查询
35
+ ClaudeSDKAdapter.destroy();
39
36
  adapters.clear();
40
37
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 检查更新,若有新版本则自动执行全局更新
3
+ * @returns true 表示已更新并需要重启(调用方应退出),false 表示无需更新或更新失败
4
+ */
5
+ export declare function checkAndUpdate(): Promise<{
6
+ updated: boolean;
7
+ latest?: string;
8
+ }>;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 启动时检查并自动更新到最新版本
3
+ */
4
+ import { spawnSync } from "node:child_process";
5
+ import { createRequire } from "node:module";
6
+ const require = createRequire(import.meta.url);
7
+ const { version: CURRENT_VERSION } = require("../package.json");
8
+ const PKG_NAME = "@wu529778790/open-im";
9
+ const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}`;
10
+ /** 从 npm registry 获取最新版本 */
11
+ async function fetchLatestVersion() {
12
+ try {
13
+ const res = await fetch(`${REGISTRY_URL}?fields=dist-tags`, {
14
+ signal: AbortSignal.timeout(5000),
15
+ });
16
+ if (!res.ok)
17
+ return null;
18
+ const data = (await res.json());
19
+ return data["dist-tags"]?.latest ?? null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ /** 简单 semver 比较:若 a < b 返回 true */
26
+ function isNewerVersion(current, latest) {
27
+ const parse = (v) => {
28
+ const m = v.replace(/^v/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
29
+ return m ? [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)] : [0, 0, 0];
30
+ };
31
+ const [c1, c2, c3] = parse(current);
32
+ const [l1, l2, l3] = parse(latest);
33
+ if (l1 !== c1)
34
+ return l1 > c1;
35
+ if (l2 !== c2)
36
+ return l2 > c2;
37
+ return l3 > c3;
38
+ }
39
+ /** 执行全局更新 */
40
+ function runGlobalUpdate() {
41
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
42
+ const result = spawnSync(npm, ["install", "-g", `${PKG_NAME}@latest`], {
43
+ stdio: "inherit",
44
+ shell: process.platform === "win32",
45
+ });
46
+ return result.status === 0;
47
+ }
48
+ /**
49
+ * 检查更新,若有新版本则自动执行全局更新
50
+ * @returns true 表示已更新并需要重启(调用方应退出),false 表示无需更新或更新失败
51
+ */
52
+ export async function checkAndUpdate() {
53
+ const latest = await fetchLatestVersion();
54
+ if (!latest || !isNewerVersion(CURRENT_VERSION, latest)) {
55
+ return { updated: false };
56
+ }
57
+ console.log(`\n📦 检测到新版本 v${latest}(当前 v${CURRENT_VERSION}),正在更新...`);
58
+ const ok = runGlobalUpdate();
59
+ if (ok) {
60
+ console.log(`\n✅ 已更新到 v${latest},正在启动服务...\n`);
61
+ spawnSync("open-im", ["start"], {
62
+ stdio: "inherit",
63
+ shell: true,
64
+ windowsHide: false,
65
+ });
66
+ return { updated: true, latest };
67
+ }
68
+ console.log("\n⚠️ 自动更新失败,请手动执行: npm install -g @wu529778790/open-im@latest");
69
+ return { updated: false };
70
+ }
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { main, needsSetup, runInteractiveSetup } from "./index.js";
7
7
  import { loadConfig } from "./config.js";
8
8
  import { runPlatformSelectionPrompt } from "./setup.js";
9
+ import { checkAndUpdate } from "./check-update.js";
9
10
  import { APP_HOME, SHUTDOWN_PORT } from "./constants.js";
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
12
  const PID_FILE = join(APP_HOME, "open-im.pid");
@@ -91,7 +92,8 @@ async function validateOrSetup() {
91
92
  async function cmdStart() {
92
93
  const pid = getPid();
93
94
  if (pid && isRunning(pid)) {
94
- console.log(`open-im 已在后台运行 (pid=${pid})`);
95
+ console.log("\n🟢 open-im 已在后台运行");
96
+ console.log(` pid: ${pid}`);
95
97
  return;
96
98
  }
97
99
  else {
@@ -100,6 +102,11 @@ async function cmdStart() {
100
102
  if (!(await validateOrSetup())) {
101
103
  process.exit(1);
102
104
  }
105
+ // 检查并自动更新到最新版本
106
+ const { updated } = await checkAndUpdate();
107
+ if (updated) {
108
+ process.exit(0);
109
+ }
103
110
  // 有 TTY 时在父进程让用户选择要启用的平台,再启动子进程
104
111
  let config = loadConfig();
105
112
  if (process.stdin.isTTY) {
@@ -118,7 +125,8 @@ async function cmdStart() {
118
125
  });
119
126
  child.unref();
120
127
  writePid(child.pid);
121
- console.log(`open-im 已在后台启动 (pid=${child.pid})`);
128
+ console.log("\n🟢 open-im 已在后台启动");
129
+ console.log(` pid: ${child.pid}`);
122
130
  }
123
131
  async function cmdStop() {
124
132
  const pid = getPid();
@@ -162,7 +170,8 @@ async function cmdStop() {
162
170
  catch {
163
171
  /* ignore */
164
172
  }
165
- console.log(`open-im 已停止 (pid=${pid})`);
173
+ console.log("\n🔴 open-im 已停止");
174
+ console.log(` pid: ${pid}`);
166
175
  }
167
176
  async function cmdInit() {
168
177
  console.log("\n━━━ open-im 配置向导 ━━━\n");
@@ -1,7 +1,5 @@
1
1
  /**
2
- * Codex CLI Runner - 解析 codex exec --json 的 JSONL 输出
3
- * 参考: https://developers.openai.com/codex/cli/reference/
4
- * https://takopi.dev/reference/runners/codex/exec-json-cheatsheet/
2
+ * Codex CLI Runner - 解析 `codex exec --json` 的 JSONL 输出。
5
3
  */
6
4
  export interface CodexRunCallbacks {
7
5
  onText: (accumulated: string) => void;
@@ -1,7 +1,5 @@
1
1
  /**
2
- * Codex CLI Runner - 解析 codex exec --json 的 JSONL 输出
3
- * 参考: https://developers.openai.com/codex/cli/reference/
4
- * https://takopi.dev/reference/runners/codex/exec-json-cheatsheet/
2
+ * Codex CLI Runner - 解析 `codex exec --json` 的 JSONL 输出。
5
3
  */
6
4
  import { spawn } from 'node:child_process';
7
5
  import { execFileSync } from 'node:child_process';
@@ -51,7 +49,6 @@ function buildCodexArgs(_prompt, sessionId, workDir, options) {
51
49
  : ["exec", ...newSessionOptions, "-"];
52
50
  }
53
51
  function quoteForWindowsCmd(arg) {
54
- // 普通 flag / sessionId / 无空格路径不需要加引号,否则引号可能被原样传给子进程。
55
52
  if (/^[A-Za-z0-9_./:=+\\-]+$/.test(arg)) {
56
53
  return arg;
57
54
  }
@@ -62,7 +59,6 @@ function quoteForWindowsCmd(arg) {
62
59
  return `"${escaped}"`;
63
60
  }
64
61
  function formatWindowsCommandName(command) {
65
- // 裸命令名(如 codex)依赖 PATH 查找,不能再包双引号,否则 cmd 会按字面量查找。
66
62
  if (/^[A-Za-z0-9_.-]+$/.test(command)) {
67
63
  return command;
68
64
  }
@@ -118,7 +114,6 @@ function resolveWindowsCodexLaunch(cliPath, args) {
118
114
  }
119
115
  }
120
116
  export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options) {
121
- // codex exec --json 非交互模式
122
117
  const args = buildCodexArgs(prompt, sessionId, workDir, options);
123
118
  const env = {};
124
119
  for (const [k, v] of Object.entries(process.env)) {
@@ -138,13 +133,11 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
138
133
  env.all_proxy = options.proxy;
139
134
  }
140
135
  if (process.platform === 'win32') {
141
- // 强制子进程在 Windows 下使用 UTF-8,避免中文源码/命令输出乱码。
142
136
  env.LANG = env.LANG || 'C.UTF-8';
143
137
  env.LC_ALL = env.LC_ALL || 'C.UTF-8';
144
138
  }
145
139
  const argsForLog = args.join(' ');
146
140
  log.info(`Spawning Codex CLI: path=${cliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}, args=${argsForLog}`);
147
- // Windows: .cmd/.bat 或简单命令名(如 codex)需通过 cmd.exe 执行,否则 spawn 报 ENOENT
148
141
  const isWinCmd = process.platform === 'win32' &&
149
142
  (/\.(cmd|bat)$/i.test(cliPath) || cliPath === 'codex');
150
143
  const directWindowsLaunch = isWinCmd
@@ -171,13 +164,11 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
171
164
  env,
172
165
  windowsHide: process.platform === 'win32',
173
166
  });
174
- // 通过 stdin 传 prompt,避免 Windows 下命令行参数引用导致中文/路径/空格被拆分。
175
167
  child.stdin?.write(prompt);
176
168
  child.stdin?.end();
177
169
  let accumulated = '';
178
170
  let accumulatedThinking = '';
179
171
  let completed = false;
180
- let threadId = '';
181
172
  const toolStats = {};
182
173
  const startTime = Date.now();
183
174
  const MAX_TIMEOUT = 2_147_483_647;
@@ -226,7 +217,7 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
226
217
  const type = event.type;
227
218
  log.debug(`[Codex event] type=${type}`);
228
219
  if (type === 'thread.started') {
229
- threadId = event.thread_id ?? '';
220
+ const threadId = event.thread_id ?? '';
230
221
  if (threadId)
231
222
  callbacks.onSessionId?.(threadId);
232
223
  return;
@@ -302,7 +293,6 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
302
293
  completed = true;
303
294
  if (timeoutHandle)
304
295
  clearTimeout(timeoutHandle);
305
- const usage = event.usage;
306
296
  const durationMs = Date.now() - startTime;
307
297
  callbacks.onComplete({
308
298
  success: true,
@@ -1,6 +1,13 @@
1
1
  import { type DWClientDownStream } from 'dingtalk-stream';
2
2
  import type { Config } from '../config.js';
3
3
  import type { DingTalkActiveTarget } from '../shared/active-chats.js';
4
+ export interface DingTalkStreamingTarget {
5
+ chatId: string;
6
+ conversationType?: string;
7
+ senderStaffId?: string;
8
+ senderId?: string;
9
+ robotCode?: string;
10
+ }
4
11
  export declare function registerSessionWebhook(chatId: string, sessionWebhook: string): void;
5
12
  export declare function sendText(chatId: string, content: string): Promise<unknown>;
6
13
  export declare function sendMarkdown(chatId: string, title: string, text: string): Promise<unknown>;
@@ -8,6 +15,14 @@ export declare function ackMessage(messageId: string, result?: unknown): void;
8
15
  export declare function initDingTalk(cfg: Config, eventHandler: (data: DWClientDownStream) => Promise<void>): Promise<void>;
9
16
  export declare function stopDingTalk(): void;
10
17
  export declare function sendProactiveText(target: string | DingTalkActiveTarget, content: string): Promise<void>;
11
- export declare function prepareStreamingCard(chatId: string, templateId: string, cardData: Record<string, unknown>): Promise<string>;
18
+ export declare function prepareStreamingCard(target: string | DingTalkStreamingTarget, templateId: string, cardData: Record<string, unknown>): Promise<string>;
12
19
  export declare function updateStreamingCard(conversationToken: string, templateId: string, cardData: Record<string, unknown>): Promise<void>;
13
20
  export declare function finishStreamingCard(conversationToken: string): Promise<void>;
21
+ /** 创建并投放卡片(卡片平台 API,支持普通群流式更新) */
22
+ export declare function createAndDeliverCard(target: DingTalkStreamingTarget, templateId: string, outTrackId: string, cardData: Record<string, unknown>): Promise<void>;
23
+ /** 更新卡片实例(用于流式更新) */
24
+ export declare function updateCardInstance(outTrackId: string, cardData: Record<string, unknown>): Promise<void>;
25
+ /** 互动卡片普通版:发送(用于 prepare 失败时的 fallback 流式) */
26
+ export declare function sendRobotInteractiveCard(target: DingTalkStreamingTarget, cardBizId: string, cardData: Record<string, unknown>): Promise<void>;
27
+ /** 互动卡片普通版:更新(单条消息流式更新) */
28
+ export declare function updateRobotInteractiveCard(cardBizId: string, cardData: Record<string, unknown>): Promise<void>;