chatccc 0.2.23 → 0.2.25

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/package.json CHANGED
@@ -1,53 +1,53 @@
1
- {
2
- "name": "chatccc",
3
- "version": "0.2.23",
4
- "description": "Feishu bot bridge for Claude Code",
5
- "license": "Apache-2.0",
6
- "type": "module",
7
- "main": "./src/index.ts",
8
- "bin": {
9
- "chatccc": "bin/chatccc.mjs"
10
- },
11
- "files": [
12
- "src/",
13
- "bin/",
14
- "images/img_readme_*.jpg",
15
- "images/img_readme_*.png",
16
- "images/avatars/status_*.png",
17
- "images/avatars/badges/",
18
- "package.json",
19
- "README.md",
20
- "config.sample.json"
21
- ],
22
- "scripts": {
23
- "dev": "tsx src/index.ts",
24
- "chatccc": "tsx src/index.ts",
25
- "start": "tsx src/index.ts",
26
- "demo:bot-test": "tsx demo/bot_test.ts",
27
- "demo:bot-test:local": "tsx demo/bot_test.ts --local",
28
- "demo:create-group": "tsx src/index.ts",
29
- "demo:create-group:local": "tsx src/index.ts --local",
30
- "demo:permission-check": "tsx demo/permission_check.ts",
31
- "demo:claude-hi": "tsx demo/claude_say_hi.ts",
32
- "demo:codex-hi": "tsx demo/codex_say_hi.ts",
33
- "test": "vitest run",
34
- "test:watch": "vitest"
35
- },
36
- "dependencies": {
37
- "@anthropic-ai/claude-agent-sdk": "0.2.133",
38
- "@larksuiteoapi/node-sdk": "^1.59.0",
39
- "nodemailer": "^8.0.7",
40
- "sharp": "^0.34.5",
41
- "tsx": "^4.0.0",
42
- "ws": "^8.18.0"
43
- },
44
- "devDependencies": {
45
- "@types/node": "^20.0.0",
46
- "@types/ws": "^8.18.1",
47
- "typescript": "^5.0.0",
48
- "vitest": "^4.1.5"
49
- },
50
- "engines": {
51
- "node": ">=20"
52
- }
53
- }
1
+ {
2
+ "name": "chatccc",
3
+ "version": "0.2.25",
4
+ "description": "Feishu bot bridge for Claude Code",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./src/index.ts",
8
+ "bin": {
9
+ "chatccc": "bin/chatccc.mjs"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "bin/",
14
+ "images/img_readme_*.jpg",
15
+ "images/img_readme_*.png",
16
+ "images/avatars/status_*.png",
17
+ "images/avatars/badges/",
18
+ "package.json",
19
+ "README.md",
20
+ "config.sample.json"
21
+ ],
22
+ "scripts": {
23
+ "dev": "tsx src/index.ts",
24
+ "chatccc": "tsx src/index.ts",
25
+ "start": "tsx src/index.ts",
26
+ "demo:bot-test": "tsx demo/bot_test.ts",
27
+ "demo:bot-test:local": "tsx demo/bot_test.ts --local",
28
+ "demo:create-group": "tsx src/index.ts",
29
+ "demo:create-group:local": "tsx src/index.ts --local",
30
+ "demo:permission-check": "tsx demo/permission_check.ts",
31
+ "demo:claude-hi": "tsx demo/claude_say_hi.ts",
32
+ "demo:codex-hi": "tsx demo/codex_say_hi.ts",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest"
35
+ },
36
+ "dependencies": {
37
+ "@anthropic-ai/claude-agent-sdk": "0.2.133",
38
+ "@larksuiteoapi/node-sdk": "^1.59.0",
39
+ "nodemailer": "^8.0.7",
40
+ "sharp": "^0.34.5",
41
+ "tsx": "^4.0.0",
42
+ "ws": "^8.18.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^20.0.0",
46
+ "@types/ws": "^8.18.1",
47
+ "typescript": "^5.0.0",
48
+ "vitest": "^4.1.5"
49
+ },
50
+ "engines": {
51
+ "node": ">=20"
52
+ }
53
+ }
@@ -8,10 +8,10 @@
8
8
  import { mkdir, readFile, writeFile } from "node:fs/promises";
9
9
  import { dirname, join } from "node:path";
10
10
 
11
- import { PROJECT_ROOT } from "../config.ts";
11
+ import { USER_DATA_DIR } from "../config.ts";
12
12
 
