chatccc 0.2.113 → 0.2.115
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 +1 -1
- package/package.json +1 -1
- package/src/__tests__/card-plain-text.test.ts +1 -1
- package/src/__tests__/cards.test.ts +1 -1
- package/src/__tests__/claude-adapter.test.ts +1 -1
- package/src/__tests__/cursor-adapter.test.ts +2 -2
- package/src/__tests__/cursor-session-meta-store.test.ts +1 -1
- package/src/__tests__/orchestrator.test.ts +75 -7
- package/src/__tests__/session.test.ts +3 -3
- package/src/adapters/adapter-interface.ts +2 -2
- package/src/adapters/cursor-session-meta-store.ts +1 -1
- package/src/cards.ts +1 -1
- package/src/index.ts +3 -1
- package/src/orchestrator.ts +214 -4
- package/src/session.ts +2 -2
package/README.md
CHANGED
|
@@ -305,7 +305,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
305
305
|
| `/newh` | 重置当前会话,保留工作目录 |
|
|
306
306
|
| `/model` | 查看或切换当前会话的模型 |
|
|
307
307
|
| `/stop` | 停止当前回复 |
|
|
308
|
-
| `/
|
|
308
|
+
| `/state` | 查看当前会话状态 |
|
|
309
309
|
| `/cd` | 查看或设置当前会话工作目录 |
|
|
310
310
|
| `/sessions` | 查看所有会话状态 |
|
|
311
311
|
| `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
|
package/package.json
CHANGED
|
@@ -107,7 +107,7 @@ describe("buildProgressCard", () => {
|
|
|
107
107
|
const parsed = JSON.parse(card);
|
|
108
108
|
const buttons = parsed.body.elements.filter((e: any) => e.tag === "button");
|
|
109
109
|
expect(buttons).toHaveLength(2);
|
|
110
|
-
expect(buttons[0].text.content).toBe("查看状态(/
|
|
110
|
+
expect(buttons[0].text.content).toBe("查看状态(/state)");
|
|
111
111
|
expect(buttons[1].text.content).toBe("停止生成(/stop)");
|
|
112
112
|
});
|
|
113
113
|
|
|
@@ -369,7 +369,7 @@ describe("createClaudeAdapter", () => {
|
|
|
369
369
|
// -------------------------------------------------------------------------
|
|
370
370
|
// getSessionInfo 行为契约
|
|
371
371
|
// - cwd 决定 /git 是否可用
|
|
372
|
-
// - model 用于 /
|
|
372
|
+
// - model 用于 /state、/sessions 显示
|
|
373
373
|
// -------------------------------------------------------------------------
|
|
374
374
|
|
|
375
375
|
it("getSessionInfo: store 中无该 sessionId 时只返回 sessionId", async () => {
|
|
@@ -296,7 +296,7 @@ describe("createCursorAdapter", () => {
|
|
|
296
296
|
// -------------------------------------------------------------------------
|
|
297
297
|
// getSessionInfo 行为契约
|
|
298
298
|
// - cwd 决定 /git 是否可用
|
|
299
|
-
// - model 决定 /
|
|
299
|
+
// - model 决定 /state、/sessions 显示的是否是 Cursor 真实模型
|
|
300
300
|
// -------------------------------------------------------------------------
|
|
301
301
|
|
|
302
302
|
it("getSessionInfo: store 中无该 sessionId 时只返回 sessionId(cwd / model 都 undefined,让上层走错误分支)", async () => {
|
|
@@ -316,7 +316,7 @@ describe("createCursorAdapter", () => {
|
|
|
316
316
|
expect(info?.model).toBeUndefined();
|
|
317
317
|
});
|
|
318
318
|
|
|
319
|
-
it("getSessionInfo: store 同时有 cwd + model 时一并返回(这是 /
|
|
319
|
+
it("getSessionInfo: store 同时有 cwd + model 时一并返回(这是 /state 显示真实模型的关键)", async () => {
|
|
320
320
|
const store = createInMemoryMetaStore({
|
|
321
321
|
"sid-known": { cwd: "F:/proj/Foo", model: "Composer 2 Fast" },
|
|
322
322
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
2
|
// cursor-session-meta-store.test.ts — sessionId→{cwd,model} 持久化的护栏单测
|
|
3
3
|
// =============================================================================
|
|
4
|
-
// 行为契约(与 cursor-adapter.getSessionInfo 配合,决定 /git、/
|
|
4
|
+
// 行为契约(与 cursor-adapter.getSessionInfo 配合,决定 /git、/state、/sessions
|
|
5
5
|
// 在 Cursor 会话上是否显示正确):
|
|
6
6
|
// - 文件不存在 / 损坏 / 非法 schema 时 get 返回 undefined,不抛异常
|
|
7
7
|
// - set 部分合并:只覆盖非空字段,已有字段保持不变
|
|
@@ -10,6 +10,7 @@ const mockStreamStates = new Map<string, { status: "running" | "done" | "stopped
|
|
|
10
10
|
|
|
11
11
|
vi.mock("../im-skills.ts", () => ({
|
|
12
12
|
buildImSkillsPrompt: async () => "",
|
|
13
|
+
buildImSkillsPromptCached: async () => "",
|
|
13
14
|
exportSkillSubDocs: async () => {},
|
|
14
15
|
}));
|
|
15
16
|
|
|
@@ -59,20 +60,21 @@ import {
|
|
|
59
60
|
_setAdapterForToolForTest,
|
|
60
61
|
_setSessionRegistryFileForTest,
|
|
61
62
|
_setSessionToolsFileForTest,
|
|
63
|
+
loadSessionRegistryForBinding,
|
|
62
64
|
recordSessionRegistry,
|
|
63
65
|
resetState,
|
|
64
66
|
} from "../session.ts";
|
|
65
67
|
import { activePrompts, resetBindingState } from "../session-chat-binding.ts";
|
|
66
68
|
|
|
67
|
-
function mockPlatform(): PlatformAdapter {
|
|
69
|
+
function mockPlatform(kind: "wechat" | "feishu" = "wechat"): PlatformAdapter {
|
|
68
70
|
return {
|
|
69
|
-
kind
|
|
71
|
+
kind,
|
|
70
72
|
sendText: vi.fn(async () => true),
|
|
71
73
|
sendCard: vi.fn(async () => true),
|
|
72
74
|
sendRawCard: vi.fn(async () => true),
|
|
73
|
-
createGroup: vi.fn(async () => "
|
|
75
|
+
createGroup: vi.fn(async () => "feishu-group"),
|
|
74
76
|
updateChatInfo: vi.fn(async () => {}),
|
|
75
|
-
getChatInfo: vi.fn(async () => ({ name: "微信会话", description: "" })),
|
|
77
|
+
getChatInfo: vi.fn(async () => ({ name: kind === "wechat" ? "微信会话" : "飞书会话", description: "" })),
|
|
76
78
|
disbandChat: vi.fn(async () => {}),
|
|
77
79
|
setChatAvatar: vi.fn(async () => {}),
|
|
78
80
|
extractSessionInfo: vi.fn(() => null),
|
|
@@ -82,15 +84,15 @@ function mockPlatform(): PlatformAdapter {
|
|
|
82
84
|
};
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
function mockAdapter(): ToolAdapter {
|
|
87
|
+
function mockAdapter(sessionId = "sid-wechat", promptText = "done"): ToolAdapter {
|
|
86
88
|
return {
|
|
87
89
|
displayName: "Claude",
|
|
88
90
|
sessionDescPrefix: "Claude Session:",
|
|
89
|
-
createSession: async () => ({ sessionId
|
|
91
|
+
createSession: vi.fn(async () => ({ sessionId })),
|
|
90
92
|
prompt: async function* () {
|
|
91
93
|
yield {
|
|
92
94
|
type: "assistant",
|
|
93
|
-
blocks: [{ type: "text", text:
|
|
95
|
+
blocks: [{ type: "text", text: promptText }],
|
|
94
96
|
};
|
|
95
97
|
},
|
|
96
98
|
getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
|
|
@@ -166,4 +168,70 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
166
168
|
|
|
167
169
|
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
168
170
|
});
|
|
171
|
+
|
|
172
|
+
it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
|
|
173
|
+
const platform = mockPlatform("feishu");
|
|
174
|
+
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
175
|
+
yield {
|
|
176
|
+
type: "assistant" as const,
|
|
177
|
+
blocks: [{ type: "text" as const, text: `收到: ${userText}` }],
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
_setAdapterForToolForTest("cursor", {
|
|
181
|
+
displayName: "Claude",
|
|
182
|
+
sessionDescPrefix: "Claude Session:",
|
|
183
|
+
createSession: vi.fn(async () => ({ sessionId: "sid-feishu-new" })),
|
|
184
|
+
prompt,
|
|
185
|
+
getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
|
|
186
|
+
sessionId,
|
|
187
|
+
cwd: "F:\\repo",
|
|
188
|
+
}),
|
|
189
|
+
closeSession: async () => {},
|
|
190
|
+
});
|
|
191
|
+
await recordSessionRegistry({
|
|
192
|
+
chatId: "feishu-p2p",
|
|
193
|
+
sessionId: "stale-sid",
|
|
194
|
+
tool: "claude",
|
|
195
|
+
chatName: "旧私聊绑定",
|
|
196
|
+
running: false,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await handleCommand(platform, "帮我看一下日志", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
200
|
+
|
|
201
|
+
expect(platform.createGroup).toHaveBeenCalledWith(expect.stringContaining("帮我看一下日志"), ["ou-user"]);
|
|
202
|
+
expect(platform.updateChatInfo).toHaveBeenCalledWith(
|
|
203
|
+
"feishu-group",
|
|
204
|
+
expect.stringContaining("帮我看一下日志"),
|
|
205
|
+
expect.stringContaining("sid-feishu-new"),
|
|
206
|
+
);
|
|
207
|
+
expect(prompt).toHaveBeenCalledWith(
|
|
208
|
+
"sid-feishu-new",
|
|
209
|
+
expect.stringContaining("帮我看一下日志"),
|
|
210
|
+
"F:\\repo",
|
|
211
|
+
expect.any(AbortSignal),
|
|
212
|
+
expect.any(Object),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const registry = await loadSessionRegistryForBinding();
|
|
216
|
+
expect(registry["feishu-p2p"]).toBeUndefined();
|
|
217
|
+
expect(registry["feishu-group"]?.sessionId).toBe("sid-feishu-new");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("cleans stale Feishu p2p binding but keeps valid commands from auto-creating a group", async () => {
|
|
221
|
+
const platform = mockPlatform("feishu");
|
|
222
|
+
await recordSessionRegistry({
|
|
223
|
+
chatId: "feishu-p2p",
|
|
224
|
+
sessionId: "stale-sid",
|
|
225
|
+
tool: "claude",
|
|
226
|
+
chatName: "旧私聊绑定",
|
|
227
|
+
running: false,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await handleCommand(platform, "/model", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
231
|
+
|
|
232
|
+
expect(platform.createGroup).not.toHaveBeenCalled();
|
|
233
|
+
expect(platform.sendRawCard).toHaveBeenCalled();
|
|
234
|
+
const registry = await loadSessionRegistryForBinding();
|
|
235
|
+
expect(registry["feishu-p2p"]).toBeUndefined();
|
|
236
|
+
});
|
|
169
237
|
});
|
|
@@ -160,7 +160,7 @@ function mockSessionInfo(chatId: string, overrides: Partial<{
|
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
162
|
* 简易 mock adapter:getSessionInfo 返回固定 SessionInfo,其他方法不实现
|
|
163
|
-
* (仅 /
|
|
163
|
+
* (仅 /state、/sessions 路径会触发 getSessionInfo,无需完整接口)。
|
|
164
164
|
*/
|
|
165
165
|
function mockAdapter(getInfo: (sid: string) => SessionInfo | undefined): ToolAdapter {
|
|
166
166
|
return {
|
|
@@ -597,7 +597,7 @@ describe("getSessionStatus", () => {
|
|
|
597
597
|
});
|
|
598
598
|
|
|
599
599
|
// -------------------------------------------------------------------------
|
|
600
|
-
// model/effort 来源:按 tool 分支(核心契约——决定 /
|
|
600
|
+
// model/effort 来源:按 tool 分支(核心契约——决定 /state 显示是否真实)
|
|
601
601
|
// -------------------------------------------------------------------------
|
|
602
602
|
|
|
603
603
|
it("Claude 会话:effort 非 null(始终显示该行);model 来自全局配置", async () => {
|
|
@@ -643,7 +643,7 @@ describe("getSessionStatus", () => {
|
|
|
643
643
|
expect(status!.model).toBe(UNKNOWN_MODEL_PLACEHOLDER);
|
|
644
644
|
});
|
|
645
645
|
|
|
646
|
-
it("Cursor 会话:adapter.getSessionInfo 抛错时降级为占位符(不阻塞 /
|
|
646
|
+
it("Cursor 会话:adapter.getSessionInfo 抛错时降级为占位符(不阻塞 /state)", async () => {
|
|
647
647
|
mockSessionInfo("chat-cursor", { sessionId: "sid-cur", tool: "cursor" });
|
|
648
648
|
_setAdapterForToolForTest(
|
|
649
649
|
"cursor",
|
|
@@ -101,7 +101,7 @@ export interface CreateSessionResult {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// ---------------------------------------------------------------------------
|
|
104
|
-
// SessionInfo — 会话元数据(/
|
|
104
|
+
// SessionInfo — 会话元数据(/state、/cd 等命令使用)
|
|
105
105
|
// ---------------------------------------------------------------------------
|
|
106
106
|
|
|
107
107
|
export interface SessionInfo {
|
|
@@ -113,7 +113,7 @@ export interface SessionInfo {
|
|
|
113
113
|
* 会话实际使用的模型展示名(如 Cursor 的 `Composer 2 Fast`)。
|
|
114
114
|
* - Cursor adapter:从 cursor-agent system/init 事件学习并持久化到 store
|
|
115
115
|
* - Claude adapter:留空(model 由 ChatCCC 配置 `CLAUDE_MODEL` 决定,不从 SDK 取)
|
|
116
|
-
* 上层 /
|
|
116
|
+
* 上层 /state、/sessions 渲染时按 tool 决定显示哪一来源。
|
|
117
117
|
*/
|
|
118
118
|
model?: string;
|
|
119
119
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// 内部已持久化)。Cursor CLI 没有等价机制,因此 ChatCCC 必须自己维护一份
|
|
6
6
|
// sessionId → { cwd, model } 映射,否则:
|
|
7
7
|
// 1. /git、/cd 等需要"会话真实工作目录"的命令将在 Cursor 会话上 100% 失败
|
|
8
|
-
// 2. /
|
|
8
|
+
// 2. /state、/sessions 显示的"模型"只能硬塞 ChatCCC 的 ANTHROPIC 环境变量,
|
|
9
9
|
// 与 Cursor 实际跑的 Composer 2 Fast 等真实模型无关
|
|
10
10
|
//
|
|
11
11
|
// 存储:
|
package/src/cards.ts
CHANGED
|
@@ -89,7 +89,7 @@ export function buildProgressCard(
|
|
|
89
89
|
elements.push({ tag: "hr" });
|
|
90
90
|
elements.push({
|
|
91
91
|
tag: "button",
|
|
92
|
-
text: { tag: "plain_text", content: "查看状态(/
|
|
92
|
+
text: { tag: "plain_text", content: "查看状态(/state)" },
|
|
93
93
|
type: "default",
|
|
94
94
|
value: { action: "status" },
|
|
95
95
|
element_id: "action_status",
|
package/src/index.ts
CHANGED
|
@@ -109,6 +109,8 @@ import { createWechatAdapter, startWechatPlatform } from "./wechat-platform.ts";
|
|
|
109
109
|
function createFeishuAdapter(): PlatformAdapter {
|
|
110
110
|
const auth = () => getTenantAccessToken();
|
|
111
111
|
return {
|
|
112
|
+
kind: "feishu",
|
|
113
|
+
|
|
112
114
|
// ---- 基础消息 ----
|
|
113
115
|
async sendText(chatId, text) {
|
|
114
116
|
return sendTextReply(await auth(), chatId, text);
|
|
@@ -285,7 +287,7 @@ function parseCardAction(data: unknown): CardActionResult | null {
|
|
|
285
287
|
}
|
|
286
288
|
if (!cmd) return null;
|
|
287
289
|
|
|
288
|
-
const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/
|
|
290
|
+
const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/state", cd: "/cd", sessions: "/sessions", newh: "/newh" };
|
|
289
291
|
let text = CMD_MAP[cmd] ?? "";
|
|
290
292
|
if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
|
|
291
293
|
const path = (action.value as Record<string, string>).path;
|
package/src/orchestrator.ts
CHANGED
|
@@ -121,6 +121,43 @@ function shouldSendWechatProcessingAck(
|
|
|
121
121
|
return platform.kind === "wechat" && chatType === "p2p" && !textLower.startsWith("/");
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
function isNonWechatP2p(platform: PlatformAdapter, chatType: string): boolean {
|
|
125
|
+
return chatType === "p2p" && platform.kind !== "wechat";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function cleanupNonWechatP2pBinding(
|
|
129
|
+
platform: PlatformAdapter,
|
|
130
|
+
chatId: string,
|
|
131
|
+
chatType: string,
|
|
132
|
+
tid: string,
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
if (!isNonWechatP2p(platform, chatType)) return;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const registry = await loadSessionRegistryForBinding();
|
|
138
|
+
const record = registry[chatId];
|
|
139
|
+
if (!record?.sessionId) return;
|
|
140
|
+
|
|
141
|
+
unbindChatFromSession(record.sessionId, chatId);
|
|
142
|
+
displayCards.delete(chatId);
|
|
143
|
+
cancelQueuedMessage(record.sessionId);
|
|
144
|
+
sessionInfoMap.delete(chatId);
|
|
145
|
+
await removeSessionRegistryRecord(chatId);
|
|
146
|
+
logTrace(tid, "BRANCH", {
|
|
147
|
+
reason: "cleanup_non_wechat_p2p_binding",
|
|
148
|
+
chatId,
|
|
149
|
+
oldSessionId: record.sessionId,
|
|
150
|
+
});
|
|
151
|
+
console.log(
|
|
152
|
+
`[${ts()}] [P2P] Removed non-WeChat p2p binding: chat=${chatId} session=${record.sessionId}`,
|
|
153
|
+
);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.log(
|
|
156
|
+
`[${ts()}] [INFO] Cannot cleanup p2p registry for ${chatId}: ${(err as Error).message}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
124
161
|
// ---------------------------------------------------------------------------
|
|
125
162
|
// handleCommand — 平台无关的命令分发
|
|
126
163
|
// ---------------------------------------------------------------------------
|
|
@@ -137,6 +174,7 @@ export async function handleCommand(
|
|
|
137
174
|
const tid = traceId ?? makeTraceId();
|
|
138
175
|
const textLower = text.toLowerCase();
|
|
139
176
|
recordChatPlatform(chatId, platform);
|
|
177
|
+
await cleanupNonWechatP2pBinding(platform, chatId, chatType, tid);
|
|
140
178
|
|
|
141
179
|
if (textLower === "/restart") {
|
|
142
180
|
logTrace(tid, "BRANCH", { cmd: "/restart" });
|
|
@@ -523,8 +561,9 @@ export async function handleCommand(
|
|
|
523
561
|
`[${ts()}] [INFO] Cannot get chat info for ${chatId}: ${(err as Error).message}`,
|
|
524
562
|
);
|
|
525
563
|
}
|
|
526
|
-
} else {
|
|
527
|
-
//
|
|
564
|
+
} else if (platform.kind === "wechat") {
|
|
565
|
+
// 微信私聊:从 session-registry.json 获取绑定的 session。
|
|
566
|
+
// 飞书私聊不再作为持久会话入口,避免历史 registry 残留误路由到旧 session。
|
|
528
567
|
try {
|
|
529
568
|
const registry = await loadSessionRegistryForBinding();
|
|
530
569
|
const record = registry[chatId];
|
|
@@ -700,8 +739,8 @@ export async function handleCommand(
|
|
|
700
739
|
return;
|
|
701
740
|
}
|
|
702
741
|
|
|
703
|
-
if (textLower === "/
|
|
704
|
-
logTrace(tid, "BRANCH", { cmd: "/
|
|
742
|
+
if (textLower === "/state") {
|
|
743
|
+
logTrace(tid, "BRANCH", { cmd: "/state" });
|
|
705
744
|
const status = await getSessionStatus(chatId);
|
|
706
745
|
const isActive = isSessionRunning(sessionId);
|
|
707
746
|
const statusText = [
|
|
@@ -1245,6 +1284,177 @@ export async function handleCommand(
|
|
|
1245
1284
|
return;
|
|
1246
1285
|
}
|
|
1247
1286
|
|
|
1287
|
+
// 无会话上下文 → /sessions 仍是有效指令,不触发飞书私聊自动建群。
|
|
1288
|
+
if (textLower === "/sessions") {
|
|
1289
|
+
logTrace(tid, "BRANCH", { cmd: "/sessions", scope: "global" });
|
|
1290
|
+
const allSessions = await getAllSessionsStatus();
|
|
1291
|
+
const now = Date.now();
|
|
1292
|
+
const cardData = allSessions.map((s) => ({
|
|
1293
|
+
sessionId: s.sessionId,
|
|
1294
|
+
chatName: s.chatName,
|
|
1295
|
+
chatId: s.chatId,
|
|
1296
|
+
active: s.active,
|
|
1297
|
+
turnCount: s.turnCount,
|
|
1298
|
+
elapsedSeconds: s.active
|
|
1299
|
+
? Math.floor((now - s.startTime) / 1000)
|
|
1300
|
+
: null,
|
|
1301
|
+
model: s.model,
|
|
1302
|
+
tool: s.tool,
|
|
1303
|
+
}));
|
|
1304
|
+
const card = buildSessionsCard(cardData, { defaultToolLabel: toolDisplayName(resolveDefaultAgentTool()) });
|
|
1305
|
+
const ok = await platform.sendRawCard(chatId, card);
|
|
1306
|
+
console.log(
|
|
1307
|
+
`[${ts()}] [SESSIONS] card sent, ok=${ok}, count=${cardData.length}`,
|
|
1308
|
+
);
|
|
1309
|
+
logTrace(tid, "DONE", { outcome: "sessions", ok, count: cardData.length });
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// 飞书私聊普通消息:不再绑定私聊本身,而是自动创建会话群并把私聊内容作为首轮 prompt。
|
|
1314
|
+
if (isNonWechatP2p(platform, chatType) && !textLower.startsWith("/")) {
|
|
1315
|
+
const tool = resolveDefaultAgentTool();
|
|
1316
|
+
const toolLabel = toolDisplayName(tool);
|
|
1317
|
+
logTrace(tid, "BRANCH", { cmd: "auto_new_from_p2p", tool });
|
|
1318
|
+
|
|
1319
|
+
if (!openId) {
|
|
1320
|
+
logTrace(tid, "DONE", { outcome: "auto_new_p2p_no_openid" });
|
|
1321
|
+
await platform.sendCard(
|
|
1322
|
+
chatId,
|
|
1323
|
+
"Error",
|
|
1324
|
+
"Cannot identify sender.",
|
|
1325
|
+
"red",
|
|
1326
|
+
);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
let sessionId: string;
|
|
1331
|
+
let sessionCwd: string;
|
|
1332
|
+
try {
|
|
1333
|
+
const init = await initClaudeSession(tool, undefined, chatId);
|
|
1334
|
+
sessionId = init.sessionId;
|
|
1335
|
+
sessionCwd = init.cwd;
|
|
1336
|
+
console.log(
|
|
1337
|
+
`[${ts()}] [AUTO-P2P 1/5] ${toolLabel} session created: ${sessionId} → OK`,
|
|
1338
|
+
);
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
console.error(`[${ts()}] [AUTO-P2P 1/5] FAIL: ${(err as Error).message}`);
|
|
1341
|
+
logTrace(tid, "DONE", {
|
|
1342
|
+
outcome: "auto_new_p2p_session_fail",
|
|
1343
|
+
error: (err as Error).message,
|
|
1344
|
+
});
|
|
1345
|
+
await platform.sendCard(
|
|
1346
|
+
chatId,
|
|
1347
|
+
"Error",
|
|
1348
|
+
`Failed to initialize ${toolLabel} session:\n${(err as Error).message}`,
|
|
1349
|
+
"red",
|
|
1350
|
+
);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const cwd = sessionCwd;
|
|
1355
|
+
const initialName = sessionChatName(text.slice(0, 10) || "新会话", cwd);
|
|
1356
|
+
let newChatId: string;
|
|
1357
|
+
try {
|
|
1358
|
+
newChatId = await platform.createGroup(initialName, [openId]);
|
|
1359
|
+
console.log(
|
|
1360
|
+
`[${ts()}] [AUTO-P2P 2/5] Created Feishu group: ${newChatId} → OK`,
|
|
1361
|
+
);
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
console.error(`[${ts()}] [AUTO-P2P 2/5] FAIL: ${(err as Error).message}`);
|
|
1364
|
+
logTrace(tid, "DONE", {
|
|
1365
|
+
outcome: "auto_new_p2p_group_fail",
|
|
1366
|
+
error: (err as Error).message,
|
|
1367
|
+
});
|
|
1368
|
+
await platform.sendCard(
|
|
1369
|
+
chatId,
|
|
1370
|
+
"Error",
|
|
1371
|
+
`Failed to create group:\n${(err as Error).message}`,
|
|
1372
|
+
"red",
|
|
1373
|
+
);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
try {
|
|
1378
|
+
const descPrefix = sessionPrefixForTool(tool);
|
|
1379
|
+
await platform.updateChatInfo(
|
|
1380
|
+
newChatId,
|
|
1381
|
+
initialName,
|
|
1382
|
+
`${descPrefix} ${sessionId}`,
|
|
1383
|
+
);
|
|
1384
|
+
console.log(
|
|
1385
|
+
`[${ts()}] [AUTO-P2P 3/5] Renamed group → name="${initialName}" (${toolLabel}) → OK`,
|
|
1386
|
+
);
|
|
1387
|
+
} catch (err) {
|
|
1388
|
+
console.error(`[${ts()}] [AUTO-P2P 3/5] FAIL: ${(err as Error).message}`);
|
|
1389
|
+
logTrace(tid, "DONE", {
|
|
1390
|
+
outcome: "auto_new_p2p_rename_fail",
|
|
1391
|
+
error: (err as Error).message,
|
|
1392
|
+
});
|
|
1393
|
+
await platform.sendCard(
|
|
1394
|
+
chatId,
|
|
1395
|
+
"Error",
|
|
1396
|
+
`Group created but rename failed:\n${(err as Error).message}`,
|
|
1397
|
+
"yellow",
|
|
1398
|
+
);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
await setDefaultCwd(cwd, newChatId);
|
|
1403
|
+
bindChatToSession(sessionId, newChatId);
|
|
1404
|
+
await recordSessionRegistry({
|
|
1405
|
+
chatId: newChatId,
|
|
1406
|
+
sessionId,
|
|
1407
|
+
tool,
|
|
1408
|
+
chatName: initialName,
|
|
1409
|
+
turnCount: 0,
|
|
1410
|
+
startTime: Date.now(),
|
|
1411
|
+
running: false,
|
|
1412
|
+
});
|
|
1413
|
+
await saveSessionTool(sessionId, tool, initialName);
|
|
1414
|
+
|
|
1415
|
+
await platform.sendCard(
|
|
1416
|
+
newChatId,
|
|
1417
|
+
`${toolLabel} Session Ready`,
|
|
1418
|
+
`已根据私聊消息创建 **${toolLabel}** 会话群。\n\n` +
|
|
1419
|
+
`**Session ID:** ${sessionId}\n` +
|
|
1420
|
+
`**工作目录:** \`${cwd}\`\n\n` +
|
|
1421
|
+
`下面会自动把你的私聊消息作为第一句话发送给 ${toolLabel}。\n\n` +
|
|
1422
|
+
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
1423
|
+
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。`,
|
|
1424
|
+
"green",
|
|
1425
|
+
);
|
|
1426
|
+
platform.setChatAvatar(newChatId, tool, "new").catch(() => {});
|
|
1427
|
+
console.log(`[${ts()}] [AUTO-P2P 4/5] Replied to new group → OK`);
|
|
1428
|
+
|
|
1429
|
+
try {
|
|
1430
|
+
logTrace(tid, "RESUME", { sessionId, tool, trigger: "auto_new_from_p2p" });
|
|
1431
|
+
await resumeAndPrompt(
|
|
1432
|
+
sessionId,
|
|
1433
|
+
text,
|
|
1434
|
+
platform,
|
|
1435
|
+
newChatId,
|
|
1436
|
+
msgTimestamp,
|
|
1437
|
+
tool,
|
|
1438
|
+
tid,
|
|
1439
|
+
);
|
|
1440
|
+
console.log(`[${ts()}] [AUTO-P2P 5/5] First prompt sent → OK`);
|
|
1441
|
+
logTrace(tid, "DONE", { outcome: "auto_new_p2p_prompt_done", newChatId, sessionId, tool });
|
|
1442
|
+
} catch (err) {
|
|
1443
|
+
console.error(`[${ts()}] [AUTO-P2P 5/5] FAIL: ${(err as Error).message}`);
|
|
1444
|
+
logTrace(tid, "DONE", {
|
|
1445
|
+
outcome: "auto_new_p2p_prompt_fail",
|
|
1446
|
+
error: (err as Error).message,
|
|
1447
|
+
});
|
|
1448
|
+
await platform.sendCard(
|
|
1449
|
+
newChatId,
|
|
1450
|
+
"Error",
|
|
1451
|
+
`Failed to send first prompt:\n${(err as Error).message}`,
|
|
1452
|
+
"red",
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1248
1458
|
// 无会话上下文 → help card
|
|
1249
1459
|
logTrace(tid, "SEND", { method: "help_card", chatId });
|
|
1250
1460
|
const card = buildHelpCard(text, { defaultToolLabel: toolDisplayName(resolveDefaultAgentTool()) });
|
package/src/session.ts
CHANGED
|
@@ -1549,7 +1549,7 @@ export function stopSession(sessionId: string): boolean {
|
|
|
1549
1549
|
}
|
|
1550
1550
|
|
|
1551
1551
|
// ---------------------------------------------------------------------------
|
|
1552
|
-
// Session status query (供 /
|
|
1552
|
+
// Session status query (供 /state、/sessions 命令使用)
|
|
1553
1553
|
// ---------------------------------------------------------------------------
|
|
1554
1554
|
//
|
|
1555
1555
|
// model / effort 的来源策略(按 tool 区分,避免硬塞 ChatCCC 全局配置导致显示
|
|
@@ -1590,7 +1590,7 @@ async function resolveModelEffort(
|
|
|
1590
1590
|
const info = await adapter.getSessionInfo(sessionId);
|
|
1591
1591
|
if (info?.model) model = info.model;
|
|
1592
1592
|
} catch {
|
|
1593
|
-
// adapter 异常时降级为占位符(不阻塞 /
|
|
1593
|
+
// adapter 异常时降级为占位符(不阻塞 /state 卡片)
|
|
1594
1594
|
}
|
|
1595
1595
|
return { model, effort: null };
|
|
1596
1596
|
}
|