@wu529778790/open-im 1.10.9-beta.4 → 1.10.9-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 +21 -0
- package/README.zh-CN.md +21 -0
- package/dist/adapters/claude-sdk-adapter.d.ts +12 -0
- package/dist/adapters/claude-sdk-adapter.js +121 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,27 @@ After `start`, the CLI prints the dashboard URL (default **`http://127.0.0.1:392
|
|
|
66
66
|
|
|
67
67
|
Session state is stored in **`~/.open-im/data/sessions.json`** (per user, not IM chat logs).
|
|
68
68
|
|
|
69
|
+
## Session continuity
|
|
70
|
+
|
|
71
|
+
open-im and Claude Code CLI share the same session storage. When you work in the same directory, you can seamlessly switch between phone and computer.
|
|
72
|
+
|
|
73
|
+
**Phone → Computer:** open-im automatically resumes the latest CLI session in the same directory — no configuration needed.
|
|
74
|
+
|
|
75
|
+
**Computer → Phone:** use `claude --continue` (or `claude -c`) to pick up the conversation that was continued on open-im.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
# On computer
|
|
79
|
+
cd /my-project && claude # work as usual, then Ctrl+C
|
|
80
|
+
|
|
81
|
+
# On phone (via IM)
|
|
82
|
+
"help me fix the login bug" # open-im auto-resumes the same session
|
|
83
|
+
|
|
84
|
+
# Back on computer
|
|
85
|
+
claude -c # continues the phone conversation
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> **Note:** only one side can be active at a time. Exit the CLI before sending messages from the phone, and vice versa.
|
|
89
|
+
|
|
69
90
|
## Configuration
|
|
70
91
|
|
|
71
92
|
### Per-platform AI
|
package/README.zh-CN.md
CHANGED
|
@@ -66,6 +66,27 @@ npx @wu529778790/open-im start
|
|
|
66
66
|
|
|
67
67
|
会话状态保存在 **`~/.open-im/data/sessions.json`**(按用户,与 IM 聊天记录无关)。
|
|
68
68
|
|
|
69
|
+
## 会话接力
|
|
70
|
+
|
|
71
|
+
open-im 和 Claude Code CLI 共享同一份 session 存储。在同一个目录下,手机和电脑可以无缝切换。
|
|
72
|
+
|
|
73
|
+
**手机接电脑:** open-im 自动恢复同目录下最新的 CLI session,无需配置。
|
|
74
|
+
|
|
75
|
+
**电脑接手机:** 使用 `claude --continue`(或 `claude -c`)接上 open-im 端的对话。
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# 电脑端
|
|
79
|
+
cd /my-project && claude # 正常工作,退出时 Ctrl+C
|
|
80
|
+
|
|
81
|
+
# 手机端(IM 消息)
|
|
82
|
+
"帮我修复登录 bug" # open-im 自动接续同一个 session
|
|
83
|
+
|
|
84
|
+
# 回到电脑端
|
|
85
|
+
claude -c # 接上手机端的对话
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> **注意:** 同一时刻只能有一端活跃。从手机发消息前先退出 CLI,反之亦然。
|
|
89
|
+
|
|
69
90
|
## 配置说明
|
|
70
91
|
|
|
71
92
|
### 按平台指定 AI
|
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
* 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
|
|
10
10
|
*/
|
|
11
11
|
import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
|
|
12
|
+
interface ClaudeSessionMeta {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
mtime: number;
|
|
15
|
+
filePath: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 扫描 ~/.claude/projects/<encoded-path>/ 找到最新的 CLI session。
|
|
19
|
+
* 只返回 JSONL 文件(排除子目录如 subagents/),按修改时间倒序。
|
|
20
|
+
* @param homeOverride 测试用:覆盖 homedir() 的返回值
|
|
21
|
+
*/
|
|
22
|
+
export declare function findLatestClaudeSession(workDir: string, homeOverride?: string): ClaudeSessionMeta | undefined;
|
|
12
23
|
/**
|
|
13
24
|
* 由 initAdapters 根据配置调用。ttlMinutes≤0 时关闭空闲回收(仍受 MAX_ACTIVE_SESSIONS 限制)。
|
|
14
25
|
*/
|
|
@@ -26,3 +37,4 @@ export declare class ClaudeSDKAdapter implements ToolAdapter {
|
|
|
26
37
|
static removeSession(sessionId: string): void;
|
|
27
38
|
run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
|
|
28
39
|
}
|
|
40
|
+
export {};
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
|
|
10
10
|
*/
|
|
11
11
|
import { unstable_v2_createSession, unstable_v2_resumeSession } from '@anthropic-ai/claude-agent-sdk';
|
|
12
|
-
import { existsSync, readFileSync } from 'fs';
|
|
12
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
13
|
+
import { execSync } from 'child_process';
|
|
13
14
|
import { homedir } from 'os';
|
|
14
15
|
import { join } from 'path';
|
|
15
16
|
import { createLogger } from '../logger.js';
|
|
@@ -46,6 +47,94 @@ function loadUserPluginSettings() {
|
|
|
46
47
|
}
|
|
47
48
|
// Pre-load user plugin settings to cache Claude Code user preferences
|
|
48
49
|
loadUserPluginSettings();
|
|
50
|
+
// ── 扫描 Claude Code CLI 的最新 session,支持手机/电脑无缝切换 ──
|
|
51
|
+
/**
|
|
52
|
+
* 将 workDir(如 /Users/mac/github/open-im)转换为 Claude Code 的项目路径编码
|
|
53
|
+
* ~/.claude/projects/-Users-mac-github-open-im/
|
|
54
|
+
*/
|
|
55
|
+
function workDirToProjectPath(workDir) {
|
|
56
|
+
// Claude Code 使用路径中的 / 替换为 -,去掉开头的 -
|
|
57
|
+
// /Users/mac/github/open-im → -Users-mac-github-open-im
|
|
58
|
+
return workDir.replace(/\//g, '-');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 检查 CLI 进程是否正在使用某个 session(通过 /proc 或 ps 检测)
|
|
62
|
+
*/
|
|
63
|
+
function isCliSessionActive(sessionId) {
|
|
64
|
+
try {
|
|
65
|
+
// macOS: 用 ps 搜索包含该 sessionId 的 claude 进程
|
|
66
|
+
const result = execSync(`ps -axo pid,command 2>/dev/null | grep -v grep | grep "claude" | grep "${sessionId}" || true`, { encoding: 'utf-8', timeout: 3000 });
|
|
67
|
+
return result.trim().length > 0;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 扫描 ~/.claude/projects/<encoded-path>/ 找到最新的 CLI session。
|
|
75
|
+
* 只返回 JSONL 文件(排除子目录如 subagents/),按修改时间倒序。
|
|
76
|
+
* @param homeOverride 测试用:覆盖 homedir() 的返回值
|
|
77
|
+
*/
|
|
78
|
+
export function findLatestClaudeSession(workDir, homeOverride) {
|
|
79
|
+
const projectDir = join(homeOverride ?? homedir(), '.claude', 'projects', workDirToProjectPath(workDir));
|
|
80
|
+
if (!existsSync(projectDir)) {
|
|
81
|
+
log.info(`No Claude Code project dir found: ${projectDir}`);
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const entries = readdirSync(projectDir, { withFileTypes: true });
|
|
86
|
+
const sessions = [];
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
// 只处理 .jsonl 文件,跳过子目录(如 subagents/)和 memory 目录
|
|
89
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
|
|
90
|
+
continue;
|
|
91
|
+
const sessionId = entry.name.replace('.jsonl', '');
|
|
92
|
+
// 校验 UUID 格式
|
|
93
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(sessionId))
|
|
94
|
+
continue;
|
|
95
|
+
const filePath = join(projectDir, entry.name);
|
|
96
|
+
try {
|
|
97
|
+
const stat = statSync(filePath);
|
|
98
|
+
// 跳过空文件
|
|
99
|
+
if (stat.size === 0)
|
|
100
|
+
continue;
|
|
101
|
+
sessions.push({ sessionId, mtime: stat.mtimeMs, filePath });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (sessions.length === 0) {
|
|
108
|
+
log.info(`No session files found in ${projectDir}`);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
// 按修改时间倒序,最新的在前
|
|
112
|
+
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
113
|
+
const latest = sessions[0];
|
|
114
|
+
log.info(`Found latest Claude Code session: ${latest.sessionId} (mtime: ${new Date(latest.mtime).toISOString()}, size: ${statSync(latest.filePath).size})`);
|
|
115
|
+
return latest;
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
log.warn(`Failed to scan Claude Code project dir: ${err}`);
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 从 JSONL 文件的第一行提取 sessionId,验证文件内容与文件名一致。
|
|
124
|
+
*/
|
|
125
|
+
function validateSessionFile(filePath, expectedSessionId) {
|
|
126
|
+
try {
|
|
127
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
128
|
+
const firstLine = content.split('\n')[0];
|
|
129
|
+
if (!firstLine)
|
|
130
|
+
return false;
|
|
131
|
+
const firstEntry = JSON.parse(firstLine);
|
|
132
|
+
return firstEntry.sessionId === expectedSessionId;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
49
138
|
// 存储所有活跃的 SDKSession 对象,key 为 sessionId
|
|
50
139
|
// 使用 Map 而不是 Set,因为我们需要通过 sessionId 获取 session
|
|
51
140
|
const activeSessions = new Map();
|
|
@@ -246,6 +335,37 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode, onS
|
|
|
246
335
|
// 恢复失败,创建新会话
|
|
247
336
|
}
|
|
248
337
|
}
|
|
338
|
+
// 没有指定 sessionId 时,尝试自动恢复 Claude Code CLI 的最新 session
|
|
339
|
+
// 实现手机/电脑无缝切换:同目录下默认共享同一个对话
|
|
340
|
+
if (!sessionId) {
|
|
341
|
+
const latest = findLatestClaudeSession(workDir);
|
|
342
|
+
if (latest) {
|
|
343
|
+
// 安全检查:如果 CLI 正在使用该 session,不能接管
|
|
344
|
+
if (isCliSessionActive(latest.sessionId)) {
|
|
345
|
+
log.info(`CLI is actively using session ${latest.sessionId}, skipping auto-resume`);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
// 验证文件内容一致性
|
|
349
|
+
if (validateSessionFile(latest.filePath, latest.sessionId)) {
|
|
350
|
+
try {
|
|
351
|
+
log.info(`Auto-resuming latest CLI session: ${latest.sessionId}`);
|
|
352
|
+
session = unstable_v2_resumeSession(latest.sessionId, sessionOptions);
|
|
353
|
+
activeSessions.set(latest.sessionId, session);
|
|
354
|
+
sessionWorkDirs.set(latest.sessionId, workDir);
|
|
355
|
+
sessionLastUsed.set(latest.sessionId, Date.now());
|
|
356
|
+
log.info(`Successfully auto-resumed CLI session: ${latest.sessionId}`);
|
|
357
|
+
return { session, sessionId: latest.sessionId };
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
log.warn(`Failed to auto-resume CLI session ${latest.sessionId}, creating new one: ${err}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
log.warn(`Session file validation failed for ${latest.sessionId}, skipping`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
249
369
|
// 创建新会话
|
|
250
370
|
session = unstable_v2_createSession(sessionOptions);
|
|
251
371
|
// 新会话的 sessionId 需要从第一个消息中获取
|