chatccc 0.1.0

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/src/config.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
4
+
5
+ import { setupFileLogging } from "./shared.ts";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Paths & logging
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ export const PROJECT_ROOT = join(__dirname, "..");
13
+ export const PID_FILE = join(PROJECT_ROOT, ".claude", "runtime.pid");
14
+
15
+ export const LOG_DIR = join(PROJECT_ROOT, "logs");
16
+ export const fileLog = setupFileLogging(LOG_DIR, "index");
17
+
18
+ export const CHAT_LOGS_DIR = join(PROJECT_ROOT, ".claude", "chat_logs");
19
+
20
+ export async function appendChatLog(chatId: string, sender: string, text: string): Promise<void> {
21
+ try {
22
+ await mkdir(CHAT_LOGS_DIR, { recursive: true });
23
+ const line = JSON.stringify({ ts: Date.now(), sender, text: text.slice(0, 200) }) + "\n";
24
+ await appendFile(join(CHAT_LOGS_DIR, `${chatId}.jsonl`), line);
25
+ } catch {
26
+ // 静默失败,不影响主流程
27
+ }
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Environment & config
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export const USE_LOCAL = process.argv.includes("--local");
35
+ export const APP_ID: string = process.env.FEISHU_CLAUDER_APP_ID ?? "";
36
+ export const APP_SECRET: string = process.env.FEISHU_CLAUDER_APP_SECRET ?? "";
37
+
38
+ export const BASE_URL = "https://open.feishu.cn/open-apis";
39
+ export const LOCAL_RELAY_URL = "ws://127.0.0.1:18080";
40
+
41
+ export const CLAUDE_MODEL =
42
+ process.env.CHATCCC_ANTHROPIC_MODEL?.trim() || "dashscope/deepseek-v4-pro-anthropic";
43
+
44
+ export const CLAUDE_EFFORT =
45
+ process.env.CHATCCC_ANTHROPIC_EFFORT?.trim() || "max";
46
+
47
+ export const WORKING_DIR_FILE = join(PROJECT_ROOT, ".claude", "working_dir.txt");
48
+
49
+ export async function getWorkingDir(): Promise<string> {
50
+ try {
51
+ const content = await readFile(WORKING_DIR_FILE, "utf-8");
52
+ const dir = content.trim();
53
+ if (dir) return dir;
54
+ } catch { /* file doesn't exist yet */ }
55
+ return PROJECT_ROOT;
56
+ }
57
+
58
+ export async function setWorkingDir(dir: string): Promise<void> {
59
+ await writeFile(WORKING_DIR_FILE, dir, "utf-8");
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Tiny helpers
64
+ // ---------------------------------------------------------------------------
65
+
66
+ export function ts(): string {
67
+ return new Date().toLocaleTimeString("zh-CN", { hour12: false });
68
+ }
69
+
70
+ export const SESSION_DESC_PREFIX = "Claude Session:";
@@ -0,0 +1,244 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import {
5
+ APP_ID,
6
+ APP_SECRET,
7
+ BASE_URL,
8
+ CHAT_LOGS_DIR,
9
+ PROJECT_ROOT,
10
+ SESSION_DESC_PREFIX,
11
+ ts,
12
+ } from "./config.ts";
13
+ import { buildButtons } from "./cards.ts";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Auth
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export async function getTenantAccessToken(): Promise<string> {
20
+ const resp = await fetch(`${BASE_URL}/auth/v3/tenant_access_token/internal`, {
21
+ method: "POST",
22
+ headers: { "Content-Type": "application/json" },
23
+ body: JSON.stringify({ app_id: APP_ID, app_secret: APP_SECRET }),
24
+ });
25
+ const data = (await resp.json()) as { code: number; msg?: string; tenant_access_token: string };
26
+ if (data.code !== 0) throw new Error(`Failed to get token: ${data.msg}`);
27
+ return data.tenant_access_token;
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Group chat CRUD
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export async function createGroupChat(
35
+ token: string,
36
+ name: string,
37
+ userIds: string[]
38
+ ): Promise<string> {
39
+ const resp = await fetch(`${BASE_URL}/im/v1/chats`, {
40
+ method: "POST",
41
+ headers: {
42
+ Authorization: `Bearer ${token}`,
43
+ "Content-Type": "application/json",
44
+ },
45
+ body: JSON.stringify({ name, description: "Creating...", user_id_list: userIds }),
46
+ });
47
+ const data = (await resp.json()) as {
48
+ code: number; msg?: string; data?: { chat_id?: string };
49
+ };
50
+ if (data.code !== 0) throw new Error(`[${data.code}] ${data.msg}`);
51
+ return data.data!.chat_id!;
52
+ }
53
+
54
+ export async function updateChatInfo(
55
+ token: string,
56
+ chatId: string,
57
+ name: string,
58
+ description: string
59
+ ): Promise<void> {
60
+ const resp = await fetch(`${BASE_URL}/im/v1/chats/${chatId}`, {
61
+ method: "PUT",
62
+ headers: {
63
+ Authorization: `Bearer ${token}`,
64
+ "Content-Type": "application/json",
65
+ },
66
+ body: JSON.stringify({ name, description }),
67
+ });
68
+ const data = (await resp.json()) as { code: number; msg?: string };
69
+ if (data.code !== 0) throw new Error(`[${data.code}] ${data.msg}`);
70
+ }
71
+
72
+ export async function getChatInfo(
73
+ token: string,
74
+ chatId: string
75
+ ): Promise<{ name: string; description: string }> {
76
+ const resp = await fetch(`${BASE_URL}/im/v1/chats/${chatId}`, {
77
+ headers: { Authorization: `Bearer ${token}` },
78
+ });
79
+ const data = (await resp.json()) as {
80
+ code: number; msg?: string; data?: { name?: string; description?: string };
81
+ };
82
+ if (data.code !== 0) throw new Error(`[${data.code}] ${data.msg}`);
83
+ return {
84
+ name: data.data?.name ?? "",
85
+ description: data.data?.description ?? "",
86
+ };
87
+ }
88
+
89
+ export function extractSessionId(description: string): string | null {
90
+ const idx = description.indexOf(SESSION_DESC_PREFIX);
91
+ if (idx === -1) return null;
92
+ const after = description.slice(idx + SESSION_DESC_PREFIX.length).trim();
93
+ const match = after.match(/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
94
+ return match ? match[1] : null;
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Messaging
99
+ // ---------------------------------------------------------------------------
100
+
101
+ export async function sendTextReply(
102
+ token: string,
103
+ chatId: string,
104
+ text: string
105
+ ): Promise<void> {
106
+ const card = JSON.stringify({
107
+ config: { wide_screen_mode: true },
108
+ elements: [{ tag: "markdown", content: text }],
109
+ });
110
+ await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
111
+ method: "POST",
112
+ headers: {
113
+ Authorization: `Bearer ${token}`,
114
+ "Content-Type": "application/json",
115
+ },
116
+ body: JSON.stringify({
117
+ receive_id: chatId,
118
+ msg_type: "interactive",
119
+ content: card,
120
+ }),
121
+ });
122
+ }
123
+
124
+ export async function addReaction(
125
+ token: string,
126
+ messageId: string,
127
+ emojiType = "Get"
128
+ ): Promise<void> {
129
+ await fetch(`${BASE_URL}/im/v1/messages/${messageId}/reactions`, {
130
+ method: "POST",
131
+ headers: {
132
+ Authorization: `Bearer ${token}`,
133
+ "Content-Type": "application/json",
134
+ },
135
+ body: JSON.stringify({ reaction_type: { emoji_type: emojiType } }),
136
+ });
137
+ }
138
+
139
+ export async function sendCardReply(
140
+ token: string,
141
+ chatId: string,
142
+ title: string,
143
+ content: string,
144
+ template = "green"
145
+ ): Promise<void> {
146
+ const card = JSON.stringify({
147
+ config: { wide_screen_mode: true },
148
+ header: { template, title: { content: title, tag: "plain_text" } },
149
+ elements: [{ tag: "div", text: { tag: "lark_md", content } }],
150
+ });
151
+
152
+ await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
153
+ method: "POST",
154
+ headers: {
155
+ Authorization: `Bearer ${token}`,
156
+ "Content-Type": "application/json",
157
+ },
158
+ body: JSON.stringify({ receive_id: chatId, msg_type: "interactive", content: card }),
159
+ });
160
+ }
161
+
162
+ // 重启后,向最后有发言的会话发送 "已重启" 卡片(基于 chat_logs 的文件修改时间)
163
+ export async function sendRestartCard(token: string): Promise<void> {
164
+ try {
165
+ const files = await readdir(CHAT_LOGS_DIR).catch(() => [] as string[]);
166
+ if (files.length === 0) {
167
+ console.log(`[${ts()}] [RESTART] No chat logs found, skipping notification`);
168
+ return;
169
+ }
170
+
171
+ let latestChatId: string | null = null;
172
+ let latestTime = 0;
173
+ for (const f of files) {
174
+ if (!f.endsWith(".jsonl")) continue;
175
+ const filePath = join(CHAT_LOGS_DIR, f);
176
+ const st = await stat(filePath).catch(() => null);
177
+ if (!st) continue;
178
+ if (st.mtimeMs > latestTime) {
179
+ latestTime = st.mtimeMs;
180
+ latestChatId = f.replace(".jsonl", "");
181
+ }
182
+ }
183
+
184
+ if (!latestChatId) {
185
+ console.log(`[${ts()}] [RESTART] Could not determine latest chat with messages`);
186
+ return;
187
+ }
188
+
189
+ console.log(`[${ts()}] [RESTART] Latest active chat: ${latestChatId} (mtime=${new Date(latestTime).toISOString()})`);
190
+
191
+ const restartCard = JSON.stringify({
192
+ config: { wide_screen_mode: true },
193
+ header: { template: "green", title: { content: "ChatCCC Started", tag: "plain_text" } },
194
+ elements: [
195
+ { tag: "div", text: { tag: "lark_md", content: "Bot 已启动完成,可以继续使用。\n\n发送 **/new** 创建新会话,或直接在已有会话群中发消息。" } },
196
+ buildButtons([
197
+ { text: "新建会话(/new)", value: JSON.stringify({ cmd: "new" }), type: "primary" },
198
+ { text: "重启Chat CCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
199
+ { text: "查看/切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
200
+ ]),
201
+ ],
202
+ });
203
+ await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
204
+ method: "POST",
205
+ headers: {
206
+ Authorization: `Bearer ${token}`,
207
+ "Content-Type": "application/json",
208
+ },
209
+ body: JSON.stringify({ receive_id: latestChatId, msg_type: "interactive", content: restartCard }),
210
+ });
211
+ console.log(`[${ts()}] [RESTART] Notification sent to chat ${latestChatId}`);
212
+ } catch (err) {
213
+ console.error(`[${ts()}] [RESTART] Failed to send notification: ${(err as Error).message}`);
214
+ }
215
+ }
216
+
217
+ // 撤回消息,成功返回 true
218
+ export async function recallMessage(token: string, messageId: string): Promise<boolean> {
219
+ const resp = await fetch(`${BASE_URL}/im/v1/messages/${messageId}`, {
220
+ method: "DELETE",
221
+ headers: { Authorization: `Bearer ${token}` },
222
+ });
223
+ const data = (await resp.json()) as { code: number };
224
+ return data.code === 0;
225
+ }
226
+
227
+ // 更新卡片消息内容(PATCH 方式仅适用于 interactive 消息)
228
+ // 返回 true 表示更新成功,false 表示 API 返回了错误
229
+ export async function updateCardMessage(token: string, messageId: string, content: string): Promise<boolean> {
230
+ const resp = await fetch(`${BASE_URL}/im/v1/messages/${messageId}`, {
231
+ method: "PATCH",
232
+ headers: {
233
+ Authorization: `Bearer ${token}`,
234
+ "Content-Type": "application/json",
235
+ },
236
+ body: JSON.stringify({ content }),
237
+ });
238
+ const data = await resp.json().catch(() => ({})) as { code: number; msg?: string };
239
+ if (data.code !== 0) {
240
+ console.error(`[${ts()}] [PATCH] updateCardMessage FAIL: messageId=${messageId} code=${data.code} msg="${data.msg}"`);
241
+ return false;
242
+ }
243
+ return true;
244
+ }