chatccc 0.1.6 → 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 +24 -6
- 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/cards.ts
CHANGED
|
@@ -1,170 +1,215 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Button helpers
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
export interface ButtonDef {
|
|
6
|
-
text: string;
|
|
7
|
-
value: string;
|
|
8
|
-
type?: "primary" | "default" | "danger";
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function buildButtons(buttons: ButtonDef[]): object {
|
|
12
|
-
return {
|
|
13
|
-
tag: "action",
|
|
14
|
-
actions: buttons.map((b) => ({
|
|
15
|
-
tag: "button",
|
|
16
|
-
text: { tag: "plain_text", content: b.text },
|
|
17
|
-
type: b.type ?? "primary",
|
|
18
|
-
value: b.value,
|
|
19
|
-
})),
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Content helpers
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
export function truncateContent(text: string, maxLines = 20, maxChars = 8000): string {
|
|
28
|
-
const lines = text.split("\n");
|
|
29
|
-
let displayText: string;
|
|
30
|
-
if (lines.length > maxLines) {
|
|
31
|
-
const firstLine = lines[0];
|
|
32
|
-
const lastLines = lines.slice(-(maxLines - 1)).join("\n");
|
|
33
|
-
displayText = firstLine + "\n...\n" + lastLines;
|
|
34
|
-
} else {
|
|
35
|
-
displayText = text;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (displayText.length > maxChars) {
|
|
39
|
-
displayText = "..." + displayText.slice(-maxChars);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return displayText;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function getToolEmoji(name: string): string {
|
|
46
|
-
const n = name.toLowerCase();
|
|
47
|
-
if (n.includes("read") || n.includes("cat")) return "\u{1F4D6}"; // 📖
|
|
48
|
-
if (n.includes("write")) return "\u{270D}\u{FE0F}"; // ✍️
|
|
49
|
-
if (n.includes("edit")) return "\u{270F}\u{FE0F}"; // ✏️
|
|
50
|
-
if (n.includes("grep") || n.includes("search")) return "\u{1F50E}"; // 🔎
|
|
51
|
-
if (n.includes("glob") || n.includes("find") || n.includes("ls")) return "\u{1F4C2}"; // 📂
|
|
52
|
-
if (n.includes("bash") || n.includes("shell") || n.includes("exec")) return "\u{1F5A5}\u{FE0F}"; // 🖥️
|
|
53
|
-
if (n.includes("websearch") || n.includes("web_search")) return "\u{1F310}"; // 🌐
|
|
54
|
-
if (n.includes("webfetch") || n.includes("web_fetch") || n.includes("fetch")) return "\u{1F4E5}"; // 📥
|
|
55
|
-
if (n.includes("todo") || n.includes("task")) return "\u{2705}"; // ✅
|
|
56
|
-
if (n.includes("agent")) return "\u{1F916}"; // 🤖
|
|
57
|
-
if (n.includes("notebook")) return "\u{1F4D3}"; // 📓
|
|
58
|
-
if (n.includes("ask") || n.includes("question")) return "\u{2753}"; // ❓
|
|
59
|
-
return "\u{1F527}"; // 🔧
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// Card builders
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
// CardKit schema 2.0 思考卡片(带停止按钮,支持打字机流式更新)
|
|
67
|
-
export function buildThinkingCardV2(
|
|
68
|
-
thinkingText: string,
|
|
69
|
-
opts: { showStop?: boolean; headerTitle?: string; headerTemplate?: string } = {}
|
|
70
|
-
): string {
|
|
71
|
-
const { showStop = true, headerTitle = "思考中...", headerTemplate = "blue" } = opts;
|
|
72
|
-
const elements: object[] = [
|
|
73
|
-
{ tag: "markdown", content: truncateContent(thinkingText), element_id: "main_content" },
|
|
74
|
-
];
|
|
75
|
-
if (showStop) {
|
|
76
|
-
elements.push({ tag: "hr" });
|
|
77
|
-
elements.push({
|
|
78
|
-
tag: "button",
|
|
79
|
-
text: { tag: "plain_text", content: "查看状态(/status)" },
|
|
80
|
-
type: "default",
|
|
81
|
-
value: { action: "status" },
|
|
82
|
-
element_id: "action_status",
|
|
83
|
-
});
|
|
84
|
-
elements.push({
|
|
85
|
-
tag: "button",
|
|
86
|
-
text: { tag: "plain_text", content: "停止生成(/stop)" },
|
|
87
|
-
type: "danger",
|
|
88
|
-
value: { action: "stop" },
|
|
89
|
-
element_id: "action_stop",
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
return JSON.stringify({
|
|
93
|
-
schema: "2.0",
|
|
94
|
-
config: {
|
|
95
|
-
update_multi: true,
|
|
96
|
-
streaming_mode: false,
|
|
97
|
-
},
|
|
98
|
-
header: {
|
|
99
|
-
template: headerTemplate,
|
|
100
|
-
title: { tag: "plain_text", content: headerTitle },
|
|
101
|
-
},
|
|
102
|
-
body: {
|
|
103
|
-
direction: "vertical",
|
|
104
|
-
elements,
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function buildHelpCard(userText: string): string {
|
|
110
|
-
return JSON.stringify({
|
|
111
|
-
config: { wide_screen_mode: true },
|
|
112
|
-
header: { template: "blue", title: { content: "ChatCCC", tag: "plain_text" } },
|
|
113
|
-
elements: [
|
|
114
|
-
{ tag: "div", text: { tag: "lark_md", content: `你发送了: ${userText}` } },
|
|
115
|
-
buildButtons([
|
|
116
|
-
{ text: "新建会话(/new)", value: JSON.stringify({ cmd: "new" }), type: "primary" },
|
|
117
|
-
{ text: "重启Chat CCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
|
|
118
|
-
{ text: "查看/切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
|
|
119
|
-
]),
|
|
120
|
-
],
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 工作路径内容(/cd 命令统一使用,返回 markdown 内容字符串)
|
|
125
|
-
export function buildCdContent(
|
|
126
|
-
dirPath: string,
|
|
127
|
-
entries: { name: string; isDir: boolean }[],
|
|
128
|
-
isUpdate: boolean,
|
|
129
|
-
maxFiles = 100
|
|
130
|
-
): string {
|
|
131
|
-
const display = entries.slice(0, maxFiles);
|
|
132
|
-
const overflow = entries.length > maxFiles ? `\n...(共 ${entries.length} 个条目,仅显示前 ${maxFiles} 个)` : "";
|
|
133
|
-
const listing = display.map(e => e.isDir ? `📁 ${e.name}/` : `📄 ${e.name}`).join("\n");
|
|
134
|
-
|
|
135
|
-
const statusLine = isUpdate
|
|
136
|
-
? `**新会话工作路径已切换至:** \`${dirPath}\``
|
|
137
|
-
: `**新会话默认工作路径:** \`${dirPath}\``;
|
|
138
|
-
|
|
139
|
-
return [
|
|
140
|
-
statusLine,
|
|
141
|
-
``,
|
|
142
|
-
`此路径持久化在配置文件中,仅影响**新建会话**的工作路径。`,
|
|
143
|
-
``,
|
|
144
|
-
`---`,
|
|
145
|
-
`**目录内容** (最多 ${maxFiles} 个):`,
|
|
146
|
-
listing,
|
|
147
|
-
overflow,
|
|
148
|
-
].join("\n");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
export function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Button helpers
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export interface ButtonDef {
|
|
6
|
+
text: string;
|
|
7
|
+
value: string;
|
|
8
|
+
type?: "primary" | "default" | "danger";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildButtons(buttons: ButtonDef[]): object {
|
|
12
|
+
return {
|
|
13
|
+
tag: "action",
|
|
14
|
+
actions: buttons.map((b) => ({
|
|
15
|
+
tag: "button",
|
|
16
|
+
text: { tag: "plain_text", content: b.text },
|
|
17
|
+
type: b.type ?? "primary",
|
|
18
|
+
value: b.value,
|
|
19
|
+
})),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Content helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export function truncateContent(text: string, maxLines = 20, maxChars = 8000): string {
|
|
28
|
+
const lines = text.split("\n");
|
|
29
|
+
let displayText: string;
|
|
30
|
+
if (lines.length > maxLines) {
|
|
31
|
+
const firstLine = lines[0];
|
|
32
|
+
const lastLines = lines.slice(-(maxLines - 1)).join("\n");
|
|
33
|
+
displayText = firstLine + "\n...\n" + lastLines;
|
|
34
|
+
} else {
|
|
35
|
+
displayText = text;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (displayText.length > maxChars) {
|
|
39
|
+
displayText = "..." + displayText.slice(-maxChars);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return displayText;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getToolEmoji(name: string): string {
|
|
46
|
+
const n = name.toLowerCase();
|
|
47
|
+
if (n.includes("read") || n.includes("cat")) return "\u{1F4D6}"; // 📖
|
|
48
|
+
if (n.includes("write")) return "\u{270D}\u{FE0F}"; // ✍️
|
|
49
|
+
if (n.includes("edit")) return "\u{270F}\u{FE0F}"; // ✏️
|
|
50
|
+
if (n.includes("grep") || n.includes("search")) return "\u{1F50E}"; // 🔎
|
|
51
|
+
if (n.includes("glob") || n.includes("find") || n.includes("ls")) return "\u{1F4C2}"; // 📂
|
|
52
|
+
if (n.includes("bash") || n.includes("shell") || n.includes("exec")) return "\u{1F5A5}\u{FE0F}"; // 🖥️
|
|
53
|
+
if (n.includes("websearch") || n.includes("web_search")) return "\u{1F310}"; // 🌐
|
|
54
|
+
if (n.includes("webfetch") || n.includes("web_fetch") || n.includes("fetch")) return "\u{1F4E5}"; // 📥
|
|
55
|
+
if (n.includes("todo") || n.includes("task")) return "\u{2705}"; // ✅
|
|
56
|
+
if (n.includes("agent")) return "\u{1F916}"; // 🤖
|
|
57
|
+
if (n.includes("notebook")) return "\u{1F4D3}"; // 📓
|
|
58
|
+
if (n.includes("ask") || n.includes("question")) return "\u{2753}"; // ❓
|
|
59
|
+
return "\u{1F527}"; // 🔧
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Card builders
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
// CardKit schema 2.0 思考卡片(带停止按钮,支持打字机流式更新)
|
|
67
|
+
export function buildThinkingCardV2(
|
|
68
|
+
thinkingText: string,
|
|
69
|
+
opts: { showStop?: boolean; headerTitle?: string; headerTemplate?: string } = {}
|
|
70
|
+
): string {
|
|
71
|
+
const { showStop = true, headerTitle = "思考中...", headerTemplate = "blue" } = opts;
|
|
72
|
+
const elements: object[] = [
|
|
73
|
+
{ tag: "markdown", content: truncateContent(thinkingText), element_id: "main_content" },
|
|
74
|
+
];
|
|
75
|
+
if (showStop) {
|
|
76
|
+
elements.push({ tag: "hr" });
|
|
77
|
+
elements.push({
|
|
78
|
+
tag: "button",
|
|
79
|
+
text: { tag: "plain_text", content: "查看状态(/status)" },
|
|
80
|
+
type: "default",
|
|
81
|
+
value: { action: "status" },
|
|
82
|
+
element_id: "action_status",
|
|
83
|
+
});
|
|
84
|
+
elements.push({
|
|
85
|
+
tag: "button",
|
|
86
|
+
text: { tag: "plain_text", content: "停止生成(/stop)" },
|
|
87
|
+
type: "danger",
|
|
88
|
+
value: { action: "stop" },
|
|
89
|
+
element_id: "action_stop",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return JSON.stringify({
|
|
93
|
+
schema: "2.0",
|
|
94
|
+
config: {
|
|
95
|
+
update_multi: true,
|
|
96
|
+
streaming_mode: false,
|
|
97
|
+
},
|
|
98
|
+
header: {
|
|
99
|
+
template: headerTemplate,
|
|
100
|
+
title: { tag: "plain_text", content: headerTitle },
|
|
101
|
+
},
|
|
102
|
+
body: {
|
|
103
|
+
direction: "vertical",
|
|
104
|
+
elements,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function buildHelpCard(userText: string): string {
|
|
110
|
+
return JSON.stringify({
|
|
111
|
+
config: { wide_screen_mode: true },
|
|
112
|
+
header: { template: "blue", title: { content: "ChatCCC", tag: "plain_text" } },
|
|
113
|
+
elements: [
|
|
114
|
+
{ tag: "div", text: { tag: "lark_md", content: `你发送了: ${userText}` } },
|
|
115
|
+
buildButtons([
|
|
116
|
+
{ text: "新建会话(/new)", value: JSON.stringify({ cmd: "new" }), type: "primary" },
|
|
117
|
+
{ text: "重启Chat CCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
|
|
118
|
+
{ text: "查看/切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
|
|
119
|
+
]),
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 工作路径内容(/cd 命令统一使用,返回 markdown 内容字符串)
|
|
125
|
+
export function buildCdContent(
|
|
126
|
+
dirPath: string,
|
|
127
|
+
entries: { name: string; isDir: boolean }[],
|
|
128
|
+
isUpdate: boolean,
|
|
129
|
+
maxFiles = 100
|
|
130
|
+
): string {
|
|
131
|
+
const display = entries.slice(0, maxFiles);
|
|
132
|
+
const overflow = entries.length > maxFiles ? `\n...(共 ${entries.length} 个条目,仅显示前 ${maxFiles} 个)` : "";
|
|
133
|
+
const listing = display.map(e => e.isDir ? `📁 ${e.name}/` : `📄 ${e.name}`).join("\n");
|
|
134
|
+
|
|
135
|
+
const statusLine = isUpdate
|
|
136
|
+
? `**新会话工作路径已切换至:** \`${dirPath}\``
|
|
137
|
+
: `**新会话默认工作路径:** \`${dirPath}\``;
|
|
138
|
+
|
|
139
|
+
return [
|
|
140
|
+
statusLine,
|
|
141
|
+
``,
|
|
142
|
+
`此路径持久化在配置文件中,仅影响**新建会话**的工作路径。`,
|
|
143
|
+
``,
|
|
144
|
+
`---`,
|
|
145
|
+
`**目录内容** (最多 ${maxFiles} 个):`,
|
|
146
|
+
listing,
|
|
147
|
+
overflow,
|
|
148
|
+
].join("\n");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 所有会话列表卡片
|
|
152
|
+
export function buildSessionsCard(sessions: Array<{
|
|
153
|
+
sessionId: string;
|
|
154
|
+
active: boolean;
|
|
155
|
+
turnCount: number;
|
|
156
|
+
elapsedSeconds: number | null;
|
|
157
|
+
model: string;
|
|
158
|
+
}>): string {
|
|
159
|
+
const text = sessions.length === 0
|
|
160
|
+
? "当前没有会话记录。"
|
|
161
|
+
: [
|
|
162
|
+
`共 **${sessions.length}** 个会话:`,
|
|
163
|
+
``,
|
|
164
|
+
...sessions.map((s, i) => {
|
|
165
|
+
const status = s.active ? "🟢 活跃" : "⚪ 空闲";
|
|
166
|
+
const shortId = s.sessionId.length > 16 ? s.sessionId.slice(0, 16) + "..." : s.sessionId;
|
|
167
|
+
let extra = "";
|
|
168
|
+
if (s.active && s.elapsedSeconds !== null) {
|
|
169
|
+
const mins = Math.floor(s.elapsedSeconds / 60);
|
|
170
|
+
const secs = s.elapsedSeconds % 60;
|
|
171
|
+
extra = ` | 本轮: ${mins}分${secs}秒`;
|
|
172
|
+
}
|
|
173
|
+
return `**${i + 1}.** \`${shortId}\` ${status} | 轮数: ${s.turnCount} | ${s.model}${extra}`;
|
|
174
|
+
}),
|
|
175
|
+
].join("\n");
|
|
176
|
+
|
|
177
|
+
return JSON.stringify({
|
|
178
|
+
config: { wide_screen_mode: true },
|
|
179
|
+
header: { template: "blue", title: { content: "所有会话", tag: "plain_text" } },
|
|
180
|
+
elements: [
|
|
181
|
+
{ tag: "div", text: { tag: "lark_md", content: text } },
|
|
182
|
+
{ tag: "hr" },
|
|
183
|
+
{
|
|
184
|
+
tag: "action",
|
|
185
|
+
actions: [{
|
|
186
|
+
tag: "button",
|
|
187
|
+
text: { tag: "plain_text", content: "收起" },
|
|
188
|
+
type: "default",
|
|
189
|
+
value: { action: "close" },
|
|
190
|
+
}],
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 状态卡片(带关闭按钮)
|
|
197
|
+
export function buildStatusCard(statusText: string, template = "blue"): string {
|
|
198
|
+
return JSON.stringify({
|
|
199
|
+
config: { wide_screen_mode: true },
|
|
200
|
+
header: { template, title: { content: "会话状态", tag: "plain_text" } },
|
|
201
|
+
elements: [
|
|
202
|
+
{ tag: "div", text: { tag: "lark_md", content: statusText } },
|
|
203
|
+
{ tag: "hr" },
|
|
204
|
+
{
|
|
205
|
+
tag: "action",
|
|
206
|
+
actions: [{
|
|
207
|
+
tag: "button",
|
|
208
|
+
text: { tag: "plain_text", content: "收起" },
|
|
209
|
+
type: "default",
|
|
210
|
+
value: { action: "close" },
|
|
211
|
+
}],
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
});
|
|
170
215
|
}
|