chatccc 0.2.218 → 0.2.220
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 +38 -16
- package/config.sample.json +4 -1
- package/package.json +2 -1
- package/src/__tests__/builtin-config.test.ts +3 -0
- package/src/__tests__/builtin-file-tools.test.ts +44 -24
- package/src/__tests__/cards.test.ts +6 -6
- package/src/__tests__/config-reload.test.ts +19 -8
- package/src/__tests__/config-sample.test.ts +6 -2
- package/src/__tests__/orchestrator.test.ts +84 -5
- package/src/__tests__/sim-platform.test.ts +10 -0
- package/src/__tests__/web-ui.test.ts +40 -0
- package/src/builtin/file-tools.ts +279 -41
- package/src/cards.ts +7 -5
- package/src/config.ts +41 -10
- package/src/index.ts +13 -0
- package/src/orchestrator.ts +1553 -1488
- package/src/web-ui.ts +164 -46
|
@@ -2,7 +2,9 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { createReadStream } from "node:fs";
|
|
4
4
|
import { copyFile, mkdir, open, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
|
-
import {
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
6
8
|
|
|
7
9
|
import { jsonSchema, tool, type ToolSet } from "ai";
|
|
8
10
|
|
|
@@ -19,6 +21,8 @@ const SEARCH_TIMEOUT_MS = 15_000;
|
|
|
19
21
|
const MAX_COMMAND_OUTPUT_BYTES = 256 * 1024;
|
|
20
22
|
const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
|
|
21
23
|
const MAX_COMMAND_TIMEOUT_MS = 900_000;
|
|
24
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
25
|
+
const FALLBACK_SKIPPED_DIRECTORIES = new Set([".git", "node_modules"]);
|
|
22
26
|
|
|
23
27
|
export interface ReadFileInput {
|
|
24
28
|
path: string;
|
|
@@ -77,6 +81,11 @@ export interface SearchCodeOutput {
|
|
|
77
81
|
truncated: boolean;
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
/** @internal Allows tests to force the dependency-free fallback path. */
|
|
85
|
+
export interface SearchCodeRuntimeOptions {
|
|
86
|
+
ripgrepCommands?: readonly string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
export interface RunCommandInput {
|
|
81
90
|
command: string;
|
|
82
91
|
cwd?: string;
|
|
@@ -546,32 +555,44 @@ function parseRgLine(line: string): SearchCodeMatch | null {
|
|
|
546
555
|
};
|
|
547
556
|
}
|
|
548
557
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const query = input.query?.trim();
|
|
555
|
-
if (!query) throw new Error("query is required");
|
|
558
|
+
interface RipgrepOutput {
|
|
559
|
+
stdout: string;
|
|
560
|
+
stderr: string;
|
|
561
|
+
truncated: boolean;
|
|
562
|
+
}
|
|
556
563
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
"
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
];
|
|
568
|
-
if (input.glob?.trim()) {
|
|
569
|
-
args.push("--glob", input.glob.trim());
|
|
564
|
+
function resolveBundledRipgrepPath(): string | undefined {
|
|
565
|
+
try {
|
|
566
|
+
const bundled = requireFromHere("@vscode/ripgrep") as { rgPath?: unknown };
|
|
567
|
+
return typeof bundled.rgPath === "string" && bundled.rgPath.trim()
|
|
568
|
+
? bundled.rgPath
|
|
569
|
+
: undefined;
|
|
570
|
+
} catch {
|
|
571
|
+
// Unsupported platforms or damaged optional platform packages must not
|
|
572
|
+
// prevent ChatCCC itself from starting; system rg / Node fallback remain.
|
|
573
|
+
return undefined;
|
|
570
574
|
}
|
|
571
|
-
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function defaultRipgrepCommands(): string[] {
|
|
578
|
+
const bundled = resolveBundledRipgrepPath();
|
|
579
|
+
return [...new Set([bundled, "rg"].filter((value): value is string => !!value))];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function isUnavailableExecutableError(err: unknown): boolean {
|
|
583
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
584
|
+
return code === "ENOENT" || code === "EACCES" || code === "EPERM";
|
|
585
|
+
}
|
|
572
586
|
|
|
573
|
-
|
|
574
|
-
|
|
587
|
+
async function runRipgrep(
|
|
588
|
+
command: string,
|
|
589
|
+
args: string[],
|
|
590
|
+
cwd: string,
|
|
591
|
+
signal?: AbortSignal,
|
|
592
|
+
): Promise<RipgrepOutput> {
|
|
593
|
+
return new Promise<RipgrepOutput>((resolvePromise, reject) => {
|
|
594
|
+
let settled = false;
|
|
595
|
+
const child = spawn(command, args, {
|
|
575
596
|
cwd,
|
|
576
597
|
shell: false,
|
|
577
598
|
windowsHide: true,
|
|
@@ -581,14 +602,23 @@ export async function searchCodeForTool(
|
|
|
581
602
|
let stdout = "";
|
|
582
603
|
let stderr = "";
|
|
583
604
|
let truncated = false;
|
|
605
|
+
const cleanup = () => {
|
|
606
|
+
clearTimeout(timeout);
|
|
607
|
+
signal?.removeEventListener("abort", abort);
|
|
608
|
+
};
|
|
609
|
+
const rejectOnce = (err: Error) => {
|
|
610
|
+
if (settled) return;
|
|
611
|
+
settled = true;
|
|
612
|
+
cleanup();
|
|
613
|
+
reject(err);
|
|
614
|
+
};
|
|
584
615
|
const timeout = setTimeout(() => {
|
|
585
616
|
child.kill();
|
|
586
|
-
|
|
617
|
+
rejectOnce(new Error(`search_code timed out after ${SEARCH_TIMEOUT_MS}ms`));
|
|
587
618
|
}, SEARCH_TIMEOUT_MS);
|
|
588
|
-
|
|
589
619
|
const abort = () => {
|
|
590
620
|
child.kill();
|
|
591
|
-
|
|
621
|
+
rejectOnce(new Error("search_code aborted"));
|
|
592
622
|
};
|
|
593
623
|
signal?.addEventListener("abort", abort, { once: true });
|
|
594
624
|
|
|
@@ -606,14 +636,11 @@ export async function searchCodeForTool(
|
|
|
606
636
|
child.stderr?.on("data", (chunk: Buffer) => {
|
|
607
637
|
stderr += chunk.toString("utf8");
|
|
608
638
|
});
|
|
609
|
-
child.on("error", (err) =>
|
|
610
|
-
clearTimeout(timeout);
|
|
611
|
-
signal?.removeEventListener("abort", abort);
|
|
612
|
-
reject(err);
|
|
613
|
-
});
|
|
639
|
+
child.on("error", (err) => rejectOnce(err));
|
|
614
640
|
child.on("close", (code) => {
|
|
615
|
-
|
|
616
|
-
|
|
641
|
+
if (settled) return;
|
|
642
|
+
settled = true;
|
|
643
|
+
cleanup();
|
|
617
644
|
if (code !== 0 && code !== 1) {
|
|
618
645
|
reject(new Error(stderr.trim() || `rg exited with code ${code}`));
|
|
619
646
|
return;
|
|
@@ -621,20 +648,231 @@ export async function searchCodeForTool(
|
|
|
621
648
|
resolvePromise({ stdout, stderr, truncated });
|
|
622
649
|
});
|
|
623
650
|
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function expandGlobBraces(pattern: string): string[] {
|
|
654
|
+
const openIndex = pattern.indexOf("{");
|
|
655
|
+
if (openIndex < 0) return [pattern];
|
|
656
|
+
const closeIndex = pattern.indexOf("}", openIndex + 1);
|
|
657
|
+
if (closeIndex < 0) return [pattern];
|
|
658
|
+
const alternatives = pattern.slice(openIndex + 1, closeIndex).split(",");
|
|
659
|
+
if (alternatives.length < 2) return [pattern];
|
|
660
|
+
return alternatives.flatMap((alternative) => expandGlobBraces(
|
|
661
|
+
pattern.slice(0, openIndex) + alternative + pattern.slice(closeIndex + 1),
|
|
662
|
+
));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function globToRegExp(pattern: string): RegExp {
|
|
666
|
+
let source = "";
|
|
667
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
668
|
+
const char = pattern[index];
|
|
669
|
+
if (char === "*") {
|
|
670
|
+
if (pattern[index + 1] === "*") {
|
|
671
|
+
index += 1;
|
|
672
|
+
if (pattern[index + 1] === "/") {
|
|
673
|
+
index += 1;
|
|
674
|
+
source += "(?:.*/)?";
|
|
675
|
+
} else {
|
|
676
|
+
source += ".*";
|
|
677
|
+
}
|
|
678
|
+
} else {
|
|
679
|
+
source += "[^/]*";
|
|
680
|
+
}
|
|
681
|
+
} else if (char === "?") {
|
|
682
|
+
source += "[^/]";
|
|
683
|
+
} else {
|
|
684
|
+
source += char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return new RegExp(`^${source}$`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function createGlobMatchers(glob: string | undefined): Array<{ regex: RegExp; basenameOnly: boolean }> {
|
|
691
|
+
if (!glob?.trim()) return [];
|
|
692
|
+
return expandGlobBraces(glob.trim().replaceAll("\\", "/")).map((pattern) => ({
|
|
693
|
+
regex: globToRegExp(pattern),
|
|
694
|
+
basenameOnly: !pattern.includes("/"),
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function matchesFallbackGlob(
|
|
699
|
+
filePath: string,
|
|
700
|
+
searchRoot: string,
|
|
701
|
+
matchers: Array<{ regex: RegExp; basenameOnly: boolean }>,
|
|
702
|
+
): boolean {
|
|
703
|
+
if (matchers.length === 0) return true;
|
|
704
|
+
const relativePath = relative(searchRoot, filePath).split(sep).join("/");
|
|
705
|
+
return matchers.some(({ regex, basenameOnly }) => regex.test(
|
|
706
|
+
basenameOnly ? basename(filePath) : relativePath,
|
|
707
|
+
));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function searchCodeWithNode(
|
|
711
|
+
query: string,
|
|
712
|
+
searchPath: string,
|
|
713
|
+
glob: string | undefined,
|
|
714
|
+
maxResults: number,
|
|
715
|
+
signal?: AbortSignal,
|
|
716
|
+
): Promise<{ matches: SearchCodeMatch[]; truncated: boolean }> {
|
|
717
|
+
let queryRegex: RegExp;
|
|
718
|
+
try {
|
|
719
|
+
queryRegex = new RegExp(query);
|
|
720
|
+
} catch (err) {
|
|
721
|
+
throw new Error(`invalid search regex: ${(err as Error).message}`);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const startedAt = Date.now();
|
|
725
|
+
const matches: SearchCodeMatch[] = [];
|
|
726
|
+
const rootInfo = await stat(searchPath);
|
|
727
|
+
const searchRoot = rootInfo.isDirectory() ? searchPath : dirname(searchPath);
|
|
728
|
+
const globMatchers = createGlobMatchers(glob);
|
|
729
|
+
let truncated = false;
|
|
730
|
+
let outputBytes = 0;
|
|
731
|
+
|
|
732
|
+
const ensureActive = () => {
|
|
733
|
+
if (signal?.aborted) throw new Error("search_code aborted");
|
|
734
|
+
if (Date.now() - startedAt >= SEARCH_TIMEOUT_MS) {
|
|
735
|
+
throw new Error(`search_code timed out after ${SEARCH_TIMEOUT_MS}ms`);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
const searchFile = async (filePath: string) => {
|
|
740
|
+
if (!matchesFallbackGlob(filePath, searchRoot, globMatchers)) return;
|
|
741
|
+
ensureActive();
|
|
742
|
+
const input = createReadStream(filePath, { encoding: "utf8" });
|
|
743
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
744
|
+
let lineNumber = 0;
|
|
745
|
+
try {
|
|
746
|
+
for await (const line of lines) {
|
|
747
|
+
ensureActive();
|
|
748
|
+
lineNumber += 1;
|
|
749
|
+
if (line.includes("\0")) break;
|
|
750
|
+
const match = queryRegex.exec(line);
|
|
751
|
+
queryRegex.lastIndex = 0;
|
|
752
|
+
if (!match) continue;
|
|
753
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
754
|
+
if (outputBytes + lineBytes > MAX_SEARCH_BYTES) {
|
|
755
|
+
truncated = true;
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
outputBytes += lineBytes;
|
|
759
|
+
matches.push({
|
|
760
|
+
path: filePath,
|
|
761
|
+
line: lineNumber,
|
|
762
|
+
column: match.index + 1,
|
|
763
|
+
text: line,
|
|
764
|
+
});
|
|
765
|
+
if (matches.length >= maxResults) {
|
|
766
|
+
truncated = true;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
} catch (err) {
|
|
771
|
+
if (signal?.aborted) throw new Error("search_code aborted");
|
|
772
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
773
|
+
if (code !== "EACCES" && code !== "EPERM" && code !== "ENOENT") throw err;
|
|
774
|
+
} finally {
|
|
775
|
+
lines.close();
|
|
776
|
+
input.destroy();
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const visit = async (currentPath: string): Promise<void> => {
|
|
781
|
+
ensureActive();
|
|
782
|
+
if (truncated) return;
|
|
783
|
+
let info;
|
|
784
|
+
try {
|
|
785
|
+
info = currentPath === searchPath ? rootInfo : await stat(currentPath);
|
|
786
|
+
} catch (err) {
|
|
787
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
788
|
+
if (code === "EACCES" || code === "EPERM" || code === "ENOENT") return;
|
|
789
|
+
throw err;
|
|
790
|
+
}
|
|
791
|
+
if (info.isFile()) {
|
|
792
|
+
await searchFile(currentPath);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (!info.isDirectory()) return;
|
|
796
|
+
|
|
797
|
+
let entries;
|
|
798
|
+
try {
|
|
799
|
+
entries = await readdir(currentPath, { withFileTypes: true });
|
|
800
|
+
} catch (err) {
|
|
801
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
802
|
+
if (code === "EACCES" || code === "EPERM" || code === "ENOENT") return;
|
|
803
|
+
throw err;
|
|
804
|
+
}
|
|
805
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
806
|
+
for (const entry of entries) {
|
|
807
|
+
if (truncated) break;
|
|
808
|
+
if (entry.name.startsWith(".")) continue;
|
|
809
|
+
if (entry.isDirectory() && FALLBACK_SKIPPED_DIRECTORIES.has(entry.name)) continue;
|
|
810
|
+
if (entry.isSymbolicLink()) continue;
|
|
811
|
+
await visit(resolve(currentPath, entry.name));
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
await visit(searchPath);
|
|
816
|
+
return { matches, truncated };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export async function searchCodeForTool(
|
|
820
|
+
cwd: string,
|
|
821
|
+
input: SearchCodeInput,
|
|
822
|
+
signal?: AbortSignal,
|
|
823
|
+
runtimeOptions: SearchCodeRuntimeOptions = {},
|
|
824
|
+
): Promise<SearchCodeOutput> {
|
|
825
|
+
const query = input.query?.trim();
|
|
826
|
+
if (!query) throw new Error("query is required");
|
|
827
|
+
if (signal?.aborted) throw new Error("search_code aborted");
|
|
828
|
+
|
|
829
|
+
const searchPath = resolveToolPath(cwd, input.path);
|
|
830
|
+
const maxResults = Math.min(toPositiveInt(input.maxResults) ?? 50, MAX_SEARCH_RESULTS);
|
|
831
|
+
const args = [
|
|
832
|
+
"--line-number",
|
|
833
|
+
"--column",
|
|
834
|
+
"--no-heading",
|
|
835
|
+
"--color",
|
|
836
|
+
"never",
|
|
837
|
+
"--max-count",
|
|
838
|
+
String(maxResults),
|
|
839
|
+
];
|
|
840
|
+
if (input.glob?.trim()) {
|
|
841
|
+
args.push("--glob", input.glob.trim());
|
|
842
|
+
}
|
|
843
|
+
args.push("--", query, searchPath);
|
|
844
|
+
|
|
845
|
+
const commands = runtimeOptions.ripgrepCommands ?? defaultRipgrepCommands();
|
|
846
|
+
let output: RipgrepOutput | undefined;
|
|
847
|
+
for (const command of commands) {
|
|
848
|
+
try {
|
|
849
|
+
output = await runRipgrep(command, args, cwd, signal);
|
|
850
|
+
break;
|
|
851
|
+
} catch (err) {
|
|
852
|
+
if (!isUnavailableExecutableError(err)) throw err;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
624
855
|
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
.
|
|
628
|
-
|
|
629
|
-
.
|
|
630
|
-
|
|
856
|
+
const fallback = output
|
|
857
|
+
? undefined
|
|
858
|
+
: await searchCodeWithNode(query, searchPath, input.glob?.trim(), maxResults, signal);
|
|
859
|
+
const matches = output
|
|
860
|
+
? output.stdout
|
|
861
|
+
.split(/\r?\n/)
|
|
862
|
+
.filter(Boolean)
|
|
863
|
+
.map(parseRgLine)
|
|
864
|
+
.filter((match): match is SearchCodeMatch => !!match)
|
|
865
|
+
.slice(0, maxResults)
|
|
866
|
+
: fallback!.matches;
|
|
631
867
|
|
|
632
868
|
return {
|
|
633
869
|
query,
|
|
634
870
|
path: searchPath,
|
|
635
871
|
...(input.glob?.trim() ? { glob: input.glob.trim() } : {}),
|
|
636
872
|
matches,
|
|
637
|
-
truncated: output
|
|
873
|
+
truncated: output
|
|
874
|
+
? output.truncated || matches.length >= maxResults
|
|
875
|
+
: fallback!.truncated,
|
|
638
876
|
};
|
|
639
877
|
}
|
|
640
878
|
|
package/src/cards.ts
CHANGED
|
@@ -133,11 +133,12 @@ export function buildHelpCard(
|
|
|
133
133
|
`发送 **/new** 创建新会话(使用 /cd 设置的目录,默认 ${defaultToolLabel})`,
|
|
134
134
|
"发送 **/new claude** 创建新 Claude 会话",
|
|
135
135
|
"发送 **/new cursor** 创建新 Cursor 会话",
|
|
136
|
-
"发送 **/new codex** 创建新 Codex 会话",
|
|
136
|
+
"发送 **/new codex** 创建新 Codex 会话",
|
|
137
|
+
"发送 **/new ccc** 创建新 CCC Agent 会话",
|
|
137
138
|
"发送 **/newh** 重置当前会话(沿用当前工作目录,不切换)",
|
|
138
139
|
"发送 **/plan** 以规划模式提问(只读,不执行写操作)",
|
|
139
140
|
"发送 **/ask** 以问答模式提问(只读,不执行写操作)",
|
|
140
|
-
"发送 **/usage**
|
|
141
|
+
"发送 **/usage** 查看当前 Agent 的用量或余额",
|
|
141
142
|
"发送 **/restart** 重启 ChatCCC 进程",
|
|
142
143
|
"发送 **/update** 更新并重启(仅 npm 全局安装可用)",
|
|
143
144
|
ABD_HELP_LINE,
|
|
@@ -152,7 +153,8 @@ export function buildHelpCard(
|
|
|
152
153
|
{ text: `新建默认会话(/new,${defaultToolLabel})`, value: JSON.stringify({ cmd: "new" }), type: "primary" },
|
|
153
154
|
{ text: "新建 Claude 会话(/new claude)", value: JSON.stringify({ cmd: "new claude" }), type: "primary" },
|
|
154
155
|
{ text: "新建 Cursor 会话(/new cursor)", value: JSON.stringify({ cmd: "new cursor" }), type: "primary" },
|
|
155
|
-
{ text: "新建 Codex 会话(/new codex)", value: JSON.stringify({ cmd: "new codex" }), type: "primary" },
|
|
156
|
+
{ text: "新建 Codex 会话(/new codex)", value: JSON.stringify({ cmd: "new codex" }), type: "primary" },
|
|
157
|
+
{ text: "新建 CCC Agent 会话(/new ccc)", value: JSON.stringify({ cmd: "new ccc" }), type: "primary" },
|
|
156
158
|
{ text: "重启 ChatCCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
|
|
157
159
|
{ text: "更新并重启(/update)", value: JSON.stringify({ cmd: "update" }), type: "danger" },
|
|
158
160
|
{ text: "切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
|
|
@@ -344,8 +346,8 @@ export function buildSessionsCard(sessions: Array<{
|
|
|
344
346
|
header: { template: "blue", title: { content: "所有会话", tag: "plain_text" } },
|
|
345
347
|
elements: [
|
|
346
348
|
{ tag: "div", text: { tag: "lark_md", content: fixedPrivateSession
|
|
347
|
-
? `当前没有会话记录。\n\n直接发送普通消息即可创建飞书私聊专属 ${defaultToolLabel} 会话;默认 Agent 变化后,下一条普通消息会创建对应 Agent 的新空会话。发送 **/new**、**/new claude**、**/new cursor** 或 **/new
|
|
348
|
-
: `当前没有会话记录。\n\n使用 **/new**(默认 ${defaultToolLabel})、**/new claude**、**/new cursor** 或 **/new
|
|
349
|
+
? `当前没有会话记录。\n\n直接发送普通消息即可创建飞书私聊专属 ${defaultToolLabel} 会话;默认 Agent 变化后,下一条普通消息会创建对应 Agent 的新空会话。发送 **/new**、**/new claude**、**/new cursor**、**/new codex** 或 **/new ccc** 会另外创建会话群。`
|
|
350
|
+
: `当前没有会话记录。\n\n使用 **/new**(默认 ${defaultToolLabel})、**/new claude**、**/new cursor**、**/new codex** 或 **/new ccc** 创建新会话。\n创建后可在任意会话群内发送 **/sessions** 查看列表,用 **/session 数字** 切换会话。` } },
|
|
349
351
|
{ tag: "hr" },
|
|
350
352
|
{ tag: "action", actions: [{ tag: "button", text: { tag: "plain_text", content: "收起" }, type: "default", value: { action: "close" } }] },
|
|
351
353
|
],
|
package/src/config.ts
CHANGED
|
@@ -104,12 +104,18 @@ export interface CodexConfig {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
export interface CccConfig {
|
|
107
|
+
/** Whether the built-in CCC Agent is available for new sessions. */
|
|
108
|
+
enabled: boolean;
|
|
109
|
+
/** Whether /new without an explicit tool should use CCC Agent. */
|
|
110
|
+
defaultAgent: boolean;
|
|
107
111
|
/** DeepSeek API Key for the ChatCCC self-developed agent. */
|
|
108
112
|
DEEPSEEK_API_KEY: string;
|
|
109
113
|
/** DeepSeek-compatible API Base URL for the ChatCCC self-developed agent. */
|
|
110
114
|
DEEPSEEK_BASE_URL: string;
|
|
111
115
|
/** Model used by the ChatCCC self-developed agent. */
|
|
112
116
|
model: string;
|
|
117
|
+
/** Optional model exposed through /model for manual per-session switching. */
|
|
118
|
+
alternativeModel: string;
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
export interface FeishuConfig {
|
|
@@ -168,8 +174,8 @@ export interface AppConfig {
|
|
|
168
174
|
ccc: CccConfig;
|
|
169
175
|
}
|
|
170
176
|
|
|
171
|
-
export type AgentTool = "claude" | "cursor" | "codex";
|
|
172
|
-
export const AGENT_TOOLS: AgentTool[] = ["claude", "cursor", "codex"];
|
|
177
|
+
export type AgentTool = "claude" | "cursor" | "codex" | "ccc";
|
|
178
|
+
export const AGENT_TOOLS: AgentTool[] = ["claude", "cursor", "codex", "ccc"];
|
|
173
179
|
export type CursorAvatarBatteryMode = "apiPercent" | "onDemandUse";
|
|
174
180
|
|
|
175
181
|
/** 获取指定 agent 配置中所有模型相关的值(最多 100 个,去重) */
|
|
@@ -190,6 +196,7 @@ export function getAllModelsForTool(tool: string, cfg: AppConfig = config): stri
|
|
|
190
196
|
collect(cfg.codex.alternativeModel);
|
|
191
197
|
} else if (tool === "ccc") {
|
|
192
198
|
collect(cfg.ccc.model);
|
|
199
|
+
collect(cfg.ccc.alternativeModel);
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
return Array.from(seen).slice(0, 100);
|
|
@@ -445,7 +452,14 @@ function loadConfig(): AppConfig {
|
|
|
445
452
|
onDemandMonthlyBudget: 1000,
|
|
446
453
|
},
|
|
447
454
|
codex: { enabled: false, defaultAgent: false, path: "", model: "", alternativeModel: "", effort: "", fastMode: false },
|
|
448
|
-
ccc: {
|
|
455
|
+
ccc: {
|
|
456
|
+
enabled: false,
|
|
457
|
+
defaultAgent: false,
|
|
458
|
+
DEEPSEEK_API_KEY: "",
|
|
459
|
+
DEEPSEEK_BASE_URL: DEFAULT_CCC_DEEPSEEK_BASE_URL,
|
|
460
|
+
model: DEFAULT_CCC_MODEL,
|
|
461
|
+
alternativeModel: "",
|
|
462
|
+
},
|
|
449
463
|
};
|
|
450
464
|
|
|
451
465
|
if (!IS_TEST_ENV) {
|
|
@@ -498,7 +512,14 @@ function loadConfig(): AppConfig {
|
|
|
498
512
|
onDemandMonthlyBudget?: unknown;
|
|
499
513
|
};
|
|
500
514
|
codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; alternativeModel?: unknown; effort?: unknown; fastMode?: unknown };
|
|
501
|
-
ccc?: {
|
|
515
|
+
ccc?: {
|
|
516
|
+
enabled?: unknown;
|
|
517
|
+
defaultAgent?: unknown;
|
|
518
|
+
DEEPSEEK_API_KEY?: unknown;
|
|
519
|
+
DEEPSEEK_BASE_URL?: unknown;
|
|
520
|
+
model?: unknown;
|
|
521
|
+
alternativeModel?: unknown;
|
|
522
|
+
};
|
|
502
523
|
webUi?: { openOnStart?: unknown };
|
|
503
524
|
chromeDevtools?: { enabled?: unknown; port?: unknown; chromePath?: unknown };
|
|
504
525
|
rawStreamLogs?: unknown;
|
|
@@ -553,7 +574,7 @@ function loadConfig(): AppConfig {
|
|
|
553
574
|
(typeof cursorRaw.model === "string" && (cursorRaw.model as string).trim()) ||
|
|
554
575
|
(typeof cursorRaw.alternativeModel === "string" && (cursorRaw.alternativeModel as string).trim()),
|
|
555
576
|
);
|
|
556
|
-
const codexNonEmpty = (): boolean =>
|
|
577
|
+
const codexNonEmpty = (): boolean =>
|
|
557
578
|
Boolean(
|
|
558
579
|
(typeof codexRaw.path === "string" && codexRaw.path.trim()) ||
|
|
559
580
|
(typeof codexRaw.command === "string" && (codexRaw.command as string).trim()) ||
|
|
@@ -561,21 +582,28 @@ function loadConfig(): AppConfig {
|
|
|
561
582
|
(typeof codexRaw.alternativeModel === "string" && (codexRaw.alternativeModel as string).trim()) ||
|
|
562
583
|
(typeof codexRaw.effort === "string" && (codexRaw.effort as string).trim()) ||
|
|
563
584
|
codexRaw.fastMode === true,
|
|
564
|
-
);
|
|
585
|
+
);
|
|
586
|
+
// 旧版 ccc 配置没有 enabled。只用 API Key 推断启用,避免 sample 中自带的
|
|
587
|
+
// 默认 Base URL / model 让升级用户在未配置凭证时意外启用 CCC Agent。
|
|
588
|
+
const cccNonEmpty = (): boolean =>
|
|
589
|
+
Boolean(typeof cccRaw.DEEPSEEK_API_KEY === "string" && cccRaw.DEEPSEEK_API_KEY.trim());
|
|
565
590
|
|
|
566
591
|
const claudeEnabled = resolveEnabled(claude.enabled, claudeNonEmpty);
|
|
567
592
|
const cursorEnabled = resolveEnabled(cursorRaw.enabled, cursorNonEmpty);
|
|
568
|
-
const codexEnabled = resolveEnabled(codexRaw.enabled, codexNonEmpty);
|
|
593
|
+
const codexEnabled = resolveEnabled(codexRaw.enabled, codexNonEmpty);
|
|
594
|
+
const cccEnabled = resolveEnabled(cccRaw.enabled, cccNonEmpty);
|
|
569
595
|
const chromeDevtoolsPort = Number(chromeDevtoolsRaw.port);
|
|
570
596
|
const explicitDefaultTool: AgentTool | null =
|
|
571
597
|
typeof claude.defaultAgent === "boolean" && claude.defaultAgent && claudeEnabled ? "claude" :
|
|
572
598
|
typeof cursorRaw.defaultAgent === "boolean" && cursorRaw.defaultAgent && cursorEnabled ? "cursor" :
|
|
573
|
-
typeof codexRaw.defaultAgent === "boolean" && codexRaw.defaultAgent && codexEnabled ? "codex" :
|
|
599
|
+
typeof codexRaw.defaultAgent === "boolean" && codexRaw.defaultAgent && codexEnabled ? "codex" :
|
|
600
|
+
typeof cccRaw.defaultAgent === "boolean" && cccRaw.defaultAgent && cccEnabled ? "ccc" :
|
|
574
601
|
null;
|
|
575
602
|
const fallbackDefaultTool: AgentTool =
|
|
576
603
|
claudeEnabled ? "claude" :
|
|
577
604
|
cursorEnabled ? "cursor" :
|
|
578
|
-
codexEnabled ? "codex" :
|
|
605
|
+
codexEnabled ? "codex" :
|
|
606
|
+
cccEnabled ? "ccc" :
|
|
579
607
|
"claude";
|
|
580
608
|
const defaultTool = explicitDefaultTool ?? fallbackDefaultTool;
|
|
581
609
|
|
|
@@ -655,12 +683,15 @@ function loadConfig(): AppConfig {
|
|
|
655
683
|
fastMode: codexRaw.fastMode === true,
|
|
656
684
|
},
|
|
657
685
|
ccc: {
|
|
686
|
+
enabled: cccEnabled,
|
|
687
|
+
defaultAgent: defaultTool === "ccc",
|
|
658
688
|
DEEPSEEK_API_KEY: normalizeOptionalConfigField(cccRaw.DEEPSEEK_API_KEY, { label: "ccc.DEEPSEEK_API_KEY" }),
|
|
659
689
|
DEEPSEEK_BASE_URL: normalizeOptionalConfigField(cccRaw.DEEPSEEK_BASE_URL, {
|
|
660
690
|
label: "ccc.DEEPSEEK_BASE_URL",
|
|
661
691
|
fallback: DEFAULT_CCC_DEEPSEEK_BASE_URL,
|
|
662
692
|
}),
|
|
663
693
|
model: normalizeOptionalConfigField(cccRaw.model, { label: "ccc.model", fallback: DEFAULT_CCC_MODEL }),
|
|
694
|
+
alternativeModel: normalizeOptionalConfigField(cccRaw.alternativeModel, { label: "ccc.alternativeModel" }),
|
|
664
695
|
},
|
|
665
696
|
};
|
|
666
697
|
}
|
|
@@ -981,7 +1012,7 @@ export const CLAUDE_SESSION_PREFIX = "Claude Code Session:";
|
|
|
981
1012
|
export const CURSOR_SESSION_PREFIX = "Cursor Session:";
|
|
982
1013
|
/** 群描述中用于识别 Codex 会话的前缀 */
|
|
983
1014
|
export const CODEX_SESSION_PREFIX = "Codex Session:";
|
|
984
|
-
/** 群描述中用于识别
|
|
1015
|
+
/** 群描述中用于识别 CCC Agent 会话的前缀 */
|
|
985
1016
|
export const CCC_SESSION_PREFIX = "CCC Session:";
|
|
986
1017
|
|
|
987
1018
|
/** 根据 tool 名称返回对应的群描述前缀 */
|
package/src/index.ts
CHANGED
|
@@ -333,6 +333,19 @@ async function processFeishuMessageEvent(data: Evt): Promise<void> {
|
|
|
333
333
|
if (delayNotice) {
|
|
334
334
|
const delayToken = await getTenantAccessToken();
|
|
335
335
|
await sendCardReply(delayToken, chatId, "延迟送达", delayNotice, "yellow").catch(() => {});
|
|
336
|
+
// 延迟送达的旧消息只发提醒、不转发给 agent 执行:
|
|
337
|
+
// 断线重连或补推时,早已过期的消息不应未经确认就触发新任务。
|
|
338
|
+
// 用户如需执行可自行重发。
|
|
339
|
+
logTrace(traceId, "DONE", {
|
|
340
|
+
outcome: "skip_delayed_feishu_message",
|
|
341
|
+
chatId,
|
|
342
|
+
msgTimestamp,
|
|
343
|
+
delayMinutes: Math.floor((Date.now() - msgTimestamp) / 60000),
|
|
344
|
+
});
|
|
345
|
+
console.log(
|
|
346
|
+
`[${ts()}] [SKIP] Delayed Feishu message not forwarded to agent: messageId=${messageId ?? "(missing id)"} chatId=${chatId} createTime=${msgTimestamp}`,
|
|
347
|
+
);
|
|
348
|
+
return;
|
|
336
349
|
}
|
|
337
350
|
|
|
338
351
|
await handleCommand(
|