@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 +4 -26
- package/dist/adapters/registry.d.ts +0 -3
- package/dist/adapters/registry.js +2 -5
- package/dist/check-update.d.ts +8 -0
- package/dist/check-update.js +70 -0
- package/dist/cli.js +12 -3
- package/dist/codex/cli-runner.d.ts +1 -3
- package/dist/codex/cli-runner.js +2 -12
- package/dist/dingtalk/client.d.ts +16 -1
- package/dist/dingtalk/client.js +321 -7
- package/dist/dingtalk/event-handler.js +12 -4
- package/dist/dingtalk/message-sender.d.ts +3 -1
- package/dist/dingtalk/message-sender.js +157 -21
- package/dist/dingtalk/message-sender.test.js +29 -2
- package/dist/index.js +2 -6
- package/dist/session/session-manager.d.ts +3 -3
- package/dist/session/session-manager.js +23 -38
- package/dist/session/session-manager.test.d.ts +1 -0
- package/dist/session/session-manager.test.js +16 -0
- package/dist/shared/ai-task.d.ts +3 -3
- package/dist/shared/ai-task.js +2 -13
- package/dist/shared/utils.d.ts +2 -3
- package/dist/shared/utils.js +73 -18
- package/dist/shared/utils.test.d.ts +1 -0
- package/dist/shared/utils.test.js +43 -0
- package/dist/telegram/event-handler.js +9 -50
- package/dist/telegram/message-sender.d.ts +0 -3
- package/dist/telegram/message-sender.js +0 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
- 多 AI 工具:支持 Claude、Codex、Cursor
|
|
9
9
|
- 流式输出:实时回传 AI 回复与工具执行进度
|
|
10
10
|
- 会话隔离:每个用户独立维护本地会话,`/new` 可重置
|
|
11
|
-
-
|
|
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
|
-
-
|
|
187
|
+
- 若配置了 `cardTemplateId`,会尝试 AI 助理 `prepare/update/finish` 流式卡片;失败则 fallback 为普通文本(自定义机器人/普通群场景下互动卡片 API 报 param.error,暂不支持单条流式更新)
|
|
189
188
|
- 启动/关闭通知会发给最近一次已互动的钉钉会话;如果服务冷启动后还没有任何钉钉会话互动过,则没有可用目标可发
|
|
190
189
|
|
|
191
|
-
钉钉 AI
|
|
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
|
-
|
|
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,
|
|
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();
|
|
35
|
+
ClaudeSDKAdapter.destroy();
|
|
39
36
|
adapters.clear();
|
|
40
37
|
}
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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;
|
package/dist/codex/cli-runner.js
CHANGED
|
@@ -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(
|
|
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>;
|