chatccc 0.2.128 → 0.2.129
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 +6 -0
- package/package.json +1 -1
- package/src/adapters/adapter-interface.ts +2 -0
- package/src/adapters/claude-adapter.ts +4 -2
- package/src/orchestrator.ts +25 -7
- package/src/session.ts +4 -1
package/README.md
CHANGED
|
@@ -310,6 +310,12 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
310
310
|
| `/sessions` | 查看所有会话状态 |
|
|
311
311
|
| `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
|
|
312
312
|
| `/restart` | 重启机器人进程 |
|
|
313
|
+
| `/plan [内容]` | 以 plan 模式传递给 Agent,**不跳过权限检查**(无 `--dangerously-skip-permissions`) |
|
|
314
|
+
| `/ask [内容]` | 以 plan 模式传递给 Agent,**不跳过权限检查**(同 `/plan`) |
|
|
315
|
+
|
|
316
|
+
> **`/plan` 与 `/ask`**:这两个指令不会被 ChatCCC 当作内置命令处理,而是把用户原话(含指令前缀)完整传给 Agent。与普通消息的区别是:调用时不添加 `--dangerously-skip-permissions`,改为 `--permission-mode plan`,Agent 会先在权限沙盒内运行,遇到命令执行等操作时需要确认。适合需要安全审查的任务场景。
|
|
317
|
+
>
|
|
318
|
+
> **群名**:`/plan` / `/ask` 消息如果是群内第一条消息,也会触发群名更新。为美观起见,群名会自动去除 `/plan` 或 `/ask` 前缀。
|
|
313
319
|
|
|
314
320
|
> **模型切换**:`/model` 查看可选模型清单,`/model <名称>` 模糊匹配切换,`/model clear` 恢复默认。当前仅 Claude Code 支持模型切换(需同时填写 `claude.model` 和 `claude.subagentModel`),Cursor / Codex 切换正在开发中。
|
|
315
321
|
|
package/package.json
CHANGED
|
@@ -128,6 +128,8 @@ export interface ToolPromptOptions {
|
|
|
128
128
|
onProcessStart?: (info: ToolProcessInfo) => void;
|
|
129
129
|
/** Called when the adapter leaves the prompt process scope normally or by abort. */
|
|
130
130
|
onProcessExit?: (info: ToolProcessInfo) => void;
|
|
131
|
+
/** 是否为 /plan 或 /ask 模式:不开启 dangerous skip permissions */
|
|
132
|
+
planMode?: boolean;
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
// ---------------------------------------------------------------------------
|
|
@@ -281,16 +281,17 @@ function buildCliArgs(
|
|
|
281
281
|
isEmpty: (value: string) => boolean,
|
|
282
282
|
mcpConfigJson: string | null,
|
|
283
283
|
extraArgs: string[],
|
|
284
|
+
planMode?: boolean,
|
|
284
285
|
): string[] {
|
|
285
286
|
const args = [
|
|
286
287
|
"-p",
|
|
287
288
|
"--output-format", "stream-json",
|
|
288
289
|
"--verbose",
|
|
289
290
|
"--setting-sources", "user,project,local",
|
|
290
|
-
"--permission-mode", "bypassPermissions",
|
|
291
|
-
"--dangerously-skip-permissions",
|
|
291
|
+
"--permission-mode", planMode ? "plan" : "bypassPermissions",
|
|
292
292
|
"--settings", "{\"maxTurns\":999999999,\"maxBudget\":999999999}",
|
|
293
293
|
];
|
|
294
|
+
if (!planMode) args.push("--dangerously-skip-permissions");
|
|
294
295
|
|
|
295
296
|
if (!isEmpty(model)) args.push("--model", model);
|
|
296
297
|
if (!isEmpty(effort)) args.push("--effort", effort);
|
|
@@ -465,6 +466,7 @@ class ClaudeAdapter implements ToolAdapter {
|
|
|
465
466
|
const args = buildCliArgs(
|
|
466
467
|
this.model, this.effort, this.isEmpty, mcpConfigJson,
|
|
467
468
|
["--resume", sessionId, "--input-format", "stream-json", "--replay-user-messages"],
|
|
469
|
+
options?.planMode,
|
|
468
470
|
);
|
|
469
471
|
|
|
470
472
|
const proc = spawnCli(args, cwd, env, true);
|
package/src/orchestrator.ts
CHANGED
|
@@ -94,6 +94,21 @@ export function sessionChatName(left: string, cwd: string): string {
|
|
|
94
94
|
return `${left}-${cwdDisplayName(cwd)}`;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/** 判断用户消息是否以 /plan 或 /ask 开头(忽略大小写和尾部空格) */
|
|
98
|
+
function isPlanOrAsk(text: string): boolean {
|
|
99
|
+
const t = text.toLowerCase().trim();
|
|
100
|
+
return t === "/plan" || t.startsWith("/plan ") || t === "/ask" || t.startsWith("/ask ");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 剥离 /plan 或 /ask 前缀,用于群名等展示场景 */
|
|
104
|
+
function stripPlanOrAskPrefix(text: string): string {
|
|
105
|
+
if (isPlanOrAsk(text)) {
|
|
106
|
+
const match = text.match(/^\/(?:plan|ask)\s*(.*)/i);
|
|
107
|
+
return (match?.[1] ?? "").trim() || text.trim();
|
|
108
|
+
}
|
|
109
|
+
return text;
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
/** 模型模糊匹配:精确匹配优先,否则找子串匹配(模型名越短越优先) */
|
|
98
113
|
function findModelMatch(input: string, models: string[]): string | null {
|
|
99
114
|
if (models.length === 0) return null;
|
|
@@ -117,8 +132,9 @@ function shouldSendWechatProcessingAck(
|
|
|
117
132
|
platform: PlatformAdapter,
|
|
118
133
|
textLower: string,
|
|
119
134
|
chatType: string,
|
|
135
|
+
text: string,
|
|
120
136
|
): boolean {
|
|
121
|
-
return platform.kind === "wechat" && chatType === "p2p" && !textLower.startsWith("/");
|
|
137
|
+
return platform.kind === "wechat" && chatType === "p2p" && (!textLower.startsWith("/") || isPlanOrAsk(text));
|
|
122
138
|
}
|
|
123
139
|
|
|
124
140
|
function isNonWechatP2p(platform: PlatformAdapter, chatType: string): boolean {
|
|
@@ -600,10 +616,10 @@ export async function handleCommand(
|
|
|
600
616
|
if (
|
|
601
617
|
chatType !== "p2p" &&
|
|
602
618
|
isUntitledSessionChatName(chatInfo!.name) &&
|
|
603
|
-
!textLower.startsWith("/")
|
|
619
|
+
(!textLower.startsWith("/") || isPlanOrAsk(text))
|
|
604
620
|
) {
|
|
605
621
|
const MAX_PREFIX = 10;
|
|
606
|
-
const prefix = text.slice(0, MAX_PREFIX);
|
|
622
|
+
const prefix = stripPlanOrAskPrefix(text).slice(0, MAX_PREFIX);
|
|
607
623
|
const adapter = getAdapterForTool(descriptionTool, sessionId);
|
|
608
624
|
const info = await adapter
|
|
609
625
|
.getSessionInfo(sessionId)
|
|
@@ -635,7 +651,7 @@ export async function handleCommand(
|
|
|
635
651
|
if (
|
|
636
652
|
chatType === "p2p" &&
|
|
637
653
|
platform.kind === "wechat" &&
|
|
638
|
-
!textLower.startsWith("/")
|
|
654
|
+
(!textLower.startsWith("/") || isPlanOrAsk(text))
|
|
639
655
|
) {
|
|
640
656
|
try {
|
|
641
657
|
const reg = await loadSessionRegistryForBinding();
|
|
@@ -1223,7 +1239,7 @@ export async function handleCommand(
|
|
|
1223
1239
|
return;
|
|
1224
1240
|
}
|
|
1225
1241
|
|
|
1226
|
-
if (shouldSendWechatProcessingAck(platform, textLower, chatType)) {
|
|
1242
|
+
if (shouldSendWechatProcessingAck(platform, textLower, chatType, text)) {
|
|
1227
1243
|
await platform.sendText(chatId, "生成中...").catch(() => {});
|
|
1228
1244
|
}
|
|
1229
1245
|
|
|
@@ -1237,6 +1253,7 @@ export async function handleCommand(
|
|
|
1237
1253
|
msgTimestamp,
|
|
1238
1254
|
descriptionTool,
|
|
1239
1255
|
tid,
|
|
1256
|
+
isPlanOrAsk(text),
|
|
1240
1257
|
);
|
|
1241
1258
|
logTrace(tid, "DONE", { outcome: "resume_done", sessionId });
|
|
1242
1259
|
console.log(`[${ts()}] [RESUME] Session ${sessionId} done`);
|
|
@@ -1311,7 +1328,7 @@ export async function handleCommand(
|
|
|
1311
1328
|
}
|
|
1312
1329
|
|
|
1313
1330
|
// 飞书私聊普通消息:不再绑定私聊本身,而是自动创建会话群并把私聊内容作为首轮 prompt。
|
|
1314
|
-
if (isNonWechatP2p(platform, chatType) && !textLower.startsWith("/")) {
|
|
1331
|
+
if (isNonWechatP2p(platform, chatType) && (!textLower.startsWith("/") || isPlanOrAsk(text))) {
|
|
1315
1332
|
const tool = resolveDefaultAgentTool();
|
|
1316
1333
|
const toolLabel = toolDisplayName(tool);
|
|
1317
1334
|
logTrace(tid, "BRANCH", { cmd: "auto_new_from_p2p", tool });
|
|
@@ -1352,7 +1369,7 @@ export async function handleCommand(
|
|
|
1352
1369
|
}
|
|
1353
1370
|
|
|
1354
1371
|
const cwd = sessionCwd;
|
|
1355
|
-
const initialName = sessionChatName(text.slice(0, 10) || "新会话", cwd);
|
|
1372
|
+
const initialName = sessionChatName(stripPlanOrAskPrefix(text).slice(0, 10) || "新会话", cwd);
|
|
1356
1373
|
let newChatId: string;
|
|
1357
1374
|
try {
|
|
1358
1375
|
newChatId = await platform.createGroup(initialName, [openId]);
|
|
@@ -1436,6 +1453,7 @@ export async function handleCommand(
|
|
|
1436
1453
|
msgTimestamp,
|
|
1437
1454
|
tool,
|
|
1438
1455
|
tid,
|
|
1456
|
+
isPlanOrAsk(text),
|
|
1439
1457
|
);
|
|
1440
1458
|
console.log(`[${ts()}] [AUTO-P2P 5/5] First prompt sent → OK`);
|
|
1441
1459
|
logTrace(tid, "DONE", { outcome: "auto_new_p2p_prompt_done", newChatId, sessionId, tool });
|
package/src/session.ts
CHANGED
|
@@ -832,8 +832,9 @@ export async function resumeAndPrompt(
|
|
|
832
832
|
msgTimestamp: number,
|
|
833
833
|
tool: string,
|
|
834
834
|
traceId?: string,
|
|
835
|
+
planMode?: boolean,
|
|
835
836
|
): Promise<void> {
|
|
836
|
-
return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
|
|
837
|
+
return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId, planMode);
|
|
837
838
|
}
|
|
838
839
|
|
|
839
840
|
// ---------------------------------------------------------------------------
|
|
@@ -848,6 +849,7 @@ export async function runAgentSession(
|
|
|
848
849
|
msgTimestamp: number,
|
|
849
850
|
tool: string,
|
|
850
851
|
traceId?: string,
|
|
852
|
+
planMode?: boolean,
|
|
851
853
|
): Promise<void> {
|
|
852
854
|
const tid = traceId ?? "";
|
|
853
855
|
|
|
@@ -1106,6 +1108,7 @@ export async function runAgentSession(
|
|
|
1106
1108
|
clearPromptProcessMonitor(sessionId);
|
|
1107
1109
|
if (exitInfo.pid !== undefined) unregisterProcess(exitInfo.pid);
|
|
1108
1110
|
},
|
|
1111
|
+
planMode,
|
|
1109
1112
|
})) {
|
|
1110
1113
|
for (const block of unifiedMsg.blocks) {
|
|
1111
1114
|
accumulateBlockContent(block, state, toolCallMap);
|