chatccc 0.2.270 → 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 +16 -10
- package/config.sample.json +4 -3
- package/deepccc-agent/README.md +147 -61
- package/deepccc-agent/package.json +5 -2
- package/dist/deepccc-agent/src/attachments.js +192 -0
- package/dist/deepccc-agent/src/cli.js +59 -13
- package/dist/deepccc-agent/src/config.js +57 -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 +68 -21
- 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 +5 -1
- 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/application/task-execution-service.js +330 -97
- package/dist/src/agent-team/domain/task-run.js +14 -1
- package/dist/src/agent-team/infrastructure/task-execution-runtime.js +7 -2
- package/dist/src/agent-team/main-agent-bootstrap.js +24 -1
- package/dist/src/agent-team/repositories/json-task-run-repository.js +22 -4
- package/dist/src/agent-team/web/agent-team-page.js +14 -7
- package/dist/src/cards.js +7 -4
- package/dist/src/config.js +12 -0
- package/dist/src/im-skills.js +9 -2
- package/dist/src/orchestrator.js +117 -29
- package/dist/src/safe-maintenance.js +4 -1
- package/dist/src/session-name.js +15 -0
- package/dist/src/session.js +54 -9
- package/dist/src/web-ui.js +76 -32
- 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 +1 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { AttachmentStore, buildAttachmentPrompt, detectImageMime, MAX_ATTACHMENTS_PER_MESSAGE, MAX_ATTACHMENT_BYTES, } from "./attachments.js";
|
|
5
|
+
import { ChatSession } from "./index.js";
|
|
6
|
+
import { DEFAULT_BUILTIN_CONTEXT_DIR, normalizeBuiltinSessionId, readBuiltinContextState } from "./context.js";
|
|
7
|
+
import { WebSessionStore, } from "./web-session-store.js";
|
|
8
|
+
export class DeepCccWebRuntime {
|
|
9
|
+
store;
|
|
10
|
+
attachmentStore;
|
|
11
|
+
loadConfig;
|
|
12
|
+
sessionFactory;
|
|
13
|
+
approvalTimeoutMs;
|
|
14
|
+
now;
|
|
15
|
+
idFactory;
|
|
16
|
+
agents = new Map();
|
|
17
|
+
activeRuns = new Map();
|
|
18
|
+
startingRuns = new Map();
|
|
19
|
+
sessionMutations = new Set();
|
|
20
|
+
events = new Map();
|
|
21
|
+
listeners = new Map();
|
|
22
|
+
globalListeners = new Set();
|
|
23
|
+
approvals = new Map();
|
|
24
|
+
nextEventId = 1;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.store = options.store ?? new WebSessionStore();
|
|
27
|
+
this.attachmentStore = options.attachmentStore ?? new AttachmentStore(resolve(this.store.rootDir) === resolve(DEFAULT_BUILTIN_CONTEXT_DIR)
|
|
28
|
+
? {}
|
|
29
|
+
: { rootDir: join(this.store.rootDir, ".attachments") });
|
|
30
|
+
this.loadConfig = options.loadConfig;
|
|
31
|
+
this.approvalTimeoutMs = options.approvalTimeoutMs ?? 5 * 60_000;
|
|
32
|
+
this.now = options.now ?? (() => new Date());
|
|
33
|
+
this.idFactory = options.idFactory ?? randomUUID;
|
|
34
|
+
this.sessionFactory = options.sessionFactory ?? ((input) => new ChatSession({
|
|
35
|
+
provider: input.config.provider,
|
|
36
|
+
apiKey: input.config.apiKey,
|
|
37
|
+
baseURL: input.config.baseURL,
|
|
38
|
+
model: input.meta.model || input.config.model,
|
|
39
|
+
subModel: input.meta.subModel || input.config.subModel,
|
|
40
|
+
effort: input.meta.effort || input.config.effort,
|
|
41
|
+
maxOutputTokens: input.config.maxOutputTokens,
|
|
42
|
+
streaming: input.config.streaming,
|
|
43
|
+
}, {
|
|
44
|
+
cwd: input.meta.cwd,
|
|
45
|
+
persist: true,
|
|
46
|
+
sessionId: input.meta.sessionId,
|
|
47
|
+
contextWindow: input.config.contextWindow,
|
|
48
|
+
permissionMode: "ask",
|
|
49
|
+
permissionResolver: input.permissionResolver,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
async listSessions() {
|
|
53
|
+
const sessions = await this.store.list();
|
|
54
|
+
return sessions.map((meta) => ({ ...meta, status: this.activeRuns.has(meta.sessionId) ? "running" : "idle" }));
|
|
55
|
+
}
|
|
56
|
+
async createSession(input) {
|
|
57
|
+
const config = this.loadConfig();
|
|
58
|
+
const session = await this.store.create({
|
|
59
|
+
...input,
|
|
60
|
+
model: input.model ?? config.model,
|
|
61
|
+
subModel: input.subModel ?? config.subModel,
|
|
62
|
+
effort: input.effort ?? config.effort,
|
|
63
|
+
});
|
|
64
|
+
this.emit(session.sessionId, "session_updated", session);
|
|
65
|
+
return session;
|
|
66
|
+
}
|
|
67
|
+
async getSession(sessionId) {
|
|
68
|
+
const meta = await this.requireSession(sessionId);
|
|
69
|
+
const context = readBuiltinContextState(sessionId, this.store.rootDir);
|
|
70
|
+
const pendingApproval = [...this.approvals.values()].find((entry) => entry.approval.sessionId === sessionId)?.approval ?? null;
|
|
71
|
+
return {
|
|
72
|
+
...meta,
|
|
73
|
+
status: this.activeRuns.has(sessionId) ? "running" : "idle",
|
|
74
|
+
runId: this.activeRuns.get(sessionId)?.runId ?? null,
|
|
75
|
+
messages: context?.messages ?? [],
|
|
76
|
+
summary: context?.summary ?? "",
|
|
77
|
+
events: this.events.get(sessionId) ?? [],
|
|
78
|
+
pendingApproval,
|
|
79
|
+
approvals: meta.approvals,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async updateSession(sessionId, patch) {
|
|
83
|
+
if (this.activeRuns.has(sessionId) || this.startingRuns.has(sessionId) || this.sessionMutations.has(sessionId)) {
|
|
84
|
+
throw new Error("Cannot change session settings while another session operation is running");
|
|
85
|
+
}
|
|
86
|
+
this.sessionMutations.add(sessionId);
|
|
87
|
+
try {
|
|
88
|
+
const updated = await this.store.update(sessionId, patch);
|
|
89
|
+
this.agents.delete(sessionId);
|
|
90
|
+
this.emit(sessionId, "session_updated", updated);
|
|
91
|
+
return updated;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
this.sessionMutations.delete(sessionId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async deleteSession(sessionId) {
|
|
98
|
+
if (this.activeRuns.has(sessionId) || this.startingRuns.has(sessionId) || this.sessionMutations.has(sessionId)) {
|
|
99
|
+
throw new Error("Stop the running session before deleting it");
|
|
100
|
+
}
|
|
101
|
+
this.sessionMutations.add(sessionId);
|
|
102
|
+
try {
|
|
103
|
+
this.agents.delete(sessionId);
|
|
104
|
+
const deleted = await this.store.delete(sessionId);
|
|
105
|
+
if (deleted) {
|
|
106
|
+
await this.attachmentStore.deleteSession(sessionId);
|
|
107
|
+
this.emit(sessionId, "session_deleted", { sessionId });
|
|
108
|
+
this.events.delete(sessionId);
|
|
109
|
+
}
|
|
110
|
+
return deleted;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
this.sessionMutations.delete(sessionId);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async sendMessage(sessionId, text, attachmentIds = []) {
|
|
117
|
+
const displayPrompt = text.trim();
|
|
118
|
+
const uniqueAttachmentIds = [...new Set(attachmentIds)];
|
|
119
|
+
if (uniqueAttachmentIds.length > MAX_ATTACHMENTS_PER_MESSAGE) {
|
|
120
|
+
throw new Error(`A message can include at most ${MAX_ATTACHMENTS_PER_MESSAGE} images`);
|
|
121
|
+
}
|
|
122
|
+
if (!displayPrompt && !uniqueAttachmentIds.length)
|
|
123
|
+
throw new Error("Message must not be empty");
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
if (this.activeRuns.has(sessionId) || this.startingRuns.has(sessionId) || this.sessionMutations.has(sessionId)) {
|
|
126
|
+
throw new Error("This session is already running");
|
|
127
|
+
}
|
|
128
|
+
this.startingRuns.set(sessionId, controller);
|
|
129
|
+
try {
|
|
130
|
+
let meta = await this.requireSession(sessionId);
|
|
131
|
+
const attachments = [];
|
|
132
|
+
for (const attachmentId of uniqueAttachmentIds) {
|
|
133
|
+
const attachment = await this.attachmentStore.get(sessionId, attachmentId);
|
|
134
|
+
if (!attachment)
|
|
135
|
+
throw new Error(`Image attachment not found: ${attachmentId}`);
|
|
136
|
+
attachments.push(attachment);
|
|
137
|
+
}
|
|
138
|
+
const prompt = buildAttachmentPrompt(displayPrompt, attachments);
|
|
139
|
+
if (meta.title === "新会话" || meta.title === "New session" || meta.title === meta.cwd.split(/[\\/]/).at(-1)) {
|
|
140
|
+
const title = displayPrompt || attachments.map((attachment) => attachment.originalName).join(", ") || "图片任务";
|
|
141
|
+
meta = await this.store.update(sessionId, { title: title.replace(/\s+/g, " ").slice(0, 42) });
|
|
142
|
+
}
|
|
143
|
+
if (controller.signal.aborted)
|
|
144
|
+
throw new DOMException("The session start was stopped", "AbortError");
|
|
145
|
+
const runId = `run-${this.idFactory()}`;
|
|
146
|
+
this.events.set(sessionId, []);
|
|
147
|
+
this.emit(sessionId, "user", { text: displayPrompt || "请分析这些图片。", attachments });
|
|
148
|
+
const promise = Promise.resolve()
|
|
149
|
+
.then(() => this.run(meta, prompt, runId, controller.signal))
|
|
150
|
+
.finally(() => {
|
|
151
|
+
this.activeRuns.delete(sessionId);
|
|
152
|
+
this.emit(sessionId, "session_updated", { sessionId, status: "idle" });
|
|
153
|
+
});
|
|
154
|
+
this.activeRuns.set(sessionId, { runId, controller, promise });
|
|
155
|
+
this.emit(sessionId, "run_started", { runId });
|
|
156
|
+
void promise.catch(() => { });
|
|
157
|
+
return { runId };
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
this.startingRuns.delete(sessionId);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async stopSession(sessionId) {
|
|
164
|
+
const active = this.activeRuns.get(sessionId);
|
|
165
|
+
const starting = this.startingRuns.get(sessionId);
|
|
166
|
+
if (!active && !starting)
|
|
167
|
+
return false;
|
|
168
|
+
starting?.abort();
|
|
169
|
+
if (!active)
|
|
170
|
+
return true;
|
|
171
|
+
active.controller.abort();
|
|
172
|
+
const pending = [...this.approvals.entries()]
|
|
173
|
+
.filter(([, waiter]) => waiter.approval.sessionId === sessionId);
|
|
174
|
+
await Promise.all(pending.map(([approvalId, waiter]) => this.finishApproval(approvalId, waiter, "deny", { stopped: true }).catch(() => { })));
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
async addAttachment(sessionId, input) {
|
|
178
|
+
await this.requireSession(sessionId);
|
|
179
|
+
return this.attachmentStore.save(sessionId, input);
|
|
180
|
+
}
|
|
181
|
+
async readAttachment(sessionId, attachmentId) {
|
|
182
|
+
await this.requireSession(sessionId);
|
|
183
|
+
return this.attachmentStore.read(sessionId, attachmentId);
|
|
184
|
+
}
|
|
185
|
+
async deleteAttachment(sessionId, attachmentId) {
|
|
186
|
+
await this.requireSession(sessionId);
|
|
187
|
+
return this.attachmentStore.delete(sessionId, attachmentId);
|
|
188
|
+
}
|
|
189
|
+
async readArtifact(sessionId, value) {
|
|
190
|
+
const meta = await this.requireSession(sessionId);
|
|
191
|
+
const path = await realpath(resolve(value)).catch(() => null);
|
|
192
|
+
if (!path)
|
|
193
|
+
return null;
|
|
194
|
+
const workspaceRoot = await realpath(meta.cwd).catch(() => resolve(meta.cwd));
|
|
195
|
+
const attachmentDir = join(this.attachmentStore.rootDir, normalizeBuiltinSessionId(sessionId));
|
|
196
|
+
const attachmentRoot = await realpath(attachmentDir).catch(() => resolve(attachmentDir));
|
|
197
|
+
if (!isPathInside(workspaceRoot, path) && !isPathInside(attachmentRoot, path)) {
|
|
198
|
+
throw new Error("Artifact path is outside the session workspace and attachment directory");
|
|
199
|
+
}
|
|
200
|
+
const info = await stat(path).catch(() => null);
|
|
201
|
+
if (!info?.isFile())
|
|
202
|
+
return null;
|
|
203
|
+
if (info.size > MAX_ATTACHMENT_BYTES)
|
|
204
|
+
throw new Error("Artifact image exceeds the 20 MB limit");
|
|
205
|
+
const bytes = await readFile(path);
|
|
206
|
+
const mimeType = detectImageMime(bytes);
|
|
207
|
+
if (!mimeType)
|
|
208
|
+
throw new Error("Artifact is not a supported PNG, JPEG, or WebP image");
|
|
209
|
+
return { path, mimeType, size: bytes.length, bytes };
|
|
210
|
+
}
|
|
211
|
+
async waitForIdle(sessionId) {
|
|
212
|
+
await this.activeRuns.get(sessionId)?.promise;
|
|
213
|
+
}
|
|
214
|
+
async resolveApproval(approvalId, answer) {
|
|
215
|
+
const waiter = this.approvals.get(approvalId);
|
|
216
|
+
if (!waiter)
|
|
217
|
+
return false;
|
|
218
|
+
await this.finishApproval(approvalId, waiter, answer);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
subscribe(sessionId, listener) {
|
|
222
|
+
const set = this.listeners.get(sessionId) ?? new Set();
|
|
223
|
+
set.add(listener);
|
|
224
|
+
this.listeners.set(sessionId, set);
|
|
225
|
+
return () => {
|
|
226
|
+
set.delete(listener);
|
|
227
|
+
if (!set.size)
|
|
228
|
+
this.listeners.delete(sessionId);
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
subscribeAll(listener) {
|
|
232
|
+
this.globalListeners.add(listener);
|
|
233
|
+
return () => this.globalListeners.delete(listener);
|
|
234
|
+
}
|
|
235
|
+
async run(meta, prompt, runId, signal) {
|
|
236
|
+
try {
|
|
237
|
+
const agent = this.agents.get(meta.sessionId) ?? this.createAgent(meta);
|
|
238
|
+
this.agents.set(meta.sessionId, agent);
|
|
239
|
+
for await (const event of agent.chat(prompt, signal))
|
|
240
|
+
this.emit(meta.sessionId, "agent", event);
|
|
241
|
+
this.emit(meta.sessionId, "run_finished", { runId, outcome: signal.aborted ? "stopped" : "done" });
|
|
242
|
+
await this.store.update(meta.sessionId, {});
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
const stopped = signal.aborted || (err instanceof Error && err.name === "AbortError");
|
|
246
|
+
this.emit(meta.sessionId, "agent", { type: "error", message: stopped ? "已停止" : err.message });
|
|
247
|
+
this.emit(meta.sessionId, "run_finished", { runId, outcome: stopped ? "stopped" : "error" });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
createAgent(meta) {
|
|
251
|
+
return this.sessionFactory({
|
|
252
|
+
meta,
|
|
253
|
+
config: this.loadConfig(),
|
|
254
|
+
permissionResolver: (request) => this.requestApproval(meta.sessionId, request),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
requestApproval(sessionId, request) {
|
|
258
|
+
const approvalId = `approval-${this.idFactory()}`;
|
|
259
|
+
const approval = {
|
|
260
|
+
...request,
|
|
261
|
+
approvalId,
|
|
262
|
+
sessionId,
|
|
263
|
+
createdAt: this.now().toISOString(),
|
|
264
|
+
};
|
|
265
|
+
return new Promise((resolve) => {
|
|
266
|
+
const ready = this.store.addApproval(sessionId, {
|
|
267
|
+
...approval,
|
|
268
|
+
status: "pending",
|
|
269
|
+
});
|
|
270
|
+
let waiter;
|
|
271
|
+
const timeout = setTimeout(() => {
|
|
272
|
+
void this.finishApproval(approvalId, waiter, "deny", { timedOut: true }).catch(() => { });
|
|
273
|
+
}, this.approvalTimeoutMs);
|
|
274
|
+
timeout.unref?.();
|
|
275
|
+
waiter = { approval, resolve, timeout, ready };
|
|
276
|
+
this.approvals.set(approvalId, waiter);
|
|
277
|
+
void ready.then(() => {
|
|
278
|
+
if (this.approvals.get(approvalId) !== waiter)
|
|
279
|
+
return;
|
|
280
|
+
this.emit(sessionId, "approval", approval);
|
|
281
|
+
}).catch(() => {
|
|
282
|
+
if (this.approvals.get(approvalId) !== waiter)
|
|
283
|
+
return;
|
|
284
|
+
clearTimeout(timeout);
|
|
285
|
+
this.approvals.delete(approvalId);
|
|
286
|
+
resolve("deny");
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
async finishApproval(approvalId, waiter, answer, extra = {}) {
|
|
291
|
+
if (this.approvals.get(approvalId) !== waiter)
|
|
292
|
+
return;
|
|
293
|
+
clearTimeout(waiter.timeout);
|
|
294
|
+
this.approvals.delete(approvalId);
|
|
295
|
+
try {
|
|
296
|
+
await waiter.ready;
|
|
297
|
+
await this.store.resolveApproval(waiter.approval.sessionId, approvalId, answer);
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
waiter.resolve(answer);
|
|
301
|
+
this.emit(waiter.approval.sessionId, "approval_resolved", { approvalId, answer, ...extra });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
emit(sessionId, type, data) {
|
|
305
|
+
const event = { eventId: this.nextEventId++, sessionId, at: this.now().toISOString(), type, data };
|
|
306
|
+
const events = [...(this.events.get(sessionId) ?? []), event].slice(-500);
|
|
307
|
+
this.events.set(sessionId, events);
|
|
308
|
+
this.notifyListeners(this.listeners.get(sessionId) ?? [], event);
|
|
309
|
+
if (["run_started", "run_finished", "session_updated", "session_deleted"].includes(type)) {
|
|
310
|
+
this.notifyListeners(this.globalListeners, event);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
notifyListeners(listeners, event) {
|
|
314
|
+
for (const listener of listeners) {
|
|
315
|
+
try {
|
|
316
|
+
listener(event);
|
|
317
|
+
}
|
|
318
|
+
catch { /* a disconnected SSE client must not affect the Agent run */ }
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async requireSession(sessionId) {
|
|
322
|
+
const meta = await this.store.get(sessionId);
|
|
323
|
+
if (!meta)
|
|
324
|
+
throw new Error(`DeepCCC web session not found: ${sessionId}`);
|
|
325
|
+
return meta;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function isPathInside(root, candidate) {
|
|
329
|
+
const rel = relative(resolve(root), resolve(candidate));
|
|
330
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
331
|
+
}
|