chatccc 0.2.272 → 0.2.276
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 +24 -22
- package/config.sample.json +4 -4
- package/deepccc-agent/README.md +147 -79
- package/deepccc-agent/package.json +68 -65
- package/dist/deepccc-agent/src/attachments.js +192 -0
- package/dist/deepccc-agent/src/cli.js +54 -13
- package/dist/deepccc-agent/src/config.js +49 -4
- package/dist/deepccc-agent/src/context.js +299 -16
- package/dist/deepccc-agent/src/file-tools.js +33 -0
- package/dist/deepccc-agent/src/index.js +48 -16
- package/dist/deepccc-agent/src/tool-protocol.js +14 -3
- package/dist/deepccc-agent/src/web-entry.js +72 -0
- package/dist/deepccc-agent/src/web-page.js +414 -0
- package/dist/deepccc-agent/src/web-runtime.js +331 -0
- package/dist/deepccc-agent/src/web-server.js +476 -0
- package/dist/deepccc-agent/src/web-session-store.js +162 -0
- package/dist/deepccc-agent/src/web-tool-presentation.js +123 -0
- package/dist/src/adapters/ccc-adapter.js +1 -0
- package/dist/src/agent-capability-grants.js +26 -0
- package/dist/src/agent-delegate-task.js +5 -2
- package/dist/src/agent-file-rpc.js +6 -1
- package/dist/src/agent-image-rpc.js +6 -1
- package/dist/src/agent-team/web/agent-team-page.js +250 -250
- package/dist/src/cards.js +7 -4
- package/dist/src/im-skills.js +9 -2
- package/dist/src/orchestrator.js +117 -29
- package/dist/src/session-name.js +15 -0
- package/dist/src/session.js +43 -7
- package/dist/src/web-ui.js +68 -51
- package/im-skills/feishu-skill/receive-send-file.md +3 -2
- package/im-skills/feishu-skill/receive-send-image.md +3 -2
- package/im-skills/feishu-skill/send-file.mjs +6 -5
- package/im-skills/feishu-skill/send-image.mjs +6 -5
- package/im-skills/feishu-skill/skill.md +4 -2
- package/package.json +76 -76
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { DEEPCCC_HOME } from "./config.js";
|
|
5
|
+
import { normalizeBuiltinSessionId } from "./context.js";
|
|
6
|
+
export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
7
|
+
export const MAX_ATTACHMENTS_PER_MESSAGE = 10;
|
|
8
|
+
export const ATTACHMENT_PROMPT_START = "<deepccc-attachments>";
|
|
9
|
+
export const ATTACHMENT_PROMPT_END = "</deepccc-attachments>";
|
|
10
|
+
export class AttachmentStore {
|
|
11
|
+
rootDir;
|
|
12
|
+
idFactory;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.rootDir = resolve(options.rootDir ?? join(DEEPCCC_HOME, "attachments"));
|
|
15
|
+
this.idFactory = options.idFactory ?? (() => randomUUID());
|
|
16
|
+
}
|
|
17
|
+
async save(sessionId, input) {
|
|
18
|
+
const bytes = Buffer.from(input.bytes);
|
|
19
|
+
if (!bytes.length)
|
|
20
|
+
throw new Error("Image attachment must not be empty");
|
|
21
|
+
if (bytes.length > MAX_ATTACHMENT_BYTES)
|
|
22
|
+
throw new Error("Image attachment exceeds the 20 MB limit");
|
|
23
|
+
const mimeType = detectImageMime(bytes);
|
|
24
|
+
if (!mimeType)
|
|
25
|
+
throw new Error("Only PNG, JPEG, or WebP image attachments are supported");
|
|
26
|
+
const normalizedSessionId = normalizeBuiltinSessionId(sessionId);
|
|
27
|
+
const attachmentId = normalizeAttachmentId(this.idFactory());
|
|
28
|
+
const extension = extensionForMime(mimeType);
|
|
29
|
+
const fileName = `${attachmentId}${extension}`;
|
|
30
|
+
const sessionDir = this.sessionDir(normalizedSessionId);
|
|
31
|
+
const absolutePath = join(sessionDir, fileName);
|
|
32
|
+
const meta = {
|
|
33
|
+
attachmentId,
|
|
34
|
+
originalName: cleanOriginalName(input.originalName, extension),
|
|
35
|
+
mimeType,
|
|
36
|
+
size: bytes.length,
|
|
37
|
+
absolutePath,
|
|
38
|
+
fileName,
|
|
39
|
+
};
|
|
40
|
+
await mkdir(sessionDir, { recursive: true });
|
|
41
|
+
const dataTemp = `${absolutePath}.${process.pid}.tmp`;
|
|
42
|
+
const metaPath = this.metaPath(normalizedSessionId, attachmentId);
|
|
43
|
+
const metaTemp = `${metaPath}.${process.pid}.tmp`;
|
|
44
|
+
await writeFile(dataTemp, bytes);
|
|
45
|
+
await rename(dataTemp, absolutePath);
|
|
46
|
+
await writeFile(metaTemp, `${JSON.stringify(meta, null, 2)}\n`, "utf8");
|
|
47
|
+
await rename(metaTemp, metaPath);
|
|
48
|
+
return publicMeta(meta);
|
|
49
|
+
}
|
|
50
|
+
async importFile(sessionId, path) {
|
|
51
|
+
const absolutePath = resolve(path);
|
|
52
|
+
const info = await stat(absolutePath).catch(() => null);
|
|
53
|
+
if (!info?.isFile())
|
|
54
|
+
throw new Error(`Image attachment not found: ${path}`);
|
|
55
|
+
if (info.size > MAX_ATTACHMENT_BYTES)
|
|
56
|
+
throw new Error(`Image attachment exceeds the 20 MB limit: ${path}`);
|
|
57
|
+
return this.save(sessionId, {
|
|
58
|
+
originalName: basename(absolutePath),
|
|
59
|
+
bytes: await readFile(absolutePath),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async get(sessionId, attachmentId) {
|
|
63
|
+
const stored = await this.readStored(sessionId, attachmentId);
|
|
64
|
+
return stored ? publicMeta(stored) : null;
|
|
65
|
+
}
|
|
66
|
+
async read(sessionId, attachmentId) {
|
|
67
|
+
const stored = await this.readStored(sessionId, attachmentId);
|
|
68
|
+
if (!stored)
|
|
69
|
+
return null;
|
|
70
|
+
try {
|
|
71
|
+
const bytes = await readFile(stored.absolutePath);
|
|
72
|
+
if (bytes.length !== stored.size || detectImageMime(bytes) !== stored.mimeType)
|
|
73
|
+
return null;
|
|
74
|
+
return { attachment: publicMeta(stored), bytes };
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async delete(sessionId, attachmentId) {
|
|
81
|
+
const stored = await this.readStored(sessionId, attachmentId);
|
|
82
|
+
if (!stored)
|
|
83
|
+
return false;
|
|
84
|
+
await Promise.all([
|
|
85
|
+
unlink(stored.absolutePath).catch(() => { }),
|
|
86
|
+
unlink(this.metaPath(sessionId, attachmentId)).catch(() => { }),
|
|
87
|
+
]);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
async deleteSession(sessionId) {
|
|
91
|
+
await rm(this.sessionDir(sessionId), { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
async readStored(sessionId, attachmentId) {
|
|
94
|
+
const normalizedId = normalizeAttachmentId(attachmentId);
|
|
95
|
+
try {
|
|
96
|
+
const value = JSON.parse(await readFile(this.metaPath(sessionId, normalizedId), "utf8"));
|
|
97
|
+
if (value.attachmentId !== normalizedId || typeof value.fileName !== "string")
|
|
98
|
+
return null;
|
|
99
|
+
if (typeof value.originalName !== "string" || typeof value.size !== "number")
|
|
100
|
+
return null;
|
|
101
|
+
if (value.mimeType !== "image/png" && value.mimeType !== "image/jpeg" && value.mimeType !== "image/webp")
|
|
102
|
+
return null;
|
|
103
|
+
if (value.fileName !== `${normalizedId}${extensionForMime(value.mimeType)}`)
|
|
104
|
+
return null;
|
|
105
|
+
const absolutePath = join(this.sessionDir(sessionId), value.fileName);
|
|
106
|
+
return { ...value, absolutePath };
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
sessionDir(sessionId) {
|
|
113
|
+
return join(this.rootDir, normalizeBuiltinSessionId(sessionId));
|
|
114
|
+
}
|
|
115
|
+
metaPath(sessionId, attachmentId) {
|
|
116
|
+
return join(this.sessionDir(sessionId), `${normalizeAttachmentId(attachmentId)}.json`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function buildAttachmentPrompt(text, attachments) {
|
|
120
|
+
const prompt = text.trim() || "请分析这些图片。";
|
|
121
|
+
if (!attachments.length)
|
|
122
|
+
return prompt;
|
|
123
|
+
const manifest = attachments.map((attachment) => ({
|
|
124
|
+
attachmentId: attachment.attachmentId,
|
|
125
|
+
originalName: attachment.originalName,
|
|
126
|
+
mimeType: attachment.mimeType,
|
|
127
|
+
size: attachment.size,
|
|
128
|
+
absolutePath: attachment.absolutePath,
|
|
129
|
+
}));
|
|
130
|
+
return [
|
|
131
|
+
prompt,
|
|
132
|
+
"",
|
|
133
|
+
ATTACHMENT_PROMPT_START,
|
|
134
|
+
JSON.stringify(manifest),
|
|
135
|
+
ATTACHMENT_PROMPT_END,
|
|
136
|
+
"以上图片以本地附件文件提供。不要假设模型原生支持图片;请使用可用工具读取这些绝对路径并自行处理。",
|
|
137
|
+
].join("\n");
|
|
138
|
+
}
|
|
139
|
+
export function parseAttachmentPrompt(content) {
|
|
140
|
+
const start = content.indexOf(ATTACHMENT_PROMPT_START);
|
|
141
|
+
const end = content.indexOf(ATTACHMENT_PROMPT_END, start + ATTACHMENT_PROMPT_START.length);
|
|
142
|
+
if (start < 0 || end < 0)
|
|
143
|
+
return { text: content, attachments: [] };
|
|
144
|
+
const raw = content.slice(start + ATTACHMENT_PROMPT_START.length, end).trim();
|
|
145
|
+
let attachments = [];
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(raw);
|
|
148
|
+
if (Array.isArray(parsed))
|
|
149
|
+
attachments = parsed.filter(isAttachmentMeta);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return { text: content, attachments: [] };
|
|
153
|
+
}
|
|
154
|
+
return { text: content.slice(0, start).trimEnd(), attachments };
|
|
155
|
+
}
|
|
156
|
+
export function detectImageMime(bytes) {
|
|
157
|
+
const buffer = Buffer.from(bytes);
|
|
158
|
+
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
|
|
159
|
+
return "image/png";
|
|
160
|
+
if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff)
|
|
161
|
+
return "image/jpeg";
|
|
162
|
+
if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP")
|
|
163
|
+
return "image/webp";
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
function extensionForMime(mimeType) {
|
|
167
|
+
return mimeType === "image/png" ? ".png" : mimeType === "image/jpeg" ? ".jpg" : ".webp";
|
|
168
|
+
}
|
|
169
|
+
function cleanOriginalName(value, extension) {
|
|
170
|
+
const cleaned = basename(value || `image${extension}`).replace(/[\u0000-\u001f<>:"/\\|?*]+/g, "_").slice(0, 180);
|
|
171
|
+
return cleaned || `image${extension}`;
|
|
172
|
+
}
|
|
173
|
+
function normalizeAttachmentId(value) {
|
|
174
|
+
const normalized = value.replace(/[^a-zA-Z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
175
|
+
if (!normalized || normalized === "." || normalized === "..")
|
|
176
|
+
throw new Error("Invalid attachment id");
|
|
177
|
+
return normalized;
|
|
178
|
+
}
|
|
179
|
+
function publicMeta(value) {
|
|
180
|
+
const { fileName: _, ...meta } = value;
|
|
181
|
+
return meta;
|
|
182
|
+
}
|
|
183
|
+
function isAttachmentMeta(value) {
|
|
184
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
185
|
+
return false;
|
|
186
|
+
const attachment = value;
|
|
187
|
+
return typeof attachment.attachmentId === "string"
|
|
188
|
+
&& typeof attachment.originalName === "string"
|
|
189
|
+
&& typeof attachment.absolutePath === "string"
|
|
190
|
+
&& typeof attachment.size === "number"
|
|
191
|
+
&& (attachment.mimeType === "image/png" || attachment.mimeType === "image/jpeg" || attachment.mimeType === "image/webp");
|
|
192
|
+
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* DeepCCC terminal REPL and JSONL streaming entrypoint — 同步自 ChatCCC
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
* node bin/deepccc.mjs
|
|
6
|
-
* node bin/deepccc.mjs --model deepseek-v4-pro
|
|
7
|
-
* node bin/deepccc.mjs --stream-json --prompt "hello"
|
|
5
|
+
* node bin/deepccc-cli.mjs
|
|
6
|
+
* node bin/deepccc-cli.mjs --model deepseek-v4-pro
|
|
7
|
+
* node bin/deepccc-cli.mjs --stream-json --prompt "hello"
|
|
8
8
|
*
|
|
9
9
|
* 交互模式(TTY)下,单轮回复渲染为固定"过程区块":状态行 + 折叠工具行 +
|
|
10
10
|
* 原地更新正文,不再滚屏刷 JSON;完成/停止/异常后定型留在屏幕上。
|
|
@@ -24,6 +24,7 @@ import { reduceProgress } from "./progress/reducer.js";
|
|
|
24
24
|
import { TerminalProgressRenderer } from "./progress/terminal-renderer.js";
|
|
25
25
|
import { progressView } from "./progress/view.js";
|
|
26
26
|
import { defaultLogDir, setupFileLogging } from "./file-log.js";
|
|
27
|
+
import { AttachmentStore, buildAttachmentPrompt } from "./attachments.js";
|
|
27
28
|
function parsePositiveIntegerOption(name, value) {
|
|
28
29
|
const parsed = Number(value);
|
|
29
30
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
@@ -46,6 +47,7 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
46
47
|
let streamJson = false;
|
|
47
48
|
let prompt = null;
|
|
48
49
|
let plain = false;
|
|
50
|
+
const images = [];
|
|
49
51
|
for (let i = 0; i < argv.length; i++) {
|
|
50
52
|
const arg = argv[i];
|
|
51
53
|
const next = argv[i + 1];
|
|
@@ -104,6 +106,10 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
104
106
|
prompt = next;
|
|
105
107
|
i++;
|
|
106
108
|
}
|
|
109
|
+
else if (arg === "--image" && next !== undefined) {
|
|
110
|
+
images.push(next);
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
107
113
|
else if (arg === "--plain") {
|
|
108
114
|
plain = true;
|
|
109
115
|
}
|
|
@@ -115,7 +121,7 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
115
121
|
help = true;
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
|
-
return { config, options, listSessions, resume, help, streamJson, prompt, plain };
|
|
124
|
+
return { config, options, listSessions, resume, help, streamJson, prompt, images, plain };
|
|
119
125
|
}
|
|
120
126
|
async function loadRuntime() {
|
|
121
127
|
const [{ ChatSession }, { config: appConfig }] = await Promise.all([
|
|
@@ -128,7 +134,7 @@ function printHelp(appConfig) {
|
|
|
128
134
|
console.log([
|
|
129
135
|
"DeepCCC terminal agent",
|
|
130
136
|
"",
|
|
131
|
-
"Usage: deepccc [options]",
|
|
137
|
+
"Usage: deepccc-cli [options]",
|
|
132
138
|
"",
|
|
133
139
|
"Options:",
|
|
134
140
|
` --provider <name> API protocol: openai or anthropic (current default ${appConfig.provider})`,
|
|
@@ -144,6 +150,7 @@ function printHelp(appConfig) {
|
|
|
144
150
|
" --list-sessions List saved sessions and exit",
|
|
145
151
|
" --stream-json One-shot mode: write JSONL events to stdout",
|
|
146
152
|
" --prompt <text> Prompt text for --stream-json",
|
|
153
|
+
" --image <path> Attach a PNG/JPEG/WebP file (repeatable; copied to the session attachment store)",
|
|
147
154
|
" --plain Force plain streaming output (no progress block renderer)",
|
|
148
155
|
" --dangerously-bypass-permissions Skip all permission prompts (aligns with chatccc's bypass mode)",
|
|
149
156
|
" --help, -h Show help",
|
|
@@ -276,8 +283,8 @@ async function runStreamJson(args) {
|
|
|
276
283
|
return 0;
|
|
277
284
|
}
|
|
278
285
|
const prompt = args.prompt ?? (!process.stdin.isTTY ? await readPromptFromStdin() : "");
|
|
279
|
-
if (!prompt.trim()) {
|
|
280
|
-
writeJsonLine({ type: "error", message: "--stream-json requires --prompt <text
|
|
286
|
+
if (!prompt.trim() && !args.images.length) {
|
|
287
|
+
writeJsonLine({ type: "error", message: "--stream-json requires --prompt <text>, stdin input, or --image <path>" });
|
|
281
288
|
return 1;
|
|
282
289
|
}
|
|
283
290
|
let runtime;
|
|
@@ -310,15 +317,29 @@ async function runStreamJson(args) {
|
|
|
310
317
|
writeJsonLine({ type: "error", message: err.message });
|
|
311
318
|
return 1;
|
|
312
319
|
}
|
|
320
|
+
let attachments;
|
|
321
|
+
try {
|
|
322
|
+
attachments = await importCliAttachments(resolvedSession.sessionId, args.images);
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
writeJsonLine({ type: "error", message: err.message });
|
|
326
|
+
return 1;
|
|
327
|
+
}
|
|
328
|
+
const promptWithAttachments = buildAttachmentPrompt(prompt, attachments);
|
|
313
329
|
writeJsonLine({
|
|
314
330
|
type: "start",
|
|
315
331
|
session_id: resolvedSession.sessionId,
|
|
316
332
|
mode: resolvedSession.mode,
|
|
317
333
|
cwd,
|
|
318
334
|
model: args.config.model ?? runtime.appConfig.model,
|
|
335
|
+
attachments: attachments.map((attachment) => ({
|
|
336
|
+
attachment_id: attachment.attachmentId,
|
|
337
|
+
name: attachment.originalName,
|
|
338
|
+
path: attachment.absolutePath,
|
|
339
|
+
})),
|
|
319
340
|
});
|
|
320
341
|
try {
|
|
321
|
-
for await (const event of session.chat(
|
|
342
|
+
for await (const event of session.chat(promptWithAttachments)) {
|
|
322
343
|
streamJsonEvent(event);
|
|
323
344
|
}
|
|
324
345
|
return 0;
|
|
@@ -425,13 +446,24 @@ async function runRepl(args) {
|
|
|
425
446
|
console.error(`${C.yellow}${err.message}${C.reset}`);
|
|
426
447
|
process.exit(1);
|
|
427
448
|
}
|
|
449
|
+
let pendingAttachments;
|
|
450
|
+
try {
|
|
451
|
+
pendingAttachments = await importCliAttachments(resolvedSession.sessionId, args.images);
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
console.error(`${C.yellow}${err.message}${C.reset}`);
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
if (pendingAttachments.length) {
|
|
458
|
+
console.log(`${C.dim}Attachments queued: ${pendingAttachments.map((attachment) => attachment.originalName).join(", ")}${C.reset}`);
|
|
459
|
+
}
|
|
428
460
|
let currentAbort = null;
|
|
429
461
|
const ctrlCState = createCtrlCState();
|
|
430
462
|
rl.prompt();
|
|
431
463
|
rl.on("line", async (line) => {
|
|
432
464
|
ctrlCState.reset();
|
|
433
465
|
const input = line.trim();
|
|
434
|
-
if (!input) {
|
|
466
|
+
if (!input && !pendingAttachments.length) {
|
|
435
467
|
rl.prompt();
|
|
436
468
|
return;
|
|
437
469
|
}
|
|
@@ -475,8 +507,10 @@ async function runRepl(args) {
|
|
|
475
507
|
}
|
|
476
508
|
let rendererEnded = false;
|
|
477
509
|
try {
|
|
510
|
+
const chatInput = buildAttachmentPrompt(input, pendingAttachments);
|
|
511
|
+
pendingAttachments = [];
|
|
478
512
|
let lastAccumulated = "";
|
|
479
|
-
for await (const event of session.chat(
|
|
513
|
+
for await (const event of session.chat(chatInput, signal)) {
|
|
480
514
|
if (renderer && view) {
|
|
481
515
|
view = reduceProgress(view, event);
|
|
482
516
|
if (event.type === "text" || event.type === "compact" || event.type === "status") {
|
|
@@ -564,8 +598,15 @@ async function runRepl(args) {
|
|
|
564
598
|
process.exit(0);
|
|
565
599
|
});
|
|
566
600
|
}
|
|
601
|
+
async function importCliAttachments(sessionId, paths) {
|
|
602
|
+
const store = new AttachmentStore();
|
|
603
|
+
const attachments = [];
|
|
604
|
+
for (const path of paths)
|
|
605
|
+
attachments.push(await store.importFile(sessionId, path));
|
|
606
|
+
return attachments;
|
|
607
|
+
}
|
|
567
608
|
/**
|
|
568
|
-
* skill create 子命令:deepccc skill create <name> [--scope global|project] [--description "..."]
|
|
609
|
+
* skill create 子命令:deepccc-cli skill create <name> [--scope global|project] [--description "..."]
|
|
569
610
|
* 默认创建为全局技能(~/.deepccc/skills/<name>/SKILL.md,Codex 结构);
|
|
570
611
|
* --scope project 创建为项目技能(<cwd>/.deepccc/skills/<name>/SKILL.md)。
|
|
571
612
|
* 新技能在下一次对话自动生效(技能索引每次 chat() 前重扫)。
|
|
@@ -574,7 +615,7 @@ function runSkillCreate(argv) {
|
|
|
574
615
|
const positional = argv.filter((a) => !a.startsWith("--"));
|
|
575
616
|
const name = positional[0];
|
|
576
617
|
if (!name) {
|
|
577
|
-
console.error("usage: deepccc skill create <name> [--scope global|project] [--description \"...\"]");
|
|
618
|
+
console.error("usage: deepccc-cli skill create <name> [--scope global|project] [--description \"...\"]");
|
|
578
619
|
process.exit(1);
|
|
579
620
|
}
|
|
580
621
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
|
|
@@ -608,7 +649,7 @@ function runSkillCreate(argv) {
|
|
|
608
649
|
console.log("hot reload: 下一次对话自动生效,无需重启");
|
|
609
650
|
}
|
|
610
651
|
async function main() {
|
|
611
|
-
// skill create 子命令:deepccc skill create <name> [--scope global|project] [--description "..."]
|
|
652
|
+
// skill create 子命令:deepccc-cli skill create <name> [--scope global|project] [--description "..."]
|
|
612
653
|
// 默认创建在全局 ~/.deepccc/skills(Codex 目录结构),--scope project 创建到 <cwd>/.deepccc/skills。
|
|
613
654
|
if (process.argv[2] === "skill" && process.argv[3] === "create") {
|
|
614
655
|
runSkillCreate(process.argv.slice(4));
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
export const DEEPCCC_HOME = join(homedir(), ".deepccc");
|
|
5
5
|
export const RAW_STREAM_LOGS_DIR = join(DEEPCCC_HOME, "raw-stream-logs");
|
|
6
|
-
const CONFIG_PATH = join(DEEPCCC_HOME, "config.json");
|
|
6
|
+
export const CONFIG_PATH = join(DEEPCCC_HOME, "config.json");
|
|
7
7
|
/**
|
|
8
8
|
* 默认配置(不读环境/文件)。导出供测试断言默认值;运行时请用 loadConfig 结果。
|
|
9
9
|
*/
|
|
@@ -31,8 +31,12 @@ export const DEFAULT_CONFIG = {
|
|
|
31
31
|
retentionDays: 7,
|
|
32
32
|
keepCompleted: false,
|
|
33
33
|
},
|
|
34
|
+
web: {
|
|
35
|
+
port: 28_080,
|
|
36
|
+
openOnStart: true,
|
|
37
|
+
},
|
|
34
38
|
};
|
|
35
|
-
function readConfigFile() {
|
|
39
|
+
export function readConfigFile() {
|
|
36
40
|
if (!existsSync(CONFIG_PATH))
|
|
37
41
|
return {};
|
|
38
42
|
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
@@ -70,13 +74,14 @@ export function normalizeDeepCccProvider(value) {
|
|
|
70
74
|
return normalized;
|
|
71
75
|
throw new Error(`DEEPCCC_PROVIDER/provider must be "openai" or "anthropic", received: ${String(value)}`);
|
|
72
76
|
}
|
|
73
|
-
function loadConfig() {
|
|
77
|
+
export function loadConfig() {
|
|
74
78
|
const file = readConfigFile();
|
|
75
79
|
const rawLogs = file.rawStreamLogs && typeof file.rawStreamLogs === "object"
|
|
76
80
|
? file.rawStreamLogs
|
|
77
81
|
: {};
|
|
78
82
|
const git = file.git && typeof file.git === "object" ? file.git : {};
|
|
79
83
|
const coAuthor = git.coAuthor && typeof git.coAuthor === "object" ? git.coAuthor : {};
|
|
84
|
+
const web = file.web && typeof file.web === "object" ? file.web : {};
|
|
80
85
|
return {
|
|
81
86
|
provider: normalizeDeepCccProvider(env("DEEPCCC_PROVIDER") ?? file.provider ?? DEFAULT_CONFIG.provider),
|
|
82
87
|
apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
|
|
@@ -101,9 +106,49 @@ function loadConfig() {
|
|
|
101
106
|
retentionDays: numberEnv("DEEPCCC_RAW_STREAM_RETENTION_DAYS") ?? rawLogs.retentionDays ?? DEFAULT_CONFIG.rawStreamLogs.retentionDays,
|
|
102
107
|
keepCompleted: boolEnv("DEEPCCC_RAW_STREAM_KEEP_COMPLETED") ?? rawLogs.keepCompleted ?? DEFAULT_CONFIG.rawStreamLogs.keepCompleted,
|
|
103
108
|
},
|
|
109
|
+
web: {
|
|
110
|
+
port: normalizeWebPort(web.port),
|
|
111
|
+
openOnStart: web.openOnStart ?? DEFAULT_CONFIG.web.openOnStart,
|
|
112
|
+
},
|
|
104
113
|
};
|
|
105
114
|
}
|
|
106
115
|
export function ensureConfigDir() {
|
|
107
116
|
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
108
117
|
}
|
|
118
|
+
/** Persist only user-editable Web fields while preserving advanced configuration. */
|
|
119
|
+
export function saveConfigPatch(patch) {
|
|
120
|
+
const existing = readConfigFile();
|
|
121
|
+
const { web: webPatch, ...flatPatch } = patch;
|
|
122
|
+
const next = {
|
|
123
|
+
...existing,
|
|
124
|
+
...flatPatch,
|
|
125
|
+
...(webPatch ? { web: { ...DEFAULT_CONFIG.web, ...(existing.web ?? {}), ...webPatch } } : {}),
|
|
126
|
+
};
|
|
127
|
+
if (patch.provider !== undefined)
|
|
128
|
+
next.provider = normalizeDeepCccProvider(patch.provider);
|
|
129
|
+
if (patch.baseURL !== undefined && !patch.baseURL.trim())
|
|
130
|
+
throw new Error("baseURL must not be empty");
|
|
131
|
+
if (patch.model !== undefined && !patch.model.trim())
|
|
132
|
+
throw new Error("model must not be empty");
|
|
133
|
+
if (patch.effort !== undefined && !["", "none", "minimal", "low", "medium", "high", "xhigh", "max"].includes(patch.effort)) {
|
|
134
|
+
throw new Error(`Unsupported effort: ${patch.effort}`);
|
|
135
|
+
}
|
|
136
|
+
if (patch.contextWindow !== undefined && (!Number.isInteger(patch.contextWindow) || patch.contextWindow <= 0)) {
|
|
137
|
+
throw new Error("contextWindow must be a positive integer");
|
|
138
|
+
}
|
|
139
|
+
if (webPatch?.port !== undefined) {
|
|
140
|
+
next.web = { ...(next.web ?? DEFAULT_CONFIG.web), port: normalizeWebPort(webPatch.port) };
|
|
141
|
+
}
|
|
142
|
+
ensureConfigDir();
|
|
143
|
+
const tempPath = `${CONFIG_PATH}.${process.pid}.tmp`;
|
|
144
|
+
writeFileSync(tempPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
145
|
+
renameSync(tempPath, CONFIG_PATH);
|
|
146
|
+
return loadConfig();
|
|
147
|
+
}
|
|
148
|
+
function normalizeWebPort(value) {
|
|
149
|
+
const parsed = Number(value ?? DEFAULT_CONFIG.web.port);
|
|
150
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65_535)
|
|
151
|
+
return DEFAULT_CONFIG.web.port;
|
|
152
|
+
return parsed;
|
|
153
|
+
}
|
|
109
154
|
export const config = loadConfig();
|