13
13
  export const CODEX_SESSION_META_FILE = join(
14
- PROJECT_ROOT,
14
+ USER_DATA_DIR,
15
15
  "state",
16
16
  "codex-session-meta.json",
17
17
  );
@@ -23,11 +23,11 @@
23
23
  import { mkdir, readFile, writeFile } from "node:fs/promises";
24
24
  import { dirname, join } from "node:path";
25
25
 
26
- import { PROJECT_ROOT } from "../config.ts";
26
+ import { USER_DATA_DIR } from "../config.ts";
27
27
 
28
28
  /** 持久化文件默认路径(生产)。测试可通过 createCursorSessionMetaStore(filePath) 注入。 */
29
29
  export const CURSOR_SESSION_META_FILE = join(
30
- PROJECT_ROOT,
30
+ USER_DATA_DIR,
31
31
  "state",
32
32
  "cursor-session-meta.json",
33
33
  );
package/src/config.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { existsSync, readFileSync, copyFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, copyFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "node:fs";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
6
+ import { appendFile, cp, mkdir, readFile, stat, writeFile } from "node:fs/promises";
7
7
 
8
8
  import { printServiceDidNotStart } from "./exit-banner.ts";
9
9
  import { appendStartupTrace, setupFileLogging } from "./shared.ts";
@@ -34,12 +34,14 @@ export type { ParsedGitTimeout } from "./config-utils.ts";
34
34
 
35
35
  export const __dirname = dirname(fileURLToPath(import.meta.url));
36
36
  export const PROJECT_ROOT = join(__dirname, "..");
37
- export const PID_FILE = join(PROJECT_ROOT, "state", "runtime.pid");
37
+ /** 用户持久化数据根目录(不随 npm 升级清空) */
38
+ export const USER_DATA_DIR = join(homedir(), ".chatccc");
39
+ export const PID_FILE = join(USER_DATA_DIR, "state", "runtime.pid");
38
40
 
39
- export const LOG_DIR = join(PROJECT_ROOT, "logs");
41
+ export const LOG_DIR = join(USER_DATA_DIR, "logs");
40
42
  export const fileLog = setupFileLogging(LOG_DIR, "index");
41
43
 
42
- export const CHAT_LOGS_DIR = join(PROJECT_ROOT, "state", "chat_logs");
44
+ export const CHAT_LOGS_DIR = join(USER_DATA_DIR, "state", "chat_logs");
43
45
 
44
46
  export async function appendChatLog(chatId: string, sender: string, text: string): Promise<void> {
45
47
  try {
@@ -104,9 +106,82 @@ export interface AppConfig {
104
106
  export type AgentTool = "claude" | "cursor" | "codex";
105
107
  export const AGENT_TOOLS: AgentTool[] = ["claude", "cursor", "codex"];
106
108
 
107
- const CONFIG_FILE = join(PROJECT_ROOT, "config.json");
109
+ const CONFIG_FILE = join(USER_DATA_DIR, "config.json");
108
110
  const CONFIG_SAMPLE_FILE = join(PROJECT_ROOT, "config.sample.json");
109
111
 
112
+ /**
113
+ * 将旧位置(PROJECT_ROOT)的持久化数据一次性迁移到 USER_DATA_DIR。
114
+ * 仅当 USER_DATA_DIR 下没有 config.json 且 PROJECT_ROOT 下有旧数据时才执行。
115
+ */
116
+ function migrateLegacyData(): void {
117
+ const oldConfig = join(PROJECT_ROOT, "config.json");
118
+ const oldState = join(PROJECT_ROOT, "state");
119
+ const oldLogs = join(PROJECT_ROOT, "logs");
120
+ const oldImagesDownloads = join(PROJECT_ROOT, "images", "downloads");
121
+
122
+ if (existsSync(CONFIG_FILE)) return; // 已迁移过或全新安装
123
+
124
+ let migrated = false;
125
+ try {
126
+ mkdirSync(USER_DATA_DIR, { recursive: true });
127
+
128
+ if (existsSync(oldConfig)) {
129
+ copyFileSync(oldConfig, CONFIG_FILE);
130
+ console.log(`[MIGRATE] config.json → ${CONFIG_FILE}`);
131
+ migrated = true;
132
+ }
133
+
134
+ if (existsSync(oldState)) {
135
+ const destState = join(USER_DATA_DIR, "state");
136
+ if (!existsSync(destState)) {
137
+ // 同步递归复制(cpSync 不可用,改用 copyFileSync 遍历)
138
+ copyDirSync(oldState, destState);
139
+ console.log(`[MIGRATE] state/ → ${destState}`);
140
+ migrated = true;
141
+ }
142
+ }
143
+
144
+ if (existsSync(oldLogs)) {
145
+ const destLogs = join(USER_DATA_DIR, "logs");
146
+ if (!existsSync(destLogs)) {
147
+ copyDirSync(oldLogs, destLogs);
148
+ console.log(`[MIGRATE] logs/ → ${destLogs}`);
149
+ migrated = true;
150
+ }
151
+ }
152
+
153
+ if (existsSync(oldImagesDownloads)) {
154
+ const destDownloads = join(USER_DATA_DIR, "images", "downloads");
155
+ if (!existsSync(destDownloads)) {
156
+ copyDirSync(oldImagesDownloads, destDownloads);
157
+ console.log(`[MIGRATE] images/downloads/ → ${destDownloads}`);
158
+ migrated = true;
159
+ }
160
+ }
161
+ } catch (err) {
162
+ console.error(`[MIGRATE] 迁移失败: ${(err as Error).message}`);
163
+ }
164
+
165
+ if (migrated) {
166
+ console.log("[MIGRATE] 旧数据已迁移到 ~/.chatccc/,原位置文件保留未删除。");
167
+ }
168
+ }
169
+
170
+ /** 递归同步复制目录(仅文件和子目录,不处理符号链接) */
171
+ function copyDirSync(src: string, dest: string): void {
172
+ mkdirSync(dest, { recursive: true });
173
+ for (const entry of readdirSync(src)) {
174
+ const srcPath = join(src, entry);
175
+ const destPath = join(dest, entry);
176
+ const s = statSync(srcPath);
177
+ if (s.isDirectory()) {
178
+ copyDirSync(srcPath, destPath);
179
+ } else if (s.isFile()) {
180
+ copyFileSync(srcPath, destPath);
181
+ }
182
+ }
183
+ }
184
+
110
185
  /**
111
186
  * 是否处于 vitest 测试环境。
112
187
  *
@@ -217,6 +292,10 @@ function loadConfig(): AppConfig {
217
292
  codex: { enabled: false, defaultAgent: false, path: "", model: "", effort: "" },
218
293
  };
219
294
 
295
+ if (!IS_TEST_ENV) {
296
+ migrateLegacyData();
297
+ }
298
+
220
299
  if (!existsSync(CONFIG_FILE)) {
221
300
  if (IS_TEST_ENV) {
222
301
  // 测试环境下绝不写文件,直接走默认值
@@ -225,6 +304,7 @@ function loadConfig(): AppConfig {
225
304
  if (existsSync(CONFIG_SAMPLE_FILE)) {
226
305
  console.log(`[CONFIG] config.json 不存在,基于 config.sample.json 创建...`);
227
306
  try {
307
+ mkdirSync(dirname(CONFIG_FILE), { recursive: true });
228
308
  copyFileSync(CONFIG_SAMPLE_FILE, CONFIG_FILE);
229
309
  } catch (err) {
230
310
  console.error(`[CONFIG] 无法从 config.sample.json 创建 config.json: ${(err as Error).message}`);
@@ -469,13 +549,13 @@ export function reloadConfigFromDisk(): void {
469
549
 
470
550
  // 新建会话的默认工作路径(/cd 命令设置,持久化到本地文件)
471
551
  // 该路径仅影响通过 /new 新建的会话,不影响已有会话的 resume。
472
- export const DEFAULT_CWD_FILE = join(PROJECT_ROOT, "state", "working_dir.txt");
552
+ export const DEFAULT_CWD_FILE = join(USER_DATA_DIR, "state", "working_dir.txt");
473
553
 
474
554
  /** 会话工具类型持久化文件 */
475
- export const SESSIONS_FILE = join(PROJECT_ROOT, "state", "sessions.json");
555
+ export const SESSIONS_FILE = join(USER_DATA_DIR, "state", "sessions.json");
476
556
 
477
557
  /** 最近成功新建会话的工作路径记录(最多 10 条) */
478
- export const RECENT_DIRS_FILE = join(PROJECT_ROOT, "state", "recent_dirs.json");
558
+ export const RECENT_DIRS_FILE = join(USER_DATA_DIR, "state", "recent_dirs.json");
479
559
  export const MAX_RECENT_DIRS = 10;
480
560
 
481
561
  /** 读取最近使用过的工作路径列表(最新的在前) */
package/src/feishu-api.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  BASE_URL,
10
10
  CHAT_LOGS_DIR,
11
11
  PROJECT_ROOT,
12
+ USER_DATA_DIR,
12
13
  CLAUDE_SESSION_PREFIX,
13
14
  CURSOR_SESSION_PREFIX,
14
15
  CODEX_SESSION_PREFIX,
@@ -262,7 +263,7 @@ export function extractSessionId(description: string): string | null {
262
263
 
263
264
  const AVATAR_DIR = resolvePath(PROJECT_ROOT, "images", "avatars");
264
265
  const AVATAR_BADGE_DIR = resolvePath(AVATAR_DIR, "badges");
265
- const AVATAR_KEY_CACHE_FILE = resolvePath(PROJECT_ROOT, "state", "avatar-image-keys.json");
266
+ const AVATAR_KEY_CACHE_FILE = resolvePath(USER_DATA_DIR, "state", "avatar-image-keys.json");
266
267
  const AVATAR_SOURCES: Record<string, string> = {
267
268
  new: resolvePath(AVATAR_DIR, "status_new.png"),
268
269
  busy: resolvePath(AVATAR_DIR, "status_busy.png"),
@@ -409,8 +410,8 @@ export async function setChatAvatar(token: string, chatId: string, tool: string,
409
410
  // Image download & cache
410
411
  // ---------------------------------------------------------------------------
411
412
 
412
- const IMAGE_DOWNLOAD_DIR = resolvePath(PROJECT_ROOT, "images", "downloads");
413
- const IMAGE_CACHE_FILE = resolvePath(PROJECT_ROOT, "state", "image-cache.json");
413
+ const IMAGE_DOWNLOAD_DIR = resolvePath(USER_DATA_DIR, "images", "downloads");
414
+ const IMAGE_CACHE_FILE = resolvePath(USER_DATA_DIR, "state", "image-cache.json");
414
415
 
415
416
  const imageCache = new Map<string, string>();
416
417
  let imageCacheLoaded = false;
package/src/shared.ts CHANGED
@@ -9,14 +9,14 @@ import {
9
9
  writeFileSync,
10
10
  } from "node:fs";
11
11
  import { createServer } from "node:http";
12
- import { dirname, join } from "node:path";
13
- import { fileURLToPath } from "node:url";
12
+ import { homedir } from "node:os";
13
+ import { join } from "node:path";
14
14
  import { WebSocketServer, WebSocket } from "ws";
15
15
 
16
16
  import { printServiceDidNotStart } from "./exit-banner.ts";
17
17
 
18
18
  /** 与 config.LOG_DIR 一致(避免 shared 依赖 config 造成循环引用) */
19
- const BANNER_LOG_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "logs");
19
+ const BANNER_LOG_DIR = join(homedir(), ".chatccc", "logs");
20
20
 
21
21
  const STARTUP_TRACE_FILE = join(BANNER_LOG_DIR, "startup-trace.log");
22
22
 
package/src/web-ui.ts CHANGED
@@ -9,15 +9,17 @@
9
9
  import { createServer, IncomingMessage, ServerResponse } from "node:http";
10
10
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
11
  import { readFile, writeFile, stat } from "node:fs/promises";
12
+ import { homedir } from "node:os";
12
13
  import { join, dirname } from "node:path";
13
14
  import { fileURLToPath } from "node:url";
14
15
  import { spawn, execSync } from "node:child_process";
15
16
 
16
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
18
  const PROJECT_ROOT = join(__dirname, "..");
18
- const CONFIG_FILE = join(PROJECT_ROOT, "config.json");
19
+ const USER_DATA_DIR = join(homedir(), ".chatccc");
20
+ const CONFIG_FILE = join(USER_DATA_DIR, "config.json");
19
21
  const CONFIG_SAMPLE_FILE = join(PROJECT_ROOT, "config.sample.json");
20
- const PID_FILE = join(PROJECT_ROOT, "state", "runtime.pid");
22
+ const PID_FILE = join(USER_DATA_DIR, "state", "runtime.pid");
21
23
 
22
24
  // ---------------------------------------------------------------------------
23
25
  // Helpers — config.json parsing & generation