chatccc 0.2.99 → 0.2.101
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/config.sample.json +2 -1
- package/package.json +1 -1
- package/src/cards.ts +43 -0
- package/src/config.ts +7 -1
- package/src/feishu-api.ts +1 -1
- package/src/orchestrator.ts +137 -0
- package/src/session.ts +7 -3
package/config.sample.json
CHANGED
package/package.json
CHANGED
package/src/cards.ts
CHANGED
|
@@ -24,6 +24,12 @@ export function buildButtons(buttons: ButtonDef[]): object {
|
|
|
24
24
|
// Content helpers
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
|
|
27
|
+
// 检测 markdown 代码块是否未闭合(``` 出现奇数次)
|
|
28
|
+
export function isCodeBlockOpen(text: string): boolean {
|
|
29
|
+
const matches = text.match(/```/g);
|
|
30
|
+
return matches ? matches.length % 2 !== 0 : false;
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
export function truncateContent(text: string, maxLines = 20, maxChars = 8000): string {
|
|
28
34
|
const lines = text.split("\n");
|
|
29
35
|
let displayText: string;
|
|
@@ -39,6 +45,11 @@ export function truncateContent(text: string, maxLines = 20, maxChars = 8000): s
|
|
|
39
45
|
displayText = "..." + displayText.slice(-maxChars);
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
// 截断后如果代码块未闭合,补上闭合标记,避免后续追加内容时误入代码块
|
|
49
|
+
if (isCodeBlockOpen(displayText)) {
|
|
50
|
+
displayText += "\n```";
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
return displayText;
|
|
43
54
|
}
|
|
44
55
|
|
|
@@ -120,6 +131,7 @@ export function buildHelpCard(
|
|
|
120
131
|
"发送 **/new cursor** 创建新 Cursor 会话",
|
|
121
132
|
"发送 **/new codex** 创建新 Codex 会话",
|
|
122
133
|
"发送 **/newh** 重置当前会话(沿用当前工作目录,不切换)",
|
|
134
|
+
"发送 **/claude** 查看或切换 Claude API 模式(官方/第三方)",
|
|
123
135
|
].join("\n");
|
|
124
136
|
return JSON.stringify({
|
|
125
137
|
config: { wide_screen_mode: true },
|
|
@@ -450,3 +462,34 @@ export function buildModelCard(
|
|
|
450
462
|
],
|
|
451
463
|
});
|
|
452
464
|
}
|
|
465
|
+
|
|
466
|
+
/** Claude API 模式切换卡片(/claude 命令) */
|
|
467
|
+
export function buildClaudeCard(
|
|
468
|
+
currentMode: "official" | "thirdparty",
|
|
469
|
+
missingFields: string[],
|
|
470
|
+
): string {
|
|
471
|
+
const modeLabel = currentMode === "thirdparty" ? "第三方 API(自定义网关)" : "官方 API(Anthropic 直连)";
|
|
472
|
+
const modeDesc = currentMode === "thirdparty"
|
|
473
|
+
? "使用第三方兼容网关,需配置 API Key 和 Base URL。"
|
|
474
|
+
: "直连 Anthropic 官方 API,使用系统环境变量中的 ANTHROPIC_API_KEY。";
|
|
475
|
+
const lines = [`**当前模式:** ${modeLabel}`, "", modeDesc];
|
|
476
|
+
|
|
477
|
+
if (missingFields.length > 0) {
|
|
478
|
+
lines.push("", `**切换失败:** 以下必填项未配置: ${missingFields.map(f => `\`${f}\``).join(", ")}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const buttons: ButtonDef[] = [
|
|
482
|
+
{ text: "/claude official", value: JSON.stringify({ cmd: "/claude official" }), type: currentMode === "official" ? "default" : "primary" },
|
|
483
|
+
{ text: "/claude 3rd", value: JSON.stringify({ cmd: "/claude 3rd" }), type: currentMode === "thirdparty" ? "default" : "primary" },
|
|
484
|
+
];
|
|
485
|
+
|
|
486
|
+
return JSON.stringify({
|
|
487
|
+
config: { wide_screen_mode: true },
|
|
488
|
+
header: { template: "blue", title: { content: "Claude API 模式", tag: "plain_text" } },
|
|
489
|
+
elements: [
|
|
490
|
+
{ tag: "div", text: { tag: "lark_md", content: lines.join("\n") } },
|
|
491
|
+
{ tag: "hr" },
|
|
492
|
+
buildButtons(buttons),
|
|
493
|
+
],
|
|
494
|
+
});
|
|
495
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -67,6 +67,8 @@ export interface ClaudeConfig {
|
|
|
67
67
|
effort: string;
|
|
68
68
|
apiKey: string;
|
|
69
69
|
baseUrl: string;
|
|
70
|
+
/** 是否使用第三方 API(非 Anthropic 官方);为 true 时 baseUrl 必须配置 */
|
|
71
|
+
useThirdPartyApi: boolean;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export interface CursorConfig {
|
|
@@ -322,7 +324,7 @@ function loadConfig(): AppConfig {
|
|
|
322
324
|
port: 18080,
|
|
323
325
|
gitTimeoutSeconds: 180,
|
|
324
326
|
allowInterrupt: false,
|
|
325
|
-
claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "" },
|
|
327
|
+
claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", useThirdPartyApi: false },
|
|
326
328
|
cursor: { enabled: false, defaultAgent: false, path: "", model: "claude-opus-4-7-max" },
|
|
327
329
|
codex: { enabled: false, defaultAgent: false, path: "", model: "", effort: "" },
|
|
328
330
|
};
|
|
@@ -466,6 +468,7 @@ function loadConfig(): AppConfig {
|
|
|
466
468
|
effort: normalizeOptionalConfigField(claude.effort, { label: "claude.effort" }),
|
|
467
469
|
apiKey: claude.apiKey ?? "",
|
|
468
470
|
baseUrl: claude.baseUrl ?? "",
|
|
471
|
+
useThirdPartyApi: typeof claude.useThirdPartyApi === "boolean" ? claude.useThirdPartyApi : false,
|
|
469
472
|
},
|
|
470
473
|
cursor: {
|
|
471
474
|
enabled: cursorEnabled,
|
|
@@ -529,6 +532,8 @@ export let CLAUDE_EFFORT = config.claude.effort;
|
|
|
529
532
|
export let CLAUDE_API_KEY = config.claude.apiKey;
|
|
530
533
|
/** Anthropic 兼容网关的 base URL(仅经 SDK 子进程 env 传递,从不写入主进程 process.env) */
|
|
531
534
|
export let CLAUDE_BASE_URL = config.claude.baseUrl;
|
|
535
|
+
/** 是否使用第三方 API(非 Anthropic 官方) */
|
|
536
|
+
export let CLAUDE_USE_THIRD_PARTY_API = config.claude.useThirdPartyApi;
|
|
532
537
|
|
|
533
538
|
/** 返回当前生效的 Claude 模型(per-session 覆盖由 session.ts 管理,此处仅返回全局配置) */
|
|
534
539
|
export function getEffectiveClaudeModel(): string {
|
|
@@ -608,6 +613,7 @@ export function applyLoadedConfig(next: AppConfig): void {
|
|
|
608
613
|
CLAUDE_EFFORT = next.claude.effort;
|
|
609
614
|
CLAUDE_API_KEY = next.claude.apiKey;
|
|
610
615
|
CLAUDE_BASE_URL = next.claude.baseUrl;
|
|
616
|
+
CLAUDE_USE_THIRD_PARTY_API = next.claude.useThirdPartyApi;
|
|
611
617
|
GIT_TIMEOUT_SECONDS = next.gitTimeoutSeconds;
|
|
612
618
|
GIT_TIMEOUT_MS = GIT_TIMEOUT_SECONDS * 1000;
|
|
613
619
|
ALLOW_INTERRUPT = next.allowInterrupt;
|
package/src/feishu-api.ts
CHANGED
|
@@ -549,7 +549,7 @@ function wrapMarkdownTables(text: string): string {
|
|
|
549
549
|
let foundSeparator = false;
|
|
550
550
|
|
|
551
551
|
const isTableRow = (line: string): boolean =>
|
|
552
|
-
/^\s*\|.+\|/.test(line);
|
|
552
|
+
/^\s*\|.+\|/.test(line) && !isSeparatorRow(line);
|
|
553
553
|
|
|
554
554
|
const isSeparatorRow = (line: string): boolean =>
|
|
555
555
|
/^\s*\|[\s\-:]+\|/.test(line) && /-/.test(line);
|
package/src/orchestrator.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { readdir, stat } from "node:fs/promises";
|
|
10
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
11
|
import { resolve, dirname } from "node:path";
|
|
11
12
|
|
|
12
13
|
import { makeTraceId, logTrace } from "./trace.ts";
|
|
@@ -15,6 +16,10 @@ import {
|
|
|
15
16
|
CLAUDE_EFFORT,
|
|
16
17
|
CLAUDE_MODEL,
|
|
17
18
|
CLAUDE_SUBAGENT_MODEL,
|
|
19
|
+
CLAUDE_API_KEY,
|
|
20
|
+
CLAUDE_BASE_URL,
|
|
21
|
+
CLAUDE_USE_THIRD_PARTY_API,
|
|
22
|
+
CONFIG_FILE,
|
|
18
23
|
GIT_TIMEOUT_MS,
|
|
19
24
|
PROJECT_ROOT,
|
|
20
25
|
anthropicConfigDisplay,
|
|
@@ -29,6 +34,7 @@ import {
|
|
|
29
34
|
sessionPrefixForTool,
|
|
30
35
|
toolDisplayName,
|
|
31
36
|
ts,
|
|
37
|
+
reloadConfigFromDisk,
|
|
32
38
|
type AgentTool,
|
|
33
39
|
} from "./config.ts";
|
|
34
40
|
import {
|
|
@@ -40,6 +46,7 @@ import {
|
|
|
40
46
|
buildSessionsCard,
|
|
41
47
|
buildQueuedCard,
|
|
42
48
|
buildQueueFullCard,
|
|
49
|
+
buildClaudeCard,
|
|
43
50
|
} from "./cards.ts";
|
|
44
51
|
import {
|
|
45
52
|
formatGitResult,
|
|
@@ -383,6 +390,7 @@ export async function handleCommand(
|
|
|
383
390
|
`**工作目录:** \`${cwd}\`\n\n` +
|
|
384
391
|
`直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
|
|
385
392
|
`发送 **/cd** 切换新建会话的默认目录。\n` +
|
|
393
|
+
`发送 **/claude** 查看或切换 Claude API 模式(官方/第三方)。\n` +
|
|
386
394
|
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
387
395
|
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
388
396
|
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
@@ -1078,6 +1086,72 @@ export async function handleCommand(
|
|
|
1078
1086
|
return;
|
|
1079
1087
|
}
|
|
1080
1088
|
|
|
1089
|
+
// /claude — 查看/切换 Claude API 模式(官方 vs 第三方)
|
|
1090
|
+
if (textLower === "/claude" || textLower === "/claude official" || textLower === "/claude 3rd") {
|
|
1091
|
+
logTrace(tid, "BRANCH", { cmd: "/claude", arg: text.slice(8).trim() || "(none)" });
|
|
1092
|
+
|
|
1093
|
+
if (textLower === "/claude official") {
|
|
1094
|
+
// 切换到官方模式:设置 useThirdPartyApi=false,清空 apiKey 和 baseUrl
|
|
1095
|
+
try {
|
|
1096
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
1097
|
+
const cfg = JSON.parse(raw);
|
|
1098
|
+
cfg.claude = cfg.claude || {};
|
|
1099
|
+
cfg.claude.useThirdPartyApi = false;
|
|
1100
|
+
cfg.claude.apiKey = "";
|
|
1101
|
+
cfg.claude.baseUrl = "";
|
|
1102
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
1103
|
+
reloadConfigFromDisk();
|
|
1104
|
+
logTrace(tid, "DONE", { outcome: "claude_official" });
|
|
1105
|
+
const card = buildClaudeCard("official", []);
|
|
1106
|
+
await platform.sendRawCard(chatId, card);
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
logTrace(tid, "DONE", { outcome: "claude_official_fail", error: (err as Error).message });
|
|
1109
|
+
await platform.sendText(chatId, `切换失败: ${(err as Error).message}`).catch(() => {});
|
|
1110
|
+
}
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (textLower === "/claude 3rd") {
|
|
1115
|
+
// 切换到第三方模式:检查 apiKey 和 baseUrl 是否已配置
|
|
1116
|
+
const missing: string[] = [];
|
|
1117
|
+
if (!CLAUDE_API_KEY.trim()) missing.push("API Key");
|
|
1118
|
+
if (!CLAUDE_BASE_URL.trim()) missing.push("Base URL");
|
|
1119
|
+
|
|
1120
|
+
if (missing.length > 0) {
|
|
1121
|
+
logTrace(tid, "DONE", { outcome: "claude_3rd_missing", missing });
|
|
1122
|
+
const card = buildClaudeCard(
|
|
1123
|
+
CLAUDE_USE_THIRD_PARTY_API ? "thirdparty" : "official",
|
|
1124
|
+
missing,
|
|
1125
|
+
);
|
|
1126
|
+
await platform.sendRawCard(chatId, card);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
try {
|
|
1131
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
1132
|
+
const cfg = JSON.parse(raw);
|
|
1133
|
+
cfg.claude = cfg.claude || {};
|
|
1134
|
+
cfg.claude.useThirdPartyApi = true;
|
|
1135
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
1136
|
+
reloadConfigFromDisk();
|
|
1137
|
+
logTrace(tid, "DONE", { outcome: "claude_3rd" });
|
|
1138
|
+
const card = buildClaudeCard("thirdparty", []);
|
|
1139
|
+
await platform.sendRawCard(chatId, card);
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
logTrace(tid, "DONE", { outcome: "claude_3rd_fail", error: (err as Error).message });
|
|
1142
|
+
await platform.sendText(chatId, `切换失败: ${(err as Error).message}`).catch(() => {});
|
|
1143
|
+
}
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// /claude — 显示当前模式和切换按钮
|
|
1148
|
+
const currentMode: "official" | "thirdparty" = CLAUDE_USE_THIRD_PARTY_API ? "thirdparty" : "official";
|
|
1149
|
+
const card = buildClaudeCard(currentMode, []);
|
|
1150
|
+
await platform.sendRawCard(chatId, card);
|
|
1151
|
+
logTrace(tid, "DONE", { outcome: "claude_query", mode: currentMode });
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1081
1155
|
// /git <args>:在「当前会话工作目录」执行 git 命令
|
|
1082
1156
|
if (textLower.startsWith("/git ") || textLower === "/git") {
|
|
1083
1157
|
const args = text === "/git" ? "" : text.slice(5).trim();
|
|
@@ -1209,6 +1283,69 @@ export async function handleCommand(
|
|
|
1209
1283
|
return;
|
|
1210
1284
|
}
|
|
1211
1285
|
|
|
1286
|
+
// 无会话上下文 → 检查是否是 /claude 查询
|
|
1287
|
+
if (textLower === "/claude" || textLower === "/claude official" || textLower === "/claude 3rd") {
|
|
1288
|
+
logTrace(tid, "BRANCH", { cmd: "/claude", arg: text.slice(8).trim() || "(none)", noSession: true });
|
|
1289
|
+
|
|
1290
|
+
if (textLower === "/claude official") {
|
|
1291
|
+
try {
|
|
1292
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
1293
|
+
const cfg = JSON.parse(raw);
|
|
1294
|
+
cfg.claude = cfg.claude || {};
|
|
1295
|
+
cfg.claude.useThirdPartyApi = false;
|
|
1296
|
+
cfg.claude.apiKey = "";
|
|
1297
|
+
cfg.claude.baseUrl = "";
|
|
1298
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
1299
|
+
reloadConfigFromDisk();
|
|
1300
|
+
logTrace(tid, "DONE", { outcome: "claude_official" });
|
|
1301
|
+
const card = buildClaudeCard("official", []);
|
|
1302
|
+
await platform.sendRawCard(chatId, card);
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
logTrace(tid, "DONE", { outcome: "claude_official_fail", error: (err as Error).message });
|
|
1305
|
+
await platform.sendText(chatId, `切换失败: ${(err as Error).message}`).catch(() => {});
|
|
1306
|
+
}
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
if (textLower === "/claude 3rd") {
|
|
1311
|
+
const missing: string[] = [];
|
|
1312
|
+
if (!CLAUDE_API_KEY.trim()) missing.push("API Key");
|
|
1313
|
+
if (!CLAUDE_BASE_URL.trim()) missing.push("Base URL");
|
|
1314
|
+
|
|
1315
|
+
if (missing.length > 0) {
|
|
1316
|
+
logTrace(tid, "DONE", { outcome: "claude_3rd_missing", missing });
|
|
1317
|
+
const card = buildClaudeCard(
|
|
1318
|
+
CLAUDE_USE_THIRD_PARTY_API ? "thirdparty" : "official",
|
|
1319
|
+
missing,
|
|
1320
|
+
);
|
|
1321
|
+
await platform.sendRawCard(chatId, card);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
try {
|
|
1326
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
1327
|
+
const cfg = JSON.parse(raw);
|
|
1328
|
+
cfg.claude = cfg.claude || {};
|
|
1329
|
+
cfg.claude.useThirdPartyApi = true;
|
|
1330
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
1331
|
+
reloadConfigFromDisk();
|
|
1332
|
+
logTrace(tid, "DONE", { outcome: "claude_3rd" });
|
|
1333
|
+
const card = buildClaudeCard("thirdparty", []);
|
|
1334
|
+
await platform.sendRawCard(chatId, card);
|
|
1335
|
+
} catch (err) {
|
|
1336
|
+
logTrace(tid, "DONE", { outcome: "claude_3rd_fail", error: (err as Error).message });
|
|
1337
|
+
await platform.sendText(chatId, `切换失败: ${(err as Error).message}`).catch(() => {});
|
|
1338
|
+
}
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const currentMode: "official" | "thirdparty" = CLAUDE_USE_THIRD_PARTY_API ? "thirdparty" : "official";
|
|
1343
|
+
const card = buildClaudeCard(currentMode, []);
|
|
1344
|
+
await platform.sendRawCard(chatId, card);
|
|
1345
|
+
logTrace(tid, "DONE", { outcome: "claude_query", mode: currentMode });
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1212
1349
|
// 无会话上下文 → 检查是否是 /model 查询
|
|
1213
1350
|
if (textLower === "/model") {
|
|
1214
1351
|
const defaultTool = resolveDefaultAgentTool();
|
package/src/session.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
toolDisplayName,
|
|
21
21
|
ts,
|
|
22
22
|
} from "./config.ts";
|
|
23
|
-
import { buildProgressCard, getToolEmoji, truncateContent } from "./cards.ts";
|
|
23
|
+
import { buildProgressCard, getToolEmoji, isCodeBlockOpen, truncateContent } from "./cards.ts";
|
|
24
24
|
import { simplifyToolUse, simplifyToolResult } from "./simplify.ts";
|
|
25
25
|
import { logTrace } from "./trace.ts";
|
|
26
26
|
import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
|
|
@@ -1430,7 +1430,9 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1430
1430
|
const delta = (accDelta + replyDelta).trim();
|
|
1431
1431
|
|
|
1432
1432
|
display.dotCount = (display.dotCount % 9) + 1;
|
|
1433
|
-
|
|
1433
|
+
let deltaBase = (delta || "");
|
|
1434
|
+
if (isCodeBlockOpen(deltaBase)) deltaBase += "\n```";
|
|
1435
|
+
const displayContent = deltaBase + "\n" + "。" .repeat(display.dotCount);
|
|
1434
1436
|
if (displayContent === display.lastSentContent) continue;
|
|
1435
1437
|
|
|
1436
1438
|
display.lastSentContent = displayContent;
|
|
@@ -1453,7 +1455,9 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1453
1455
|
}
|
|
1454
1456
|
|
|
1455
1457
|
display.dotCount = (display.dotCount % 9) + 1;
|
|
1456
|
-
|
|
1458
|
+
let contentBase = state.accumulatedContent + state.finalReply;
|
|
1459
|
+
if (isCodeBlockOpen(contentBase)) contentBase += "\n```";
|
|
1460
|
+
const fullContent = contentBase + "\n" + "。".repeat(display.dotCount);
|
|
1457
1461
|
if (fullContent === display.lastSentContent) continue;
|
|
1458
1462
|
|
|
1459
1463
|
display.lastSentContent = fullContent;
|