chatccc 0.2.5 → 0.2.6
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 +39 -14
- package/package.json +1 -1
- package/src/__tests__/cards.test.ts +122 -5
- package/src/__tests__/claude-adapter.test.ts +2 -2
- package/src/__tests__/cursor-adapter.test.ts +250 -0
- package/src/__tests__/session.test.ts +2 -1
- package/src/adapters/claude-adapter.ts +3 -2
- package/src/adapters/cursor-adapter.ts +229 -0
- package/src/cards.ts +121 -22
- package/src/config.ts +62 -4
- package/src/feishu-api.ts +22 -8
- package/src/index.ts +107 -32
- package/src/session.ts +81 -18
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// cursor-adapter.ts — Cursor Agent CLI 适配器
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// 通过 agent -p --output-format stream-json 与 Cursor agent 交互。
|
|
5
|
+
// 命令行可通过 CHATCCC_CURSOR_COMMAND / CHATCCC_CURSOR_ARGS 环境变量自定义。
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
9
|
+
import { createInterface } from "node:readline";
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
ToolAdapter,
|
|
13
|
+
UnifiedBlock,
|
|
14
|
+
UnifiedStreamMessage,
|
|
15
|
+
CreateSessionResult,
|
|
16
|
+
SessionInfo,
|
|
17
|
+
} from "./adapter-interface.ts";
|
|
18
|
+
import { CURSOR_AGENT_COMMAND, CURSOR_AGENT_ARGS } from "../config.ts";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// 类型:Cursor JSONL 消息行
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
interface CursorMessageLine {
|
|
25
|
+
type?: string;
|
|
26
|
+
subtype?: string;
|
|
27
|
+
session_id?: string;
|
|
28
|
+
cwd?: string;
|
|
29
|
+
model?: string;
|
|
30
|
+
message?: {
|
|
31
|
+
role?: string;
|
|
32
|
+
content?: CursorContentBlock[];
|
|
33
|
+
};
|
|
34
|
+
result?: string;
|
|
35
|
+
duration_ms?: number;
|
|
36
|
+
usage?: {
|
|
37
|
+
inputTokens?: number;
|
|
38
|
+
outputTokens?: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface CursorContentBlock {
|
|
43
|
+
type?: string;
|
|
44
|
+
text?: string;
|
|
45
|
+
thinking?: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
input?: unknown;
|
|
48
|
+
tool_use_id?: string;
|
|
49
|
+
content?: unknown;
|
|
50
|
+
is_error?: boolean;
|
|
51
|
+
query?: string;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// normalizeCursorMessage — Cursor 消息 → UnifiedStreamMessage | null
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export function normalizeCursorMessage(
|
|
60
|
+
msg: CursorMessageLine,
|
|
61
|
+
): UnifiedStreamMessage | null {
|
|
62
|
+
if (msg.type === "assistant" && msg.message?.content) {
|
|
63
|
+
const blocks: UnifiedBlock[] = [];
|
|
64
|
+
for (const block of msg.message.content) {
|
|
65
|
+
if (block.type === "thinking" && block.thinking) {
|
|
66
|
+
blocks.push({ type: "thinking", thinking: block.thinking });
|
|
67
|
+
} else if (block.type === "tool_use") {
|
|
68
|
+
blocks.push({
|
|
69
|
+
type: "tool_use",
|
|
70
|
+
name: block.name ?? "unknown",
|
|
71
|
+
input: block.input,
|
|
72
|
+
});
|
|
73
|
+
} else if (block.type === "tool_result") {
|
|
74
|
+
blocks.push({
|
|
75
|
+
type: "tool_result",
|
|
76
|
+
tool_use_id: block.tool_use_id ?? "",
|
|
77
|
+
content: block.content,
|
|
78
|
+
is_error: block.is_error,
|
|
79
|
+
});
|
|
80
|
+
} else if (block.type === "redacted_thinking") {
|
|
81
|
+
blocks.push({ type: "redacted_thinking" });
|
|
82
|
+
} else if (block.type === "search_result") {
|
|
83
|
+
blocks.push({
|
|
84
|
+
type: "search_result",
|
|
85
|
+
query: block.query ?? "",
|
|
86
|
+
});
|
|
87
|
+
} else if (block.type === "text" && block.text) {
|
|
88
|
+
blocks.push({ type: "text", text: block.text });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { type: "assistant", blocks };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (msg.type === "user" && msg.message?.content) {
|
|
95
|
+
const blocks: UnifiedBlock[] = [];
|
|
96
|
+
for (const block of msg.message.content) {
|
|
97
|
+
if (block.type === "tool_result") {
|
|
98
|
+
blocks.push({
|
|
99
|
+
type: "tool_result",
|
|
100
|
+
tool_use_id: block.tool_use_id ?? "",
|
|
101
|
+
content: block.content,
|
|
102
|
+
is_error: block.is_error,
|
|
103
|
+
});
|
|
104
|
+
} else if (block.type === "text" && block.text) {
|
|
105
|
+
blocks.push({ type: "text", text: block.text });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { type: "user", blocks };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (msg.type === "system" && msg.subtype === "compact_boundary") {
|
|
112
|
+
const meta = (msg as Record<string, unknown>).compact_metadata as
|
|
113
|
+
| { trigger?: "manual" | "auto"; pre_tokens?: number; post_tokens?: number }
|
|
114
|
+
| undefined;
|
|
115
|
+
if (!meta) return null;
|
|
116
|
+
return {
|
|
117
|
+
type: "system",
|
|
118
|
+
blocks: [
|
|
119
|
+
{
|
|
120
|
+
type: "compact_boundary",
|
|
121
|
+
trigger: meta.trigger ?? "auto",
|
|
122
|
+
pre_tokens: meta.pre_tokens ?? 0,
|
|
123
|
+
post_tokens: meta.post_tokens,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// 子进程辅助函数
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
function spawnAgent(
|
|
137
|
+
extraArgs: string[],
|
|
138
|
+
cwd?: string,
|
|
139
|
+
): ChildProcess {
|
|
140
|
+
const allArgs = [...CURSOR_AGENT_ARGS, ...extraArgs];
|
|
141
|
+
return spawn(CURSOR_AGENT_COMMAND, allArgs, {
|
|
142
|
+
cwd,
|
|
143
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
144
|
+
windowsHide: true,
|
|
145
|
+
shell: true,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function* readJsonLines(
|
|
150
|
+
proc: ChildProcess,
|
|
151
|
+
signal?: AbortSignal,
|
|
152
|
+
): AsyncGenerator<CursorMessageLine> {
|
|
153
|
+
const rl = createInterface({ input: proc.stdout!, crlfDelay: Infinity });
|
|
154
|
+
for await (const line of rl) {
|
|
155
|
+
if (signal?.aborted) break;
|
|
156
|
+
const trimmed = line.trim();
|
|
157
|
+
if (!trimmed) continue;
|
|
158
|
+
try {
|
|
159
|
+
yield JSON.parse(trimmed) as CursorMessageLine;
|
|
160
|
+
} catch { /* 非 JSON 行静默跳过 */ }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// 适配器实现
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
class CursorAdapter implements ToolAdapter {
|
|
169
|
+
readonly displayName = "Cursor";
|
|
170
|
+
readonly sessionDescPrefix = "Cursor Session:";
|
|
171
|
+
private activeProcs = new Set<ChildProcess>();
|
|
172
|
+
|
|
173
|
+
async createSession(cwd: string): Promise<CreateSessionResult> {
|
|
174
|
+
const proc = spawnAgent(["ok"], cwd);
|
|
175
|
+
this.activeProcs.add(proc);
|
|
176
|
+
|
|
177
|
+
for await (const msg of readJsonLines(proc)) {
|
|
178
|
+
if (msg.type === "system" && msg.subtype === "init" && msg.session_id) {
|
|
179
|
+
const sessionId = msg.session_id;
|
|
180
|
+
this.activeProcs.delete(proc);
|
|
181
|
+
proc.kill();
|
|
182
|
+
return { sessionId };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
proc.kill();
|
|
187
|
+
this.activeProcs.delete(proc);
|
|
188
|
+
throw new Error("No session ID in Cursor init event");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async *prompt(
|
|
192
|
+
sessionId: string,
|
|
193
|
+
userText: string,
|
|
194
|
+
cwd: string,
|
|
195
|
+
signal?: AbortSignal,
|
|
196
|
+
): AsyncIterable<UnifiedStreamMessage> {
|
|
197
|
+
const proc = spawnAgent(["--resume", sessionId, userText], cwd);
|
|
198
|
+
this.activeProcs.add(proc);
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
for await (const raw of readJsonLines(proc, signal)) {
|
|
202
|
+
if (signal?.aborted) break;
|
|
203
|
+
const normalized = normalizeCursorMessage(raw);
|
|
204
|
+
if (normalized) yield normalized;
|
|
205
|
+
}
|
|
206
|
+
} finally {
|
|
207
|
+
proc.kill();
|
|
208
|
+
this.activeProcs.delete(proc);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async getSessionInfo(
|
|
213
|
+
_sessionId: string,
|
|
214
|
+
): Promise<SessionInfo | undefined> {
|
|
215
|
+
return { sessionId: _sessionId };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async closeSession(_sessionId: string): Promise<void> {
|
|
219
|
+
// 子进程由 prompt 的 finally 自动 kill
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// 工厂函数
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
export function createCursorAdapter(): ToolAdapter {
|
|
228
|
+
return new CursorAdapter();
|
|
229
|
+
}
|
package/src/cards.ts
CHANGED
|
@@ -114,10 +114,12 @@ export function buildHelpCard(userText: string): string {
|
|
|
114
114
|
header: { template: "blue", title: { content: "ChatCCC", tag: "plain_text" } },
|
|
115
115
|
elements: [
|
|
116
116
|
{ tag: "div", text: { tag: "lark_md", content: `你发送了: ${userText}` } },
|
|
117
|
+
{ tag: "div", text: { tag: "lark_md", content: "使用 **/new**(默认 Claude Code)或 **/new claude** / **/new cursor** 创建新会话" } },
|
|
117
118
|
buildButtons([
|
|
118
|
-
{ text: "
|
|
119
|
-
{ text: "
|
|
120
|
-
{ text: "
|
|
119
|
+
{ text: "新建 Claude Code 会话(/new claude)", value: JSON.stringify({ cmd: "new" }), type: "primary" },
|
|
120
|
+
{ text: "新建 Cursor 会话(/new cursor)", value: JSON.stringify({ cmd: "new cursor" }), type: "primary" },
|
|
121
|
+
{ text: "重启 ChatCCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
|
|
122
|
+
{ text: "查看/切换工作路径及最近使用(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
|
|
121
123
|
]),
|
|
122
124
|
],
|
|
123
125
|
});
|
|
@@ -159,37 +161,134 @@ export function buildCdContent(
|
|
|
159
161
|
return lines.join("\n");
|
|
160
162
|
}
|
|
161
163
|
|
|
162
|
-
//
|
|
164
|
+
// 工作路径卡片(/cd 无参数时使用,含最近使用路径按钮)
|
|
165
|
+
export function buildCdCard(
|
|
166
|
+
dirPath: string,
|
|
167
|
+
entries: { name: string; isDir: boolean }[],
|
|
168
|
+
recentDirs: string[],
|
|
169
|
+
sessionCwd?: string,
|
|
170
|
+
maxFiles = 100,
|
|
171
|
+
): string {
|
|
172
|
+
const display = entries.slice(0, maxFiles);
|
|
173
|
+
const overflow = entries.length > maxFiles ? `\n...(共 ${entries.length} 个条目,仅显示前 ${maxFiles} 个)` : "";
|
|
174
|
+
const listing = display.map(e => e.isDir ? `📁 ${e.name}/` : `📄 ${e.name}`).join("\n");
|
|
175
|
+
|
|
176
|
+
const currentLine = sessionCwd
|
|
177
|
+
? `**当前会话工作路径:** \`${sessionCwd}\``
|
|
178
|
+
: "";
|
|
179
|
+
|
|
180
|
+
const elements: object[] = [];
|
|
181
|
+
|
|
182
|
+
if (currentLine) {
|
|
183
|
+
elements.push({ tag: "markdown", content: currentLine });
|
|
184
|
+
}
|
|
185
|
+
elements.push({ tag: "markdown", content: `**新会话默认工作路径:** \`${dirPath}\`` });
|
|
186
|
+
|
|
187
|
+
if (recentDirs.length > 0) {
|
|
188
|
+
elements.push({ tag: "hr" });
|
|
189
|
+
elements.push({ tag: "markdown", content: "**最近使用过的路径(点击切换):**" });
|
|
190
|
+
elements.push({
|
|
191
|
+
tag: "action",
|
|
192
|
+
actions: recentDirs.map(d => {
|
|
193
|
+
const name = d.split(/[\\/]/).filter(Boolean).pop() ?? d;
|
|
194
|
+
const label = d.length > 36 ? `...${d.slice(-33)}` : d;
|
|
195
|
+
return {
|
|
196
|
+
tag: "button",
|
|
197
|
+
text: { tag: "plain_text", content: label },
|
|
198
|
+
type: "default",
|
|
199
|
+
value: { action: "cd", path: d },
|
|
200
|
+
};
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
elements.push({ tag: "hr" });
|
|
206
|
+
elements.push({
|
|
207
|
+
tag: "markdown",
|
|
208
|
+
content: [
|
|
209
|
+
`**目录内容** (最多 ${maxFiles} 个):`,
|
|
210
|
+
listing,
|
|
211
|
+
overflow,
|
|
212
|
+
].join("\n"),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return JSON.stringify({
|
|
216
|
+
schema: "2.0",
|
|
217
|
+
config: { wide_screen_mode: true },
|
|
218
|
+
header: {
|
|
219
|
+
template: "blue",
|
|
220
|
+
title: { tag: "plain_text", content: "工作路径" },
|
|
221
|
+
},
|
|
222
|
+
body: {
|
|
223
|
+
direction: "vertical",
|
|
224
|
+
elements,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 所有会话列表卡片(Claude Code 优先,然后 Cursor)
|
|
163
230
|
export function buildSessionsCard(sessions: Array<{
|
|
164
231
|
sessionId: string;
|
|
165
232
|
active: boolean;
|
|
166
233
|
turnCount: number;
|
|
167
234
|
elapsedSeconds: number | null;
|
|
168
235
|
model: string;
|
|
236
|
+
tool: string;
|
|
169
237
|
}>): string {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
238
|
+
// 按 tool 分组排序:Claude Code 在前,Cursor 在后
|
|
239
|
+
const claudeCodeSessions = sessions.filter(s => s.tool !== "cursor");
|
|
240
|
+
const cursorSessions = sessions.filter(s => s.tool === "cursor");
|
|
241
|
+
const hasClaudeCode = claudeCodeSessions.length > 0;
|
|
242
|
+
const hasCursor = cursorSessions.length > 0;
|
|
243
|
+
|
|
244
|
+
if (sessions.length === 0) {
|
|
245
|
+
return JSON.stringify({
|
|
246
|
+
config: { wide_screen_mode: true },
|
|
247
|
+
header: { template: "blue", title: { content: "所有会话", tag: "plain_text" } },
|
|
248
|
+
elements: [
|
|
249
|
+
{ tag: "div", text: { tag: "lark_md", content: "当前没有会话记录。\n\n使用 **/new**(默认 Claude Code)、**/new claude** 或 **/new cursor** 创建新会话。" } },
|
|
250
|
+
{ tag: "hr" },
|
|
251
|
+
{ tag: "action", actions: [{ tag: "button", text: { tag: "plain_text", content: "收起" }, type: "default", value: { action: "close" } }] },
|
|
252
|
+
],
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const formatSession = (s: typeof sessions[0], i: number) => {
|
|
257
|
+
const status = s.active ? "🟢 活跃" : "⚪ 空闲";
|
|
258
|
+
const shortId = s.sessionId.length > 16 ? s.sessionId.slice(0, 16) + "..." : s.sessionId;
|
|
259
|
+
let extra = "";
|
|
260
|
+
if (s.active && s.elapsedSeconds !== null) {
|
|
261
|
+
const mins = Math.floor(s.elapsedSeconds / 60);
|
|
262
|
+
const secs = s.elapsedSeconds % 60;
|
|
263
|
+
extra = ` | 本轮: ${mins}分${secs}秒`;
|
|
264
|
+
}
|
|
265
|
+
const toolLabel = s.tool === "cursor" ? "Cursor" : "Claude Code";
|
|
266
|
+
return `**${i + 1}.** \`${shortId}\` ${status} | 工具: ${toolLabel} | 轮数: ${s.turnCount} | ${s.model}${extra}`;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const lines: string[] = [`共 **${sessions.length}** 个会话:`, ""];
|
|
270
|
+
let idx = 0;
|
|
271
|
+
|
|
272
|
+
if (hasClaudeCode) {
|
|
273
|
+
lines.push("**Claude Code 会话:**", "");
|
|
274
|
+
for (const s of claudeCodeSessions) {
|
|
275
|
+
lines.push(formatSession(s, idx++));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (hasCursor) {
|
|
280
|
+
if (hasClaudeCode) lines.push("", "**Cursor 会话:**", "");
|
|
281
|
+
else lines.push("**Cursor 会话:**", "");
|
|
282
|
+
for (const s of cursorSessions) {
|
|
283
|
+
lines.push(formatSession(s, idx++));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
187
286
|
|
|
188
287
|
return JSON.stringify({
|
|
189
288
|
config: { wide_screen_mode: true },
|
|
190
289
|
header: { template: "blue", title: { content: "所有会话", tag: "plain_text" } },
|
|
191
290
|
elements: [
|
|
192
|
-
{ tag: "div", text: { tag: "lark_md", content:
|
|
291
|
+
{ tag: "div", text: { tag: "lark_md", content: lines.join("\n") } },
|
|
193
292
|
{ tag: "hr" },
|
|
194
293
|
{
|
|
195
294
|
tag: "action",
|
package/src/config.ts
CHANGED
|
@@ -62,13 +62,56 @@ export const CLAUDE_MODEL = process.env.CHATCCC_ANTHROPIC_MODEL?.trim() || "defa
|
|
|
62
62
|
|
|
63
63
|
export const CLAUDE_EFFORT = process.env.CHATCCC_ANTHROPIC_EFFORT?.trim() || "default";
|
|
64
64
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
65
|
+
/** 探测 cursor-agent 安装路径(优先环境变量,其次 LocalAppData,最后默认 agent) */
|
|
66
|
+
function detectCursorAgent(): string {
|
|
67
|
+
if (process.env.CHATCCC_CURSOR_COMMAND?.trim()) return process.env.CHATCCC_CURSOR_COMMAND.trim();
|
|
68
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
69
|
+
if (localAppData) {
|
|
70
|
+
const defaultPath = join(localAppData, "cursor-agent", "agent.cmd");
|
|
71
|
+
if (existsSync(defaultPath)) return defaultPath;
|
|
72
|
+
}
|
|
73
|
+
return "agent";
|
|
74
|
+
}
|
|
75
|
+
export const CURSOR_AGENT_COMMAND = detectCursorAgent();
|
|
76
|
+
/** Cursor agent 参数:-p 非交互模式,stream-json 流式 JSONL 输出 */
|
|
77
|
+
export const CURSOR_AGENT_ARGS = (process.env.CHATCCC_CURSOR_ARGS?.trim() || "-p --output-format stream-json --stream-partial-output").split(/\s+/).filter(Boolean);
|
|
67
78
|
|
|
68
79
|
// 新建会话的默认工作路径(/cd 命令设置,持久化到本地文件)
|
|
69
|
-
// 该路径仅影响通过 /new
|
|
80
|
+
// 该路径仅影响通过 /new 新建的会话,不影响已有会话的 resume。
|
|
70
81
|
export const DEFAULT_CWD_FILE = join(PROJECT_ROOT, ".claude", "working_dir.txt");
|
|
71
82
|
|
|
83
|
+
/** 会话工具类型持久化文件 */
|
|
84
|
+
export const SESSIONS_FILE = join(PROJECT_ROOT, ".claude", "sessions.json");
|
|
85
|
+
|
|
86
|
+
/** 最近成功新建会话的工作路径记录(最多 10 条) */
|
|
87
|
+
export const RECENT_DIRS_FILE = join(PROJECT_ROOT, ".claude", "recent_dirs.json");
|
|
88
|
+
export const MAX_RECENT_DIRS = 10;
|
|
89
|
+
|
|
90
|
+
/** 读取最近使用过的工作路径列表(最新的在前) */
|
|
91
|
+
export async function getRecentDirs(): Promise<string[]> {
|
|
92
|
+
try {
|
|
93
|
+
const raw = await readFile(RECENT_DIRS_FILE, "utf-8");
|
|
94
|
+
const arr = JSON.parse(raw);
|
|
95
|
+
if (Array.isArray(arr)) return arr.filter((d: unknown) => typeof d === "string");
|
|
96
|
+
} catch { /* file doesn't exist or corrupted */ }
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 添加一个路径到最近使用列表(去重、限制数量、最新的在前) */
|
|
101
|
+
export async function addRecentDir(dir: string): Promise<void> {
|
|
102
|
+
const dirs = await getRecentDirs();
|
|
103
|
+
const filtered = dirs.filter(d => d !== dir);
|
|
104
|
+
filtered.unshift(dir);
|
|
105
|
+
const trimmed = filtered.slice(0, MAX_RECENT_DIRS);
|
|
106
|
+
try {
|
|
107
|
+
await mkdir(dirname(RECENT_DIRS_FILE), { recursive: true });
|
|
108
|
+
await writeFile(RECENT_DIRS_FILE, JSON.stringify(trimmed, null, 2), "utf-8");
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error(`[${ts()}] Failed to save recent_dirs.json: ${(err as Error).message}`);
|
|
111
|
+
fileLog.flush();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
72
115
|
/** 读取 /cd 设置的默认工作路径。若文件不存在或路径已失效,回退到用户主目录。 */
|
|
73
116
|
export async function getDefaultCwd(): Promise<string> {
|
|
74
117
|
try {
|
|
@@ -227,4 +270,19 @@ export function explainMissingFeishuCredentialsAndExit(): never {
|
|
|
227
270
|
process.exit(1);
|
|
228
271
|
}
|
|
229
272
|
|
|
230
|
-
|
|
273
|
+
/** 群描述中用于识别 Claude Code 会话的前缀 */
|
|
274
|
+
export const CLAUDE_SESSION_PREFIX = "Claude Code Session:";
|
|
275
|
+
/** 群描述中用于识别 Cursor 会话的前缀 */
|
|
276
|
+
export const CURSOR_SESSION_PREFIX = "Cursor Session:";
|
|
277
|
+
|
|
278
|
+
/** 根据 tool 名称返回对应的群描述前缀 */
|
|
279
|
+
export function sessionPrefixForTool(tool: string): string {
|
|
280
|
+
if (tool === "cursor") return CURSOR_SESSION_PREFIX;
|
|
281
|
+
return CLAUDE_SESSION_PREFIX;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** 根据 tool 名称返回用于状态展示的标签 */
|
|
285
|
+
export function toolDisplayName(tool: string): string {
|
|
286
|
+
if (tool === "cursor") return "Cursor";
|
|
287
|
+
return "Claude Code";
|
|
288
|
+
}
|
package/src/feishu-api.ts
CHANGED
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
BASE_URL,
|
|
8
8
|
CHAT_LOGS_DIR,
|
|
9
9
|
PROJECT_ROOT,
|
|
10
|
-
|
|
10
|
+
CLAUDE_SESSION_PREFIX,
|
|
11
|
+
CURSOR_SESSION_PREFIX,
|
|
11
12
|
ts,
|
|
12
13
|
} from "./config.ts";
|
|
13
14
|
import { buildButtons } from "./cards.ts";
|
|
@@ -219,12 +220,24 @@ export async function getChatInfo(
|
|
|
219
220
|
};
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
export function extractSessionInfo(description: string): { sessionId: string; tool: string } | null {
|
|
224
|
+
const PREFIXES: Array<{ prefix: string; tool: string }> = [
|
|
225
|
+
{ prefix: CLAUDE_SESSION_PREFIX, tool: "claude" },
|
|
226
|
+
{ prefix: CURSOR_SESSION_PREFIX, tool: "cursor" },
|
|
227
|
+
];
|
|
228
|
+
for (const { prefix, tool } of PREFIXES) {
|
|
229
|
+
const idx = description.indexOf(prefix);
|
|
230
|
+
if (idx === -1) continue;
|
|
231
|
+
const after = description.slice(idx + prefix.length).trim();
|
|
232
|
+
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);
|
|
233
|
+
if (match) return { sessionId: match[1], tool };
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** @deprecated 使用 extractSessionInfo 代替 */
|
|
222
239
|
export function extractSessionId(description: string): string | null {
|
|
223
|
-
|
|
224
|
-
if (idx === -1) return null;
|
|
225
|
-
const after = description.slice(idx + SESSION_DESC_PREFIX.length).trim();
|
|
226
|
-
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);
|
|
227
|
-
return match ? match[1] : null;
|
|
240
|
+
return extractSessionInfo(description)?.sessionId ?? null;
|
|
228
241
|
}
|
|
229
242
|
|
|
230
243
|
// ---------------------------------------------------------------------------
|
|
@@ -325,9 +338,10 @@ export async function sendRestartCard(token: string): Promise<void> {
|
|
|
325
338
|
config: { wide_screen_mode: true },
|
|
326
339
|
header: { template: "green", title: { content: "ChatCCC Started", tag: "plain_text" } },
|
|
327
340
|
elements: [
|
|
328
|
-
{ tag: "div", text: { tag: "lark_md", content: "Bot 已启动完成,可以继续使用。\n\n
|
|
341
|
+
{ tag: "div", text: { tag: "lark_md", content: "Bot 已启动完成,可以继续使用。\n\n使用 **/new**(默认 Claude Code)或 **/new claude** / **/new cursor** 创建新会话" } },
|
|
329
342
|
buildButtons([
|
|
330
|
-
{ text: "
|
|
343
|
+
{ text: "新建 Claude Code 会话(/new claude)", value: JSON.stringify({ cmd: "new" }), type: "primary" },
|
|
344
|
+
{ text: "新建 Cursor 会话(/new cursor)", value: JSON.stringify({ cmd: "new cursor" }), type: "primary" },
|
|
331
345
|
{ text: "重启Chat CCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
|
|
332
346
|
{ text: "查看/切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
|
|
333
347
|
]),
|