chatccc 0.2.118 → 0.2.120
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 +2 -2
- package/package.json +1 -1
- package/src/__tests__/orchestrator.test.ts +55 -13
- package/src/adapters/claude-adapter.ts +3 -2
- package/src/orchestrator.ts +172 -118
package/README.md
CHANGED
|
@@ -313,8 +313,8 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
313
313
|
| `/model clear` | 清除模型覆盖,恢复默认 | 全部 |
|
|
314
314
|
| `/cd` | 查看当前工作目录(含最近目录快捷按钮) | 全部 |
|
|
315
315
|
| `/cd <路径>` | 设置新建会话的默认工作目录 | 全部 |
|
|
316
|
-
| `/plan <消息>` |
|
|
317
|
-
| `/ask <消息>` |
|
|
316
|
+
| `/plan <消息>` | Claude Code 使用 CLI 只读/规划模式;其他 Agent 会收到包含 `/plan` 前缀的原始消息,可能通过消息内容自行支持 | Claude Code(CLI 模式)/ 全部(消息透传) |
|
|
317
|
+
| `/ask <消息>` | Cursor 使用 CLI 只读/问答模式;其他 Agent 会收到包含 `/ask` 前缀的原始消息,可能通过消息内容自行支持 | Cursor(CLI 模式)/ 全部(消息透传) |
|
|
318
318
|
| `/git <子命令>` | 在当前会话工作目录执行 git 命令并回传输出 | 全部 |
|
|
319
319
|
| `/deleteg` | 解散当前群聊,保留 Agent 会话 | 全部(仅群聊) |
|
|
320
320
|
| `/restart` | 重启 ChatCCC 服务进程 | 全部 |
|
package/package.json
CHANGED
|
@@ -154,25 +154,67 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
154
154
|
expect(platform.sendCard).not.toHaveBeenCalled();
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
-
it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
|
|
158
|
-
const platform = mockPlatform();
|
|
159
|
-
await recordSessionRegistry({
|
|
160
|
-
chatId: "wx-chat",
|
|
161
|
-
sessionId: "sid-wechat",
|
|
157
|
+
it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
|
|
158
|
+
const platform = mockPlatform();
|
|
159
|
+
await recordSessionRegistry({
|
|
160
|
+
chatId: "wx-chat",
|
|
161
|
+
sessionId: "sid-wechat",
|
|
162
162
|
tool: "claude",
|
|
163
163
|
chatName: "ready-session",
|
|
164
164
|
running: false,
|
|
165
165
|
});
|
|
166
166
|
|
|
167
167
|
await handleCommand(platform, "继续说明", "wx-chat", "wx-user", Date.now(), "p2p");
|
|
168
|
-
|
|
169
|
-
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it(
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
168
|
+
|
|
169
|
+
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it.each([
|
|
173
|
+
{ tool: "claude" as const, sessionId: "sid-claude", text: "/ask 只回答不要改文件" },
|
|
174
|
+
{ tool: "cursor" as const, sessionId: "sid-cursor", text: "/plan 先给我计划" },
|
|
175
|
+
])("passes unsupported mode commands through as raw prompts for $tool", async ({ tool, sessionId, text }) => {
|
|
176
|
+
const platform = mockPlatform();
|
|
177
|
+
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
178
|
+
yield {
|
|
179
|
+
type: "assistant" as const,
|
|
180
|
+
blocks: [{ type: "text" as const, text: `收到: ${userText}` }],
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
_setAdapterForToolForTest(tool, {
|
|
184
|
+
displayName: tool,
|
|
185
|
+
sessionDescPrefix: `${tool} Session:`,
|
|
186
|
+
createSession: vi.fn(async () => ({ sessionId })),
|
|
187
|
+
prompt,
|
|
188
|
+
getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
|
|
189
|
+
sessionId,
|
|
190
|
+
cwd: "F:\\repo",
|
|
191
|
+
}),
|
|
192
|
+
closeSession: async () => {},
|
|
193
|
+
});
|
|
194
|
+
await recordSessionRegistry({
|
|
195
|
+
chatId: "wx-chat",
|
|
196
|
+
sessionId,
|
|
197
|
+
tool,
|
|
198
|
+
chatName: "ready-session",
|
|
199
|
+
running: false,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await handleCommand(platform, text, "wx-chat", "wx-user", Date.now(), "p2p");
|
|
203
|
+
|
|
204
|
+
expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", expect.stringContaining("仅在以下 Agent 会话中可用"));
|
|
205
|
+
expect(prompt).toHaveBeenCalledWith(
|
|
206
|
+
sessionId,
|
|
207
|
+
expect.stringContaining(text),
|
|
208
|
+
"F:\\repo",
|
|
209
|
+
expect.any(AbortSignal),
|
|
210
|
+
expect.objectContaining({ permissionMode: undefined }),
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
|
|
215
|
+
const platform = mockPlatform("feishu");
|
|
216
|
+
const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
|
|
217
|
+
yield {
|
|
176
218
|
type: "assistant" as const,
|
|
177
219
|
blocks: [{ type: "text" as const, text: `收到: ${userText}` }],
|
|
178
220
|
};
|
|
@@ -276,14 +276,15 @@ function buildCliArgs(
|
|
|
276
276
|
extraArgs: string[],
|
|
277
277
|
permissionMode?: "plan" | "ask",
|
|
278
278
|
): string[] {
|
|
279
|
-
const permMode = permissionMode === "plan" ? "plan" : "bypassPermissions";
|
|
279
|
+
const permMode = permissionMode === "plan" ? "plan" : permissionMode === "ask" ? "default" : "bypassPermissions";
|
|
280
|
+
const skipPermissions = permissionMode !== "plan" && permissionMode !== "ask";
|
|
280
281
|
const args = [
|
|
281
282
|
"-p",
|
|
282
283
|
"--output-format", "stream-json",
|
|
283
284
|
"--verbose",
|
|
284
285
|
"--setting-sources", "user,project,local",
|
|
285
286
|
"--permission-mode", permMode,
|
|
286
|
-
"--dangerously-skip-permissions",
|
|
287
|
+
...(skipPermissions ? ["--dangerously-skip-permissions"] : []),
|
|
287
288
|
"--settings", "{\"maxTurns\":0}",
|
|
288
289
|
];
|
|
289
290
|
|
package/src/orchestrator.ts
CHANGED
|
@@ -158,6 +158,54 @@ function resolveModeCommand(cmd: string): { mode: "plan" | "ask"; supportedTools
|
|
|
158
158
|
return mode ? { mode, supportedTools } : null;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/** 如果当前群名是未命名会话("新会话" 或 "新会话-xxx"),用消息内容前缀重命名 */
|
|
162
|
+
async function renameUntitledSession(
|
|
163
|
+
platform: PlatformAdapter,
|
|
164
|
+
chatId: string,
|
|
165
|
+
chatType: string,
|
|
166
|
+
sessionId: string,
|
|
167
|
+
tool: string,
|
|
168
|
+
namePrefix: string,
|
|
169
|
+
description: string | undefined,
|
|
170
|
+
chatInfo: { name: string } | undefined,
|
|
171
|
+
): Promise<void> {
|
|
172
|
+
const MAX_PREFIX = 10;
|
|
173
|
+
const prefix = namePrefix.slice(0, MAX_PREFIX);
|
|
174
|
+
|
|
175
|
+
if (chatType !== "p2p" && chatInfo && isUntitledSessionChatName(chatInfo.name)) {
|
|
176
|
+
const adapter = getAdapterForTool(tool, sessionId);
|
|
177
|
+
const info = await adapter.getSessionInfo(sessionId).catch(() => undefined);
|
|
178
|
+
const sessionCwd = info?.cwd ?? (await getDefaultCwd(chatId));
|
|
179
|
+
const newName = sessionChatName(prefix, sessionCwd);
|
|
180
|
+
try {
|
|
181
|
+
await platform.updateChatInfo(chatId, newName, description!);
|
|
182
|
+
console.log(`[${ts()}] [RENAME] First message → group renamed to "${newName}"`);
|
|
183
|
+
await recordSessionRegistry({ chatId, sessionId, tool, chatName: newName }).catch(() => {});
|
|
184
|
+
await saveSessionTool(sessionId, tool, newName).catch(() => {});
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error(`[${ts()}] [RENAME] Failed: ${(err as Error).message}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (chatType === "p2p" && platform.kind === "wechat") {
|
|
191
|
+
try {
|
|
192
|
+
const reg = await loadSessionRegistryForBinding();
|
|
193
|
+
const rec = reg[chatId];
|
|
194
|
+
if (rec && rec.sessionId === sessionId && isUntitledSessionChatName(rec.chatName ?? "")) {
|
|
195
|
+
const adapter = getAdapterForTool(tool, sessionId);
|
|
196
|
+
const info = await adapter.getSessionInfo(sessionId).catch(() => undefined);
|
|
197
|
+
const sessionCwd = info?.cwd ?? (await getDefaultCwd(chatId));
|
|
198
|
+
const newName2 = sessionChatName(prefix, sessionCwd);
|
|
199
|
+
await recordSessionRegistry({ chatId, sessionId, tool, chatName: newName2 }).catch(() => {});
|
|
200
|
+
await saveSessionTool(sessionId, tool, newName2).catch(() => {});
|
|
201
|
+
console.log(`[${ts()}] [RENAME] WeChat P2P → "${newName2}"`);
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error(`[${ts()}] [RENAME] WeChat P2P failed: ${(err as Error).message}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
161
209
|
async function cleanupNonWechatP2pBinding(
|
|
162
210
|
platform: PlatformAdapter,
|
|
163
211
|
chatId: string,
|
|
@@ -1206,126 +1254,132 @@ export async function handleCommand(
|
|
|
1206
1254
|
}
|
|
1207
1255
|
|
|
1208
1256
|
// /plan <message> — 只读/规划模式
|
|
1209
|
-
if (textLower.startsWith("/plan ")) {
|
|
1210
|
-
const planText = text.slice(6).trim();
|
|
1211
|
-
const resolved = resolveModeCommand("/plan");
|
|
1212
|
-
if (!planText) {
|
|
1213
|
-
await platform.sendText(chatId, "用法:`/plan <消息>` — 在只读/规划模式下提问。").catch(() => {});
|
|
1214
|
-
logTrace(tid, "DONE", { outcome: "plan_no_message" });
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
if (
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1257
|
+
if (textLower.startsWith("/plan ")) {
|
|
1258
|
+
const planText = text.slice(6).trim();
|
|
1259
|
+
const resolved = resolveModeCommand("/plan");
|
|
1260
|
+
if (!planText) {
|
|
1261
|
+
await platform.sendText(chatId, "用法:`/plan <消息>` — 在只读/规划模式下提问。").catch(() => {});
|
|
1262
|
+
logTrace(tid, "DONE", { outcome: "plan_no_message" });
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (resolved?.supportedTools.includes(descriptionTool) === true) {
|
|
1266
|
+
logTrace(tid, "BRANCH", { cmd: "/plan", sessionId, tool: descriptionTool });
|
|
1267
|
+
|
|
1268
|
+
await renameUntitledSession(
|
|
1269
|
+
platform, chatId, chatType, sessionId, descriptionTool,
|
|
1270
|
+
planText, description, chatInfo,
|
|
1271
|
+
);
|
|
1272
|
+
|
|
1273
|
+
if (isSessionRunning(sessionId)) {
|
|
1274
|
+
const queued = enqueueMessage(sessionId, {
|
|
1275
|
+
text, chatId, openId, msgTimestamp, chatType, traceId: tid,
|
|
1276
|
+
});
|
|
1277
|
+
if (queued) {
|
|
1278
|
+
logTrace(tid, "QUEUED", { sessionId, mode: "plan" });
|
|
1279
|
+
if (platform.kind === "wechat") {
|
|
1280
|
+
await platform.sendText(chatId, "当前会话正在生成中,你的消息已进入缓存队列,生成完成后会立即处理。发送 /cancel 可取消缓存。").catch(() => {});
|
|
1281
|
+
} else {
|
|
1282
|
+
await platform.sendRawCard(chatId, buildQueuedCard(planText)).catch(() => {});
|
|
1283
|
+
}
|
|
1284
|
+
} else {
|
|
1285
|
+
logTrace(tid, "QUEUE_FULL", { sessionId });
|
|
1286
|
+
if (platform.kind === "wechat") {
|
|
1287
|
+
await platform.sendText(chatId, "当前缓存队列中已有消息等待处理,请等待或发送 /stop(停止生成)或 /cancel(取消缓存)。").catch(() => {});
|
|
1288
|
+
} else {
|
|
1289
|
+
await platform.sendRawCard(chatId, buildQueueFullCard()).catch(() => {});
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (shouldSendWechatProcessingAck(platform, planText.toLowerCase(), chatType)) {
|
|
1296
|
+
await platform.sendText(chatId, "生成中...").catch(() => {});
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
try {
|
|
1300
|
+
logTrace(tid, "RESUME", { sessionId, tool: descriptionTool, mode: "plan" });
|
|
1301
|
+
await resumeAndPrompt(
|
|
1302
|
+
sessionId, planText, platform, chatId, msgTimestamp,
|
|
1303
|
+
descriptionTool, tid, "plan",
|
|
1304
|
+
);
|
|
1305
|
+
logTrace(tid, "DONE", { outcome: "plan_done", sessionId });
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
logTrace(tid, "DONE", { outcome: "plan_fail", error: (err as Error).message });
|
|
1308
|
+
await platform.sendCard(
|
|
1309
|
+
chatId, "Error",
|
|
1310
|
+
`Failed to run plan mode:\n${(err as Error).message}`,
|
|
1311
|
+
"red",
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
logTrace(tid, "BRANCH", { cmd: "/plan", sessionId, tool: descriptionTool, fallback: "raw_message" });
|
|
1318
|
+
}
|
|
1268
1319
|
|
|
1269
1320
|
// /ask <message> — 只读/问答模式
|
|
1270
|
-
if (textLower.startsWith("/ask ")) {
|
|
1271
|
-
const askText = text.slice(5).trim();
|
|
1272
|
-
const resolved = resolveModeCommand("/ask");
|
|
1273
|
-
if (!askText) {
|
|
1274
|
-
await platform.sendText(chatId, "用法:`/ask <消息>` — 在只读/问答模式下提问。").catch(() => {});
|
|
1275
|
-
logTrace(tid, "DONE", { outcome: "ask_no_message" });
|
|
1276
|
-
return;
|
|
1277
|
-
}
|
|
1278
|
-
if (
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1321
|
+
if (textLower.startsWith("/ask ")) {
|
|
1322
|
+
const askText = text.slice(5).trim();
|
|
1323
|
+
const resolved = resolveModeCommand("/ask");
|
|
1324
|
+
if (!askText) {
|
|
1325
|
+
await platform.sendText(chatId, "用法:`/ask <消息>` — 在只读/问答模式下提问。").catch(() => {});
|
|
1326
|
+
logTrace(tid, "DONE", { outcome: "ask_no_message" });
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (resolved?.supportedTools.includes(descriptionTool) === true) {
|
|
1330
|
+
logTrace(tid, "BRANCH", { cmd: "/ask", sessionId, tool: descriptionTool });
|
|
1331
|
+
|
|
1332
|
+
await renameUntitledSession(
|
|
1333
|
+
platform, chatId, chatType, sessionId, descriptionTool,
|
|
1334
|
+
askText, description, chatInfo,
|
|
1335
|
+
);
|
|
1336
|
+
|
|
1337
|
+
if (isSessionRunning(sessionId)) {
|
|
1338
|
+
const queued = enqueueMessage(sessionId, {
|
|
1339
|
+
text, chatId, openId, msgTimestamp, chatType, traceId: tid,
|
|
1340
|
+
});
|
|
1341
|
+
if (queued) {
|
|
1342
|
+
logTrace(tid, "QUEUED", { sessionId, mode: "ask" });
|
|
1343
|
+
if (platform.kind === "wechat") {
|
|
1344
|
+
await platform.sendText(chatId, "当前会话正在生成中,你的消息已进入缓存队列,生成完成后会立即处理。发送 /cancel 可取消缓存。").catch(() => {});
|
|
1345
|
+
} else {
|
|
1346
|
+
await platform.sendRawCard(chatId, buildQueuedCard(askText)).catch(() => {});
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
logTrace(tid, "QUEUE_FULL", { sessionId });
|
|
1350
|
+
if (platform.kind === "wechat") {
|
|
1351
|
+
await platform.sendText(chatId, "当前缓存队列中已有消息等待处理,请等待或发送 /stop(停止生成)或 /cancel(取消缓存)。").catch(() => {});
|
|
1352
|
+
} else {
|
|
1353
|
+
await platform.sendRawCard(chatId, buildQueueFullCard()).catch(() => {});
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (shouldSendWechatProcessingAck(platform, askText.toLowerCase(), chatType)) {
|
|
1360
|
+
await platform.sendText(chatId, "生成中...").catch(() => {});
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
try {
|
|
1364
|
+
logTrace(tid, "RESUME", { sessionId, tool: descriptionTool, mode: "ask" });
|
|
1365
|
+
await resumeAndPrompt(
|
|
1366
|
+
sessionId, askText, platform, chatId, msgTimestamp,
|
|
1367
|
+
descriptionTool, tid, "ask",
|
|
1368
|
+
);
|
|
1369
|
+
logTrace(tid, "DONE", { outcome: "ask_done", sessionId });
|
|
1370
|
+
} catch (err) {
|
|
1371
|
+
logTrace(tid, "DONE", { outcome: "ask_fail", error: (err as Error).message });
|
|
1372
|
+
await platform.sendCard(
|
|
1373
|
+
chatId, "Error",
|
|
1374
|
+
`Failed to run ask mode:\n${(err as Error).message}`,
|
|
1375
|
+
"red",
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
logTrace(tid, "BRANCH", { cmd: "/ask", sessionId, tool: descriptionTool, fallback: "raw_message" });
|
|
1382
|
+
}
|
|
1329
1383
|
|
|
1330
1384
|
const lastTs = lastMsgTimestamps.get(chatId);
|
|
1331
1385
|
if (lastTs !== undefined && msgTimestamp <= lastTs) {
|