chatccc 0.2.3 → 0.2.4

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,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -23,7 +23,9 @@
23
23
  "demo:create-group": "tsx --env-file=.env src/index.ts",
24
24
  "demo:create-group:local": "tsx --env-file=.env src/index.ts --local",
25
25
  "demo:permission-check": "tsx --env-file=.env demo/permission_check.ts",
26
- "demo:claude-hi": "tsx --env-file=.env demo/claude_say_hi.ts"
26
+ "demo:claude-hi": "tsx --env-file=.env demo/claude_say_hi.ts",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest"
27
29
  },
28
30
  "dependencies": {
29
31
  "@anthropic-ai/claude-agent-sdk": "0.2.133",
@@ -34,7 +36,8 @@
34
36
  "devDependencies": {
35
37
  "@types/node": "^20.0.0",
36
38
  "@types/ws": "^8.18.1",
37
- "typescript": "^5.0.0"
39
+ "typescript": "^5.0.0",
40
+ "vitest": "^4.1.5"
38
41
  },
39
42
  "engines": {
40
43
  "node": ">=20"
@@ -0,0 +1,297 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ buildProgressCard,
4
+ buildHelpCard,
5
+ buildCdContent,
6
+ buildSessionsCard,
7
+ buildStatusCard,
8
+ buildButtons,
9
+ truncateContent,
10
+ getToolEmoji,
11
+ } from "../cards.ts";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // truncateContent
15
+ // ---------------------------------------------------------------------------
16
+
17
+ describe("truncateContent", () => {
18
+ it("returns original text when under limits", () => {
19
+ expect(truncateContent("hello")).toBe("hello");
20
+ });
21
+
22
+ it("truncates lines when exceeding maxLines (default 20)", () => {
23
+ const lines = Array.from({ length: 25 }, (_, i) => `line ${i + 1}`);
24
+ const text = lines.join("\n");
25
+ const result = truncateContent(text);
26
+ const resultLines = result.split("\n");
27
+ expect(resultLines.length).toBe(21); // 1 first + 1 "..." + 19 last
28
+ expect(resultLines[0]).toBe("line 1");
29
+ expect(resultLines[1]).toBe("...");
30
+ expect(resultLines[2]).toBe("line 7");
31
+ expect(resultLines[20]).toBe("line 25");
32
+ });
33
+
34
+ it("truncates chars when exceeding maxChars", () => {
35
+ const long = "x".repeat(9000);
36
+ const result = truncateContent(long, 100, 500);
37
+ expect(result.length).toBeLessThanOrEqual(503); // "..." + 500 chars
38
+ expect(result.startsWith("...")).toBe(true);
39
+ });
40
+
41
+ it("returns empty string for empty input", () => {
42
+ expect(truncateContent("")).toBe("");
43
+ });
44
+
45
+ it("respects custom maxLines and maxChars", () => {
46
+ const lines = Array.from({ length: 10 }, (_, i) => `line ${i + 1}`);
47
+ const text = lines.join("\n");
48
+ const result = truncateContent(text, 5, 1000);
49
+ const resultLines = result.split("\n");
50
+ expect(resultLines[0]).toBe("line 1");
51
+ expect(resultLines[1]).toBe("...");
52
+ expect(resultLines[resultLines.length - 1]).toBe("line 10");
53
+ expect(resultLines.length).toBe(6); // 1 first + "..." + 4 last
54
+ });
55
+ });
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // getToolEmoji
59
+ // ---------------------------------------------------------------------------
60
+
61
+ describe("getToolEmoji", () => {
62
+ it("returns correct emoji for each known tool name", () => {
63
+ expect(getToolEmoji("Read")).toBe("\u{1F4D6}"); // 📖
64
+ expect(getToolEmoji("Write")).toBe("\u{270D}\u{FE0F}"); // âœī¸
65
+ expect(getToolEmoji("Edit")).toBe("\u{270F}\u{FE0F}"); // âœī¸
66
+ expect(getToolEmoji("Grep")).toBe("\u{1F50E}"); // 🔎
67
+ expect(getToolEmoji("Glob")).toBe("\u{1F4C2}"); // 📂
68
+ expect(getToolEmoji("Bash")).toBe("\u{1F5A5}\u{FE0F}"); // đŸ–Ĩī¸
69
+ expect(getToolEmoji("WebSearch")).toBe("\u{1F310}"); // 🌐
70
+ expect(getToolEmoji("WebFetch")).toBe("\u{1F4E5}"); // đŸ“Ĩ
71
+ expect(getToolEmoji("TodoWrite")).toBe("\u{2705}"); // ✅
72
+ expect(getToolEmoji("Agent")).toBe("\u{1F916}"); // 🤖
73
+ expect(getToolEmoji("NotebookEdit")).toBe("\u{1F4D3}"); // 📓
74
+ expect(getToolEmoji("AskUserQuestion")).toBe("\u{2753}");// ❓
75
+ });
76
+
77
+ it("returns wrench for unknown tool names", () => {
78
+ expect(getToolEmoji("UnknownTool")).toBe("\u{1F527}");
79
+ expect(getToolEmoji("cat")).toBe("\u{1F527}");
80
+ expect(getToolEmoji("")).toBe("\u{1F527}");
81
+ });
82
+ });
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // buildProgressCard
86
+ // ---------------------------------------------------------------------------
87
+
88
+ describe("buildProgressCard", () => {
89
+ it("returns valid JSON with correct schema", () => {
90
+ const card = buildProgressCard("test content");
91
+ const parsed = JSON.parse(card);
92
+ expect(parsed.schema).toBe("2.0");
93
+ expect(parsed.config.update_multi).toBe(true);
94
+ expect(parsed.config.streaming_mode).toBe(false);
95
+ });
96
+
97
+ it("uses default header title 'į”Ÿæˆä¸­...'", () => {
98
+ const card = buildProgressCard("hello");
99
+ const parsed = JSON.parse(card);
100
+ expect(parsed.header.title.content).toBe("į”Ÿæˆä¸­...");
101
+ expect(parsed.header.template).toBe("blue");
102
+ });
103
+
104
+ it("includes stop button by default", () => {
105
+ const card = buildProgressCard("hello");
106
+ const parsed = JSON.parse(card);
107
+ const buttons = parsed.body.elements.filter((e: any) => e.tag === "button");
108
+ expect(buttons).toHaveLength(2);
109
+ expect(buttons[0].text.content).toBe("æŸĨįœ‹įŠļ态īŧˆ/statusīŧ‰");
110
+ expect(buttons[1].text.content).toBe("停æ­ĸį”Ÿæˆīŧˆ/stopīŧ‰");
111
+ });
112
+
113
+ it("hides stop button when showStop is false", () => {
114
+ const card = buildProgressCard("hello", { showStop: false });
115
+ const parsed = JSON.parse(card);
116
+ const buttons = parsed.body.elements.filter((e: any) => e.tag === "button");
117
+ expect(buttons).toHaveLength(0);
118
+ });
119
+
120
+ it("uses custom header title and template", () => {
121
+ const card = buildProgressCard("hello", { headerTitle: "åˇ˛åŽŒæˆ", headerTemplate: "green" });
122
+ const parsed = JSON.parse(card);
123
+ expect(parsed.header.title.content).toBe("åˇ˛åŽŒæˆ");
124
+ expect(parsed.header.template).toBe("green");
125
+ });
126
+
127
+ it("includes markdown element with truncated content", () => {
128
+ const card = buildProgressCard("markdown text");
129
+ const parsed = JSON.parse(card);
130
+ const md = parsed.body.elements.find((e: any) => e.tag === "markdown");
131
+ expect(md).toBeDefined();
132
+ expect(md.element_id).toBe("main_content");
133
+ });
134
+ });
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // buildHelpCard
138
+ // ---------------------------------------------------------------------------
139
+
140
+ describe("buildHelpCard", () => {
141
+ it("returns valid JSON with user text", () => {
142
+ const card = buildHelpCard("äŊ åĨŊ");
143
+ const parsed = JSON.parse(card);
144
+ expect(parsed.header.title.content).toBe("ChatCCC");
145
+ expect(parsed.elements[0].text.content).toContain("äŊ åĨŊ");
146
+ });
147
+
148
+ it("includes action buttons", () => {
149
+ const card = buildHelpCard("test");
150
+ const parsed = JSON.parse(card);
151
+ const action = parsed.elements[1];
152
+ expect(action.tag).toBe("action");
153
+ expect(action.actions).toHaveLength(3);
154
+ });
155
+ });
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // buildCdContent
159
+ // ---------------------------------------------------------------------------
160
+
161
+ describe("buildCdContent", () => {
162
+ const entries = [
163
+ { name: "src", isDir: true },
164
+ { name: "README.md", isDir: false },
165
+ { name: "package.json", isDir: false },
166
+ ];
167
+
168
+ it("returns markdown with path and listing", () => {
169
+ const content = buildCdContent("/home/user/project", entries, false);
170
+ expect(content).toContain("/home/user/project");
171
+ expect(content).toContain("📁 src/");
172
+ expect(content).toContain("📄 README.md");
173
+ expect(content).toContain("📄 package.json");
174
+ });
175
+
176
+ it("shows åˇ˛åˆ‡æĸ when isUpdate is true", () => {
177
+ const content = buildCdContent("/home/user/project", entries, true);
178
+ expect(content).toContain("īŧˆåˇ˛åˆ‡æĸīŧ‰");
179
+ });
180
+
181
+ it("shows currentCwd when provided", () => {
182
+ const content = buildCdContent("/new/path", entries, false, "/old/path");
183
+ expect(content).toContain("åŊ“前äŧšč¯åˇĨäŊœčˇ¯åž„");
184
+ expect(content).toContain("/old/path");
185
+ });
186
+
187
+ it("omits currentCwd line when not provided", () => {
188
+ const content = buildCdContent("/new/path", entries, false);
189
+ expect(content).not.toContain("åŊ“前äŧšč¯åˇĨäŊœčˇ¯åž„");
190
+ });
191
+
192
+ it("caps listing at maxFiles", () => {
193
+ const many = Array.from({ length: 150 }, (_, i) => ({
194
+ name: `file${i}.txt`,
195
+ isDir: false,
196
+ }));
197
+ const content = buildCdContent("/path", many, false);
198
+ expect(content).toContain("äģ…æ˜žį¤ē前 100 ä¸Ē");
199
+ });
200
+ });
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // buildSessionsCard
204
+ // ---------------------------------------------------------------------------
205
+
206
+ describe("buildSessionsCard", () => {
207
+ it("returns valid JSON for empty sessions", () => {
208
+ const card = buildSessionsCard([]);
209
+ const parsed = JSON.parse(card);
210
+ expect(parsed.elements[0].text.content).toContain("æ˛Ąæœ‰äŧšč¯čްåŊ•");
211
+ });
212
+
213
+ it("returns valid JSON with session listing", () => {
214
+ const card = buildSessionsCard([
215
+ { sessionId: "abc123", active: true, turnCount: 5, elapsedSeconds: 120, model: "Claude Opus 4.7" },
216
+ ]);
217
+ const parsed = JSON.parse(card);
218
+ expect(parsed.elements[0].text.content).toContain("å…ą **1** ä¸Ēäŧšč¯");
219
+ expect(parsed.elements[0].text.content).toContain("abc123");
220
+ expect(parsed.elements[0].text.content).toContain("đŸŸĸ æ´ģ衃");
221
+ });
222
+
223
+ it("shows idle status for inactive sessions", () => {
224
+ const card = buildSessionsCard([
225
+ { sessionId: "xyz", active: false, turnCount: 0, elapsedSeconds: null, model: "Claude Sonnet 4.6" },
226
+ ]);
227
+ const parsed = JSON.parse(card);
228
+ expect(parsed.elements[0].text.content).toContain("âšĒ įŠē闲");
229
+ });
230
+
231
+ it("shows elapsed time for active sessions", () => {
232
+ const card = buildSessionsCard([
233
+ { sessionId: "active123", active: true, turnCount: 3, elapsedSeconds: 95, model: "Claude Opus 4.7" },
234
+ ]);
235
+ const parsed = JSON.parse(card);
236
+ expect(parsed.elements[0].text.content).toContain("1分35į§’");
237
+ });
238
+
239
+ it("includes close button", () => {
240
+ const card = buildSessionsCard([]);
241
+ const parsed = JSON.parse(card);
242
+ const action = parsed.elements[2];
243
+ expect(action.actions[0].text.content).toBe("æ”ļčĩˇ");
244
+ });
245
+ });
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // buildStatusCard
249
+ // ---------------------------------------------------------------------------
250
+
251
+ describe("buildStatusCard", () => {
252
+ it("returns valid JSON with status text", () => {
253
+ const card = buildStatusCard("ä¸€åˆ‡æ­Ŗå¸¸");
254
+ const parsed = JSON.parse(card);
255
+ expect(parsed.header.title.content).toBe("äŧšč¯įŠļ态");
256
+ expect(parsed.elements[0].text.content).toBe("ä¸€åˆ‡æ­Ŗå¸¸");
257
+ });
258
+
259
+ it("uses custom template color", () => {
260
+ const card = buildStatusCard("č­Ļ告", "red");
261
+ const parsed = JSON.parse(card);
262
+ expect(parsed.header.template).toBe("red");
263
+ });
264
+
265
+ it("includes close button", () => {
266
+ const card = buildStatusCard("test");
267
+ const parsed = JSON.parse(card);
268
+ const action = parsed.elements[2];
269
+ expect(action.actions[0].text.content).toBe("æ”ļčĩˇ");
270
+ });
271
+ });
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // buildButtons
275
+ // ---------------------------------------------------------------------------
276
+
277
+ describe("buildButtons", () => {
278
+ it("returns action with buttons", () => {
279
+ const result = buildButtons([
280
+ { text: "įĄŽčŽ¤", value: "ok", type: "primary" },
281
+ ]);
282
+ const obj = result as any;
283
+ expect(obj.tag).toBe("action");
284
+ expect(obj.actions).toHaveLength(1);
285
+ expect(obj.actions[0].text.content).toBe("įĄŽčŽ¤");
286
+ expect(obj.actions[0].type).toBe("primary");
287
+ expect(obj.actions[0].value).toBe("ok");
288
+ });
289
+
290
+ it("defaults button type to primary", () => {
291
+ const result = buildButtons([
292
+ { text: "取æļˆ", value: "cancel" },
293
+ ]);
294
+ const obj = result as any;
295
+ expect(obj.actions[0].type).toBe("primary");
296
+ });
297
+ });
@@ -0,0 +1,151 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import {
3
+ chatSessionMap,
4
+ sessionInfoMap,
5
+ processedMessages,
6
+ MAX_PROCESSED,
7
+ resetState,
8
+ getSessionStatus,
9
+ getAllSessionsStatus,
10
+ } from "../session.ts";
11
+
12
+ // Helper to create a mock active session entry
13
+ function mockActiveSession(chatId: string, overrides: Partial<{
14
+ accumulatedContent: string;
15
+ finalText: string;
16
+ stopped: boolean;
17
+ }> = {}) {
18
+ chatSessionMap.set(chatId, {
19
+ gen: 1,
20
+ close: () => {},
21
+ cardId: null,
22
+ stopped: overrides.stopped ?? false,
23
+ accumulatedContent: overrides.accumulatedContent ?? "thinking...",
24
+ finalText: overrides.finalText ?? "",
25
+ spinnerTimer: null,
26
+ msgTimestamp: Date.now(),
27
+ sequence: 0,
28
+ cardBusy: false,
29
+ });
30
+ }
31
+
32
+ function mockSessionInfo(chatId: string, overrides: Partial<{
33
+ sessionId: string;
34
+ turnCount: number;
35
+ lastContextTokens: number;
36
+ startTime: number;
37
+ }> = {}) {
38
+ sessionInfoMap.set(chatId, {
39
+ sessionId: overrides.sessionId ?? "test-session-id",
40
+ turnCount: overrides.turnCount ?? 3,
41
+ lastContextTokens: overrides.lastContextTokens ?? 50000,
42
+ startTime: overrides.startTime ?? Date.now(),
43
+ model: "Claude Opus 4.7",
44
+ effort: "high",
45
+ });
46
+ }
47
+
48
+ describe("resetState", () => {
49
+ it("clears all maps and sets", () => {
50
+ chatSessionMap.set("chat1", {
51
+ gen: 1, close: () => {}, cardId: null, stopped: false,
52
+ accumulatedContent: "", finalText: "", spinnerTimer: null,
53
+ msgTimestamp: 0, sequence: 0, cardBusy: false,
54
+ });
55
+ sessionInfoMap.set("chat1", {
56
+ sessionId: "s1", turnCount: 1, lastContextTokens: 0,
57
+ startTime: 0, model: "", effort: "",
58
+ });
59
+ processedMessages.add("msg1");
60
+
61
+ resetState();
62
+
63
+ expect(chatSessionMap.size).toBe(0);
64
+ expect(sessionInfoMap.size).toBe(0);
65
+ expect(processedMessages.size).toBe(0);
66
+ });
67
+ });
68
+
69
+ describe("getSessionStatus", () => {
70
+ beforeEach(() => {
71
+ chatSessionMap.clear();
72
+ sessionInfoMap.clear();
73
+ });
74
+
75
+ it("returns null for unknown chatId", () => {
76
+ expect(getSessionStatus("nonexistent")).toBeNull();
77
+ });
78
+
79
+ it("returns status for idle session (info exists, no active session)", () => {
80
+ mockSessionInfo("chat1");
81
+ const status = getSessionStatus("chat1");
82
+ expect(status).not.toBeNull();
83
+ expect(status!.sessionId).toBe("test-session-id");
84
+ expect(status!.running).toBe(false);
85
+ expect(status!.turnCount).toBe(3);
86
+ expect(status!.accumulatedLength).toBe(0);
87
+ });
88
+
89
+ it("returns running=true for active session", () => {
90
+ mockSessionInfo("chat1");
91
+ mockActiveSession("chat1", { accumulatedContent: "thinking...", finalText: "reply" });
92
+ const status = getSessionStatus("chat1");
93
+ expect(status!.running).toBe(true);
94
+ expect(status!.accumulatedLength).toBe(16); // "thinking..."(11) + "reply"(5)
95
+ });
96
+
97
+ it("returns running=false for stopped session", () => {
98
+ mockSessionInfo("chat1");
99
+ mockActiveSession("chat1", { stopped: true });
100
+ const status = getSessionStatus("chat1");
101
+ expect(status!.running).toBe(false);
102
+ });
103
+
104
+ it("returns correct turnCount and other info fields", () => {
105
+ mockSessionInfo("chat1", { turnCount: 7, lastContextTokens: 100000 });
106
+ const status = getSessionStatus("chat1");
107
+ expect(status!.turnCount).toBe(7);
108
+ expect(status!.lastContextTokens).toBe(100000);
109
+ });
110
+ });
111
+
112
+ describe("getAllSessionsStatus", () => {
113
+ beforeEach(() => {
114
+ chatSessionMap.clear();
115
+ sessionInfoMap.clear();
116
+ });
117
+
118
+ it("returns empty array when no sessions", () => {
119
+ expect(getAllSessionsStatus()).toEqual([]);
120
+ });
121
+
122
+ it("returns statuses for all recorded sessions", () => {
123
+ mockSessionInfo("chat1", { sessionId: "s1" });
124
+ mockSessionInfo("chat2", { sessionId: "s2" });
125
+ mockActiveSession("chat1");
126
+ const result = getAllSessionsStatus();
127
+ expect(result).toHaveLength(2);
128
+ expect(result.find(r => r.chatId === "chat1")!.active).toBe(true);
129
+ expect(result.find(r => r.chatId === "chat2")!.active).toBe(false);
130
+ });
131
+
132
+ it("marks stopped sessions as not active", () => {
133
+ mockSessionInfo("chat1");
134
+ mockActiveSession("chat1", { stopped: true });
135
+ const result = getAllSessionsStatus();
136
+ expect(result[0].active).toBe(false);
137
+ });
138
+ });
139
+
140
+ describe("processedMessages dedup", () => {
141
+ it("supports add/has semantics", () => {
142
+ processedMessages.clear();
143
+ processedMessages.add("msg_001");
144
+ expect(processedMessages.has("msg_001")).toBe(true);
145
+ expect(processedMessages.has("msg_002")).toBe(false);
146
+ });
147
+
148
+ it("MAX_PROCESSED is defined", () => {
149
+ expect(MAX_PROCESSED).toBe(5000);
150
+ });
151
+ });
package/src/cards.ts CHANGED
@@ -42,35 +42,37 @@ export function truncateContent(text: string, maxLines = 20, maxChars = 8000): s
42
42
  return displayText;
43
43
  }
