chatccc 0.1.5 → 0.2.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/README.md +10 -12
- package/bin/chatccc.mjs +26 -9
- package/package.json +2 -1
- package/src/cardkit.ts +157 -157
- package/src/cards.ts +214 -169
- package/src/config.ts +223 -80
- package/src/exit-banner.ts +23 -0
- package/src/feishu-api.ts +248 -243
- package/src/index.ts +708 -599
- package/src/session.ts +455 -409
- package/src/shared.ts +285 -185
package/src/config.ts
CHANGED
|
@@ -1,81 +1,224 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export const
|
|
14
|
-
|
|
15
|
-
export const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export const
|
|
19
|
-
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
await
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export const
|
|
37
|
-
|
|
38
|
-
export const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export const
|
|
42
|
-
|
|
43
|
-
export const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
export
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
export async function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
|
|
6
|
+
import { printServiceDidNotStart } from "./exit-banner.ts";
|
|
7
|
+
import { appendStartupTrace, setupFileLogging } from "./shared.ts";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Paths & logging
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
export const PROJECT_ROOT = join(__dirname, "..");
|
|
15
|
+
export const PID_FILE = join(PROJECT_ROOT, ".claude", "runtime.pid");
|
|
16
|
+
|
|
17
|
+
export const LOG_DIR = join(PROJECT_ROOT, "logs");
|
|
18
|
+
export const fileLog = setupFileLogging(LOG_DIR, "index");
|
|
19
|
+
|
|
20
|
+
export const CHAT_LOGS_DIR = join(PROJECT_ROOT, ".claude", "chat_logs");
|
|
21
|
+
|
|
22
|
+
export async function appendChatLog(chatId: string, sender: string, text: string): Promise<void> {
|
|
23
|
+
try {
|
|
24
|
+
await mkdir(CHAT_LOGS_DIR, { recursive: true });
|
|
25
|
+
const line = JSON.stringify({ ts: Date.now(), sender, text: text.slice(0, 200) }) + "\n";
|
|
26
|
+
await appendFile(join(CHAT_LOGS_DIR, `${chatId}.jsonl`), line);
|
|
27
|
+
} catch {
|
|
28
|
+
// 静默失败,不影响主流程
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Environment & config
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export const USE_LOCAL = process.argv.includes("--local");
|
|
37
|
+
export const APP_ID: string = process.env.CHATCCC_APP_ID ?? "";
|
|
38
|
+
export const APP_SECRET: string = process.env.CHATCCC_APP_SECRET ?? "";
|
|
39
|
+
|
|
40
|
+
/** 当前工作目录下的 .env(全局 chatccc 会尝试用 tsx --env-file 加载此文件) */
|
|
41
|
+
export const ENV_FILE_CWD = join(process.cwd(), ".env");
|
|
42
|
+
|
|
43
|
+
export const BASE_URL = "https://open.feishu.cn/open-apis";
|
|
44
|
+
|
|
45
|
+
export const CHATCCC_PORT = parseInt(process.env.CHATCCC_PORT?.trim() ?? "18080", 10);
|
|
46
|
+
|
|
47
|
+
/** 与 CHATCCC_PORT 一致,供 --local 连接本机中继 */
|
|
48
|
+
export const LOCAL_RELAY_URL = `ws://127.0.0.1:${CHATCCC_PORT}`;
|
|
49
|
+
|
|
50
|
+
/** 未设置时为 `default`;不区分大小写的 `default` 表示交给 SDK/CLI,调用时不传对应字段 */
|
|
51
|
+
export function isSdkAnthropicDefault(value: string): boolean {
|
|
52
|
+
return value.trim().toLowerCase() === "default";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 状态展示用:default 族一律显示为小写 `default` */
|
|
56
|
+
export function anthropicConfigDisplay(value: string): string {
|
|
57
|
+
return isSdkAnthropicDefault(value) ? "default" : value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const CLAUDE_MODEL = process.env.CHATCCC_ANTHROPIC_MODEL?.trim() || "default";
|
|
61
|
+
|
|
62
|
+
export const CLAUDE_EFFORT = process.env.CHATCCC_ANTHROPIC_EFFORT?.trim() || "default";
|
|
63
|
+
|
|
64
|
+
// 新建会话的默认工作路径(/cd 命令设置,持久化到本地文件)
|
|
65
|
+
// 该路径仅影响通过 /new 新建的 Claude 会话,不影响已有会话的 resume。
|
|
66
|
+
export const DEFAULT_CWD_FILE = join(PROJECT_ROOT, ".claude", "working_dir.txt");
|
|
67
|
+
|
|
68
|
+
/** 读取 /cd 设置的默认工作路径。若文件不存在或路径已失效,回退到 PROJECT_ROOT。 */
|
|
69
|
+
export async function getDefaultCwd(): Promise<string> {
|
|
70
|
+
try {
|
|
71
|
+
const content = await readFile(DEFAULT_CWD_FILE, "utf-8");
|
|
72
|
+
const dir = content.trim();
|
|
73
|
+
if (dir) {
|
|
74
|
+
try {
|
|
75
|
+
const s = await stat(dir);
|
|
76
|
+
if (s.isDirectory()) return dir;
|
|
77
|
+
} catch { /* path gone, fall through */ }
|
|
78
|
+
}
|
|
79
|
+
} catch { /* file doesn't exist yet */ }
|
|
80
|
+
return PROJECT_ROOT;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 设置新建会话的默认工作路径(由 /cd 命令调用) */
|
|
84
|
+
export async function setDefaultCwd(dir: string): Promise<void> {
|
|
85
|
+
await writeFile(DEFAULT_CWD_FILE, dir, "utf-8");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Tiny helpers
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
export function ts(): string {
|
|
93
|
+
return new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 仅用于日志确认「已读到某个 App」,不泄露 Secret */
|
|
97
|
+
export function maskAppId(id: string): string {
|
|
98
|
+
if (!id) return "(空)";
|
|
99
|
+
if (id.length <= 10) return `${id.slice(0, 4)}***`;
|
|
100
|
+
return `${id.slice(0, 8)}…${id.slice(-4)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 启动时逐项说明环境变量:成功=已从进程环境读入非空值;失败=必填缺失;默认=未设置则使用内置默认。
|
|
105
|
+
* (.env 需由 tsx --env-file 或系统注入到 process.env 后才算「已读入」。)
|
|
106
|
+
*/
|
|
107
|
+
export function reportEnvironmentVariableReadout(): void {
|
|
108
|
+
const get = (key: string): string => process.env[key]?.trim() ?? "";
|
|
109
|
+
|
|
110
|
+
const rawId = get("CHATCCC_APP_ID");
|
|
111
|
+
const rawSecret = get("CHATCCC_APP_SECRET");
|
|
112
|
+
const rawPort = process.env.CHATCCC_PORT?.trim();
|
|
113
|
+
const rawModel = process.env.CHATCCC_ANTHROPIC_MODEL?.trim();
|
|
114
|
+
const rawEffort = process.env.CHATCCC_ANTHROPIC_EFFORT?.trim();
|
|
115
|
+
|
|
116
|
+
const portBad =
|
|
117
|
+
rawPort !== undefined &&
|
|
118
|
+
rawPort !== "" &&
|
|
119
|
+
(Number.isNaN(CHATCCC_PORT) || CHATCCC_PORT < 1 || CHATCCC_PORT > 65535);
|
|
120
|
+
|
|
121
|
+
const row = (label: string, name: string, kind: "必填" | "可选", ok: boolean, detail: string): void => {
|
|
122
|
+
const state = ok ? "成功" : "失败";
|
|
123
|
+
console.log(` [${state}] [${kind}] ${name}`);
|
|
124
|
+
console.log(` ${label}: ${detail}`);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
console.log(" --- 环境变量读取结果(成功=已读入;失败=必填缺失或格式错误;默认=未设置则用内置值)---");
|
|
128
|
+
|
|
129
|
+
const envExists = existsSync(ENV_FILE_CWD);
|
|
130
|
+
console.log(
|
|
131
|
+
` [信息] 工作目录下 .env: ${envExists ? "存在" : "不存在"} → ${ENV_FILE_CWD}`
|
|
132
|
+
);
|
|
133
|
+
if (!envExists) {
|
|
134
|
+
console.log(
|
|
135
|
+
" 若使用全局 chatccc:当前目录无 .env 时不会自动加载;请 cd 到含 .env 的目录或设置系统环境变量。"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
row(
|
|
140
|
+
"飞书应用",
|
|
141
|
+
"CHATCCC_APP_ID",
|
|
142
|
+
"必填",
|
|
143
|
+
Boolean(rawId),
|
|
144
|
+
rawId ? `已读入,摘要 ${maskAppId(rawId)}` : "未读入或为空"
|
|
145
|
+
);
|
|
146
|
+
row(
|
|
147
|
+
"飞书应用",
|
|
148
|
+
"CHATCCC_APP_SECRET",
|
|
149
|
+
"必填",
|
|
150
|
+
Boolean(rawSecret),
|
|
151
|
+
rawSecret ? "已读入(内容不在日志中显示)" : "未读入或为空"
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (portBad) {
|
|
155
|
+
row("监听端口", "CHATCCC_PORT", "可选", false, `值无效 "${rawPort}",解析得到 ${CHATCCC_PORT},请填写 1–65535 的整数`);
|
|
156
|
+
} else if (rawPort) {
|
|
157
|
+
row("监听端口", "CHATCCC_PORT", "可选", true, `已读入,使用 ${CHATCCC_PORT}`);
|
|
158
|
+
} else {
|
|
159
|
+
console.log(` [默认] [可选] CHATCCC_PORT`);
|
|
160
|
+
console.log(` 监听端口: 未在环境中设置,使用内置默认 ${CHATCCC_PORT}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (rawModel) {
|
|
164
|
+
row("Claude 模型", "CHATCCC_ANTHROPIC_MODEL", "可选", true, `已读入 → ${rawModel}`);
|
|
165
|
+
} else {
|
|
166
|
+
console.log(` [默认] [可选] CHATCCC_ANTHROPIC_MODEL`);
|
|
167
|
+
console.log(
|
|
168
|
+
` Claude 模型: 未设置 → ${anthropicConfigDisplay(CLAUDE_MODEL)}(不区分大小写的 default 时不传入 SDK)`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (rawEffort) {
|
|
173
|
+
row("思考深度", "CHATCCC_ANTHROPIC_EFFORT", "可选", true, `已读入 → ${rawEffort}`);
|
|
174
|
+
} else {
|
|
175
|
+
console.log(` [默认] [可选] CHATCCC_ANTHROPIC_EFFORT`);
|
|
176
|
+
console.log(
|
|
177
|
+
` 思考深度: 未设置 → ${anthropicConfigDisplay(CLAUDE_EFFORT)}(不区分大小写的 default 时不传入 SDK)`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log(" ------------------------------------------------------------------");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 飞书凭证缺失时打印可操作的说明并退出 */
|
|
185
|
+
export function explainMissingFeishuCredentialsAndExit(): never {
|
|
186
|
+
appendStartupTrace("explainMissingFeishuCredentialsAndExit: exiting", {
|
|
187
|
+
hasAppId: Boolean(APP_ID.trim()),
|
|
188
|
+
hasAppSecret: Boolean(APP_SECRET.trim()),
|
|
189
|
+
});
|
|
190
|
+
const hasEnvFile = existsSync(ENV_FILE_CWD);
|
|
191
|
+
const missing: string[] = [];
|
|
192
|
+
if (!APP_ID.trim()) missing.push("CHATCCC_APP_ID");
|
|
193
|
+
if (!APP_SECRET.trim()) missing.push("CHATCCC_APP_SECRET");
|
|
194
|
+
|
|
195
|
+
console.error("\n" + "=".repeat(64));
|
|
196
|
+
console.error(" ChatCCC 启动失败:飞书应用凭证未就绪");
|
|
197
|
+
console.error("=".repeat(64));
|
|
198
|
+
console.error("\n【失败步骤】环境与变量检查(在连接飞书之前)");
|
|
199
|
+
console.error(`\n【未配置的环境变量】\n - ${missing.join("\n - ")}`);
|
|
200
|
+
console.error(`\n【当前工作目录】\n ${process.cwd()}`);
|
|
201
|
+
console.error(`\n【.env 文件】\n 路径: ${ENV_FILE_CWD}`);
|
|
202
|
+
if (hasEnvFile) {
|
|
203
|
+
console.error(
|
|
204
|
+
" 状态: 文件存在,但上述变量仍为空。请打开 .env 检查:\n" +
|
|
205
|
+
" - 变量名是否完全一致(区分大小写)\n" +
|
|
206
|
+
" - 等号两侧不要加引号除非值里需要\n" +
|
|
207
|
+
" - 保存为 UTF-8,避免错误编码\n" +
|
|
208
|
+
" - 若用全局命令 chatccc:必须在放 .env 的目录下执行(先 cd 到项目根)"
|
|
209
|
+
);
|
|
210
|
+
} else {
|
|
211
|
+
console.error(
|
|
212
|
+
" 状态: 文件不存在。\n" +
|
|
213
|
+
" 处理: 复制 .env.example 为 .env 并填入飞书开放平台的 App ID / App Secret;\n" +
|
|
214
|
+
" 或在系统环境变量中设置上述两个变量后重开终端。\n" +
|
|
215
|
+
" 若使用全局 chatccc:请先 cd 到项目根目录再运行,以便加载该目录下的 .env。"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
console.error(`\n【程序包根目录(与「工作目录」可能不同)】\n ${PROJECT_ROOT}`);
|
|
219
|
+
console.error("\n" + "=".repeat(64) + "\n");
|
|
220
|
+
printServiceDidNotStart(`未配置: ${missing.join("、")}`);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
81
224
|
export const SESSION_DESC_PREFIX = "Claude Session:";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 启动失败或进程立即退出时,在控制台输出统一、醒目的说明(避免用户误以为在后台运行)。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function printServiceDidNotStart(summary: string): void {
|
|
6
|
+
const bar = "=".repeat(68);
|
|
7
|
+
console.error("\n\n" + bar);
|
|
8
|
+
console.error(" [ 未启动 ] ChatCCC 已退出,当前没有在后台运行。");
|
|
9
|
+
console.error(" [ 提示 ] 修好下列「原因」后请重新执行: chatccc 或 npm run dev");
|
|
10
|
+
console.error(bar);
|
|
11
|
+
console.error(` 原因: ${summary}`);
|
|
12
|
+
console.error(bar + "\n\n");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 启动成功、进程将常驻时提示用户不要关窗口 */
|
|
16
|
+
export function printServiceRunningHint(mode: "sdk" | "local"): void {
|
|
17
|
+
const bar = "-".repeat(68);
|
|
18
|
+
const tail = mode === "sdk" ? "飞书长连接与本地中继已就绪。" : "本地中继客户端已就绪。";
|
|
19
|
+
console.log("\n" + bar);
|
|
20
|
+
console.log(` [ 运行中 ] ${tail}`);
|
|
21
|
+
console.log(" [ 提示 ] 请保持本窗口打开;要停止请按 Ctrl+C。");
|
|
22
|
+
console.log(bar + "\n");
|
|
23
|
+
}
|