44
44
 
45
+ const TOOL_EMOJI_MAP: Record<string, string> = {
46
+ Read: "\u{1F4D6}", // 📖
47
+ Write: "\u{270D}\u{FE0F}", // âœī¸
48
+ Edit: "\u{270F}\u{FE0F}", // âœī¸
49
+ Grep: "\u{1F50E}", // 🔎
50
+ Glob: "\u{1F4C2}", // 📂
51
+ Bash: "\u{1F5A5}\u{FE0F}", // đŸ–Ĩī¸
52
+ WebSearch: "\u{1F310}", // 🌐
53
+ WebFetch: "\u{1F4E5}", // đŸ“Ĩ
54
+ TodoWrite: "\u{2705}", // ✅
55
+ Agent: "\u{1F916}", // 🤖
56
+ NotebookEdit: "\u{1F4D3}", // 📓
57
+ AskUserQuestion: "\u{2753}",// ❓
58
+ };
59
+
45
60
  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}"; // 🔧
61
+ return TOOL_EMOJI_MAP[name] ?? "\u{1F527}"; // 🔧
60
62
  }
61
63
 
62
64
  // ---------------------------------------------------------------------------
63
65
  // Card builders
64
66
  // ---------------------------------------------------------------------------
65
67
 
66
- // CardKit schema 2.0 æ€č€ƒåĄį‰‡īŧˆå¸Ļ停æ­ĸ按钎īŧŒæ”¯æŒæ‰“å­—æœēæĩåŧæ›´æ–°īŧ‰
67
- export function buildThinkingCardV2(
68
- thinkingText: string,
68
+ // CardKit schema 2.0 čŋ›åēĻåĄį‰‡īŧˆå¸Ļ停æ­ĸ按钎īŧŒæ”¯æŒæĩåŧæ›´æ–°īŧ‰
69
+ export function buildProgressCard(
70
+ text: string,
69
71
  opts: { showStop?: boolean; headerTitle?: string; headerTemplate?: string } = {}
70
72
  ): string {
71
- const { showStop = true, headerTitle = "æ€č€ƒä¸­...", headerTemplate = "blue" } = opts;
73
+ const { showStop = true, headerTitle = "į”Ÿæˆä¸­...", headerTemplate = "blue" } = opts;
72
74
  const elements: object[] = [
73
- { tag: "markdown", content: truncateContent(thinkingText), element_id: "main_content" },
75
+ { tag: "markdown", content: truncateContent(text), element_id: "main_content" },
74
76
  ];
75
77
  if (showStop) {
76
78
  elements.push({ tag: "hr" });
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * session ID, resumes the session via SDK, sends the user's text, and
14
14
  * streams the response to the session's jsonl file.
15
15
  *
16
- * Buttons: thinking cards have a 停æ­ĸ button; help messages have /new and /restart buttons.
16
+ * Buttons: progress cards have a 停æ­ĸ button; help messages have /new and /restart buttons.
17
17
  *
18
18
  * Usage:
19
19
  * npm run dev
@@ -67,7 +67,7 @@ import {
67
67
  verifyAllPermissions,
68
68
  reportPermissionResults,
69
69
  } from "./feishu-api.ts";
70
- import { buildHelpCard, buildStatusCard, buildThinkingCardV2, buildCdContent, buildSessionsCard } from "./cards.ts";
70
+ import { buildHelpCard, buildStatusCard, buildProgressCard, buildCdContent, buildSessionsCard } from "./cards.ts";
71
71
  import { updateCardKitCard } from "./cardkit.ts";
72
72
  import {
73
73
  MAX_PROCESSED,
@@ -420,9 +420,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
420
420
  await new Promise(r => setTimeout(r, 20));
421
421
  }
422
422
  const cardId = existing.cardId;
423
- const thinking = existing.accumulatedThinking;
424
- const interruptedCard = buildThinkingCardV2(
425
- thinking || "æ–°é—Žéĸ˜åˇ˛æäē¤īŧŒåŊ“å‰å›žå¤åˇ˛ä¸­æ–­ã€‚",
423
+ const currentContent = existing.accumulatedContent;
424
+ const interruptedCard = buildProgressCard(
425
+ currentContent || "æ–°é—Žéĸ˜åˇ˛æäē¤īŧŒåŊ“å‰å›žå¤åˇ˛ä¸­æ–­ã€‚",
426
426
  { showStop: false, headerTitle: "åˇ˛ä¸­æ–­", headerTemplate: "yellow" }
427
427
  );
428
428
  let nextSeq = existing.sequence + 1;
package/src/session.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  isSdkAnthropicDefault,
10
10
  ts,
11
11
  } from "./config.ts";
12
- import { buildThinkingCardV2, getToolEmoji, truncateContent } from "./cards.ts";
12
+ import { buildProgressCard, getToolEmoji, truncateContent } from "./cards.ts";
13
13
  import {
14
14
  createCardKitCard,
15
15
  sendCardKitMessage,
@@ -30,7 +30,7 @@ export const chatSessionMap = new Map<string, {
30
30
  close: () => void;
31
31
  cardId: string | null;
32
32
  stopped: boolean;
33
- accumulatedThinking: string;
33
+ accumulatedContent: string;
34
34
  finalText: string;
35
35
  spinnerTimer: ReturnType<typeof setInterval> | null;
36
36
  msgTimestamp: number;
@@ -132,7 +132,7 @@ export async function resumeAndPrompt(
132
132
  close: () => session.close(),
133
133
  cardId: null,
134
134
  stopped: false,
135
- accumulatedThinking: "",
135
+ accumulatedContent: "",
136
136
  finalText: "",
137
137
  spinnerTimer: null,
138
138
  msgTimestamp,
@@ -155,7 +155,7 @@ export async function resumeAndPrompt(
155
155
 
156
156
  await session.send(userText);
157
157
 
158
- cardId = await createCardKitCard(token, buildThinkingCardV2("", { showStop: true, headerTitle: "į”Ÿæˆä¸­..." })).catch((err) => {
158
+ cardId = await createCardKitCard(token, buildProgressCard("", { showStop: true, headerTitle: "į”Ÿæˆä¸­..." })).catch((err) => {
159
159
  console.error(`[${ts()}] [CARDIKT] createCard FAIL: chatId=${chatId} ${(err as Error).message}`);
160
160
  fileLog.flush();
161
161
  sendTextReply(token, chatId, "âš ī¸ æĩåŧåĄį‰‡åˆ›åģēå¤ąč´Ĩīŧˆå¯čƒŊ因限æĩīŧ‰īŧŒå°†äŊŋį”¨æ–‡æœŦ回复。").catch(() => {});
@@ -178,8 +178,8 @@ export async function resumeAndPrompt(
178
178
 
179
179
  const stream = session.stream();
180
180
 
181
- let thinkingCount = 0;
182
- let accumulatedThinking = "";
181
+ let chunkCount = 0;
182
+ let accumulatedContent = "";
183
183
  let finalText = "";
184
184
 
185
185
  let cardCreatedAt = Date.now();
@@ -201,11 +201,11 @@ export async function resumeAndPrompt(
201
201
  try {
202
202
  // 1. ᔍåŊ“å‰į´¯į§¯å†…åŽšæ›´æ–°æ—§åĄį‰‡ä¸ēé™æ€į‰ˆæœŦ
203
203
  const oldSeqBase = cEntry.sequence;
204
- const oldDisplay = truncateContent(accumulatedThinking + finalText) || "处ᐆ䏭...";
205
- const oldCard = buildThinkingCardV2(oldDisplay, { showStop: false, headerTitle: "į”Ÿæˆä¸­...īŧˆä¸ŠčŊŽīŧ‰" });
204
+ const oldDisplay = truncateContent(accumulatedContent + finalText) || "处ᐆ䏭...";
205
+ const oldCard = buildProgressCard(oldDisplay, { showStop: false, headerTitle: "į”Ÿæˆä¸­...īŧˆä¸ŠčŊŽīŧ‰" });
206
206
  await updateCardKitCard(token, oldCardId!, oldCard, oldSeqBase + 1).catch(() => {});
207
207
  // 2. 创åģēæ–°åĄį‰‡åšļ发送
208
- const newCardId = await createCardKitCard(token, buildThinkingCardV2("", { showStop: true, headerTitle: "į”Ÿæˆä¸­..." }));
208
+ const newCardId = await createCardKitCard(token, buildProgressCard("", { showStop: true, headerTitle: "į”Ÿæˆä¸­..." }));
209
209
  if (!newCardId) throw new Error("createCardKitCard returned empty");
210
210
  await sendCardKitMessage(token, chatId, newCardId);
211
211
  // 3. 切æĸåˆ°æ–°åĄį‰‡
@@ -225,21 +225,21 @@ export async function resumeAndPrompt(
225
225
  }
226
226
 
227
227
  dotCount = (dotCount % 9) + 1;
228
- const content = truncateContent(accumulatedThinking + finalText + "\n" + "。".repeat(dotCount));
228
+ const content = truncateContent(accumulatedContent + finalText + "\n" + "。".repeat(dotCount));
229
229
  if (content === lastSentContent) return;
230
230
 
231
231
  lastSentContent = content;
232
232
  cEntry.cardBusy = true;
233
233
  const mySeq = cEntry.sequence + 1;
234
234
  try {
235
- const card = buildThinkingCardV2(content, { showStop: true, headerTitle: "į”Ÿæˆä¸­..." });
235
+ const card = buildProgressCard(content, { showStop: true, headerTitle: "į”Ÿæˆä¸­..." });
236
236
  await updateCardKitCard(token, cardId!, card, mySeq);
237
237
  cEntry.sequence = mySeq;
238
- cEntry.accumulatedThinking = accumulatedThinking;
238
+ cEntry.accumulatedContent = accumulatedContent;
239
239
  streamErrorNotified = false;
240
240
  healthLogTicks++;
241
241
  if (healthLogTicks % 10 === 0) {
242
- console.log(`[${ts()}] [CARDIKT] update health: seq=${mySeq} thinking=${accumulatedThinking.length}chars text=${finalText.length}chars cardAge=${Math.round((Date.now() - cardCreatedAt) / 1000)}s`);
242
+ console.log(`[${ts()}] [CARDIKT] update health: seq=${mySeq} content=${accumulatedContent.length}chars text=${finalText.length}chars cardAge=${Math.round((Date.now() - cardCreatedAt) / 1000)}s`);
243
243
  }
244
244
  } catch (err) {
245
245
  console.error(`[${ts()}] CardKit update error: chatId=${chatId} cardId=${cardId} seq=${mySeq} ${(err as Error).message}`);
@@ -266,14 +266,14 @@ export async function resumeAndPrompt(
266
266
  for (const block of sdkMsg.message.content) {
267
267
 
268
268
  if (block.type === "thinking" && block.thinking) {
269
- thinkingCount++;
270
- accumulatedThinking += block.thinking;
269
+ chunkCount++;
270
+ accumulatedContent += block.thinking;
271
271
  } else if (block.type === "tool_use") {
272
272
  const toolName = (block as { name?: string }).name ?? "unknown";
273
273
  const toolInput = (block as { input?: unknown }).input;
274
274
  const inputStr = typeof toolInput === "object" ? JSON.stringify(toolInput) : String(toolInput ?? "");
275
275
  const shortInput = inputStr.length > 300 ? inputStr.slice(0, 300) + "..." : inputStr;
276
- accumulatedThinking += `\n\n${getToolEmoji(toolName)} **${toolName}**\n\`${shortInput}\`\n`;
276
+ accumulatedContent += `\n\n${getToolEmoji(toolName)} **${toolName}**\n\`${shortInput}\`\n`;
277
277
  } else if (block.type === "tool_result") {
278
278
  const toolUseId = (block as { tool_use_id?: string }).tool_use_id ?? "";
279
279
  const resultContent = (block as { content?: unknown }).content;
@@ -288,12 +288,12 @@ export async function resumeAndPrompt(
288
288
  const shortResult = resultStr.length > 200 ? resultStr.slice(0, 200) + "..." : resultStr;
289
289
  const isError = (block as { is_error?: boolean }).is_error;
290
290
  const icon = isError ? "❌" : "✅";
291
- accumulatedThinking += `${icon} *${toolUseId.slice(-6)}*: ${shortResult}\n`;
291
+ accumulatedContent += `${icon} *${toolUseId.slice(-6)}*: ${shortResult}\n`;
292
292
  } else if (block.type === "redacted_thinking") {
293
- accumulatedThinking += "\n\nâš ī¸ æ€č€ƒå†…åŽščĸĢ厉全čŋ‡æģ¤\n";
293
+ accumulatedContent += "\n\nâš ī¸ 内厚čĸĢ厉全čŋ‡æģ¤\n";
294
294
  } else if (block.type === "search_result") {
295
295
  const searchQuery = (block as { query?: string }).query ?? "";
296
- accumulatedThinking += `\n\n🔍 联įŊ‘搜į´ĸ: **${searchQuery}**\n`;
296
+ accumulatedContent += `\n\n🔍 联įŊ‘搜į´ĸ: **${searchQuery}**\n`;
297
297
  } else if (block.type === "text" && block.text) {
298
298
  finalText += block.text;
299
299
  const entry = chatSessionMap.get(chatId);
@@ -304,7 +304,7 @@ export async function resumeAndPrompt(
304
304
  const compactMeta = (sdkMsg as { compact_metadata?: { trigger?: string; pre_tokens?: number; post_tokens?: number } }).compact_metadata;
305
305
  if (compactMeta) {
306
306
  const triggerLabel = compactMeta.trigger === "manual" ? "手动" : "č‡Ē动";
307
- accumulatedThinking += `\n\n🔄 上下文压įŧŠ(${triggerLabel}): **${compactMeta.pre_tokens}** → **${compactMeta.post_tokens}** tokens\n`;
307
+ accumulatedContent += `\n\n🔄 上下文压įŧŠ(${triggerLabel}): **${compactMeta.pre_tokens}** → **${compactMeta.post_tokens}** tokens\n`;
308
308
  // 更新持䚅化上下文 token 数
309
309
  if (compactMeta.post_tokens) {
310
310
  const info = sessionInfoMap.get(chatId);
@@ -325,19 +325,19 @@ export async function resumeAndPrompt(
325
325
  const wasStopped = cEntry.stopped;
326
326
  chatSessionMap.delete(chatId);
327
327
 
328
- if (cardId && accumulatedThinking) {
328
+ if (cardId && accumulatedContent) {
329
329
  while (cEntry.cardBusy) {
330
330
  await new Promise(r => setTimeout(r, 20));
331
331
  }
332
332
  const nextSeq = cEntry.sequence + 1;
333
333
  if (wasStopped) {
334
- const stopCard = buildThinkingCardV2(accumulatedThinking || "厞停æ­ĸ", { showStop: false, headerTitle: "厞停æ­ĸ", headerTemplate: "red" });
334
+ const stopCard = buildProgressCard(accumulatedContent || "厞停æ­ĸ", { showStop: false, headerTitle: "厞停æ­ĸ", headerTemplate: "red" });
335
335
  await updateCardKitCard(token, cardId, stopCard, nextSeq).catch((err) => {
336
336
  console.error(`[${ts()}] CardKit finalize: chatId=${chatId} cardId=${cardId} ${(err as Error).message}`);
337
337
  fileLog.flush();
338
338
  });
339
339
  } else {
340
- const doneCard = buildThinkingCardV2(accumulatedThinking, { showStop: false, headerTitle: "厌成" });
340
+ const doneCard = buildProgressCard(accumulatedContent, { showStop: false, headerTitle: "厌成" });
341
341
  await updateCardKitCard(token, cardId, doneCard, nextSeq).catch((err) => {
342
342
  console.error(`[${ts()}] CardKit finalize: chatId=${chatId} cardId=${cardId} ${(err as Error).message}`);
343
343
  fileLog.flush();
@@ -353,16 +353,16 @@ export async function resumeAndPrompt(
353
353
  console.error(`[${ts()}] Failed to send partial text: ${(err as Error).message}`)
354
354
  );
355
355
  }
356
- console.log(`[${ts()}] Session ${sessionId} stopped by user (thinking chunks: ${thinkingCount})`);
356
+ console.log(`[${ts()}] Session ${sessionId} stopped by user (content chunks: ${chunkCount})`);
357
357
  return;
358
358
  }
359
359
 
360
360
  if (streamErrorNotified) {
361
361
  // CardKit streaming failed — send everything as text to ensure user sees the result
362
- if (accumulatedThinking.trim()) {
363
- const shortThinking = truncateContent(accumulatedThinking, 30, 4000);
364
- await sendTextReply(token, chatId, `[æ€č€ƒčŋ‡į¨‹]\n${shortThinking}`).catch((err) =>
365
- console.error(`[${ts()}] Failed to send thinking fallback: ${(err as Error).message}`)
362
+ if (accumulatedContent.trim()) {
363
+ const shortContent = truncateContent(accumulatedContent, 30, 4000);
364
+ await sendTextReply(token, chatId, `[į”Ÿæˆčŋ‡į¨‹]\n${shortContent}`).catch((err) =>
365
+ console.error(`[${ts()}] Failed to send content fallback: ${(err as Error).message}`)
366
366
  );
367
367
  }
368
368
  if (finalText.trim()) {
@@ -376,15 +376,15 @@ export async function resumeAndPrompt(
376
376
  await sendTextReply(token, chatId, finalText.trim()).catch((err) =>
377
377
  console.error(`[${ts()}] Failed to send final text: ${(err as Error).message}`)
378
378
  );
379
- } else if (!cardId && accumulatedThinking.trim()) {
380
- const shortThinking = truncateContent(accumulatedThinking, 30, 4000);
381
- await sendTextReply(token, chatId, `[æ€č€ƒčŋ‡į¨‹]\n${shortThinking}`).catch((err) =>
382
- console.error(`[${ts()}] Failed to send thinking text: ${(err as Error).message}`)
379
+ } else if (!cardId && accumulatedContent.trim()) {
380
+ const shortContent = truncateContent(accumulatedContent, 30, 4000);
381
+ await sendTextReply(token, chatId, `[į”Ÿæˆčŋ‡į¨‹]\n${shortContent}`).catch((err) =>
382
+ console.error(`[${ts()}] Failed to send content text: ${(err as Error).message}`)
383
383
  );
384
384
  }
385
385
  }
386
386
 
387
- console.log(`[${ts()}] Session ${sessionId} stream complete (thinking chunks: ${thinkingCount})`);
387
+ console.log(`[${ts()}] Session ${sessionId} stream complete (content chunks: ${chunkCount})`);
388
388
  }
389
389
 
390
390
  // ---------------------------------------------------------------------------
@@ -415,7 +415,7 @@ export function getSessionStatus(chatId: string): SessionStatus | null {
415
415
  startTime: info.startTime,
416
416
  model: anthropicConfigDisplay(info.model),
417
417
  effort: anthropicConfigDisplay(info.effort),
418
- accumulatedLength: active ? active.accumulatedThinking.length + active.finalText.length : 0,
418
+ accumulatedLength: active ? active.accumulatedContent.length + active.finalText.length : 0,
419
419
  };
420
420
  }
421
421