deepccc 0.2.4 → 0.2.6

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 CHANGED
@@ -42,10 +42,10 @@ npm run dev
42
42
  内部域名和绝对路径替换为公开示例,任务内容、模型配置、Agent 回复和审批流程均来自
43
43
  实际运行结果。
44
44
 
45
- 多会话可以并行处理不同任务,每个会话分别选择 model 和 effort,并实时保留 Agent
46
- 回复与工具过程:
45
+ 多会话可以并行处理不同任务,每个会话分别选择 model、subModel 和 effort。下图来自
46
+ `deepccc-agent/` 工作目录中的真实提问“这个项目妙在哪?”:
47
47
 
48
- ![DeepCCC Web UI 多会话与真实 Agent 回复](docs/deepccc-web-ui.png)
48
+ ![DeepCCC Web UI 图片附件与真实 Agent 回复](docs/deepccc-web-ui.png)
49
49
 
50
50
  命中需要确认的命令时,审批卡会直接出现在当前会话时间线中,不打断到弹窗:
51
51
 
@@ -59,6 +59,7 @@ npm run dev
59
59
 
60
60
  - Web-first:多会话、持久化历史、实时流式过程、停止和恢复
61
61
  - 会话配置:每个会话独立选择 model、subModel 和 effort
62
+ - 图片附件:Web 支持选择、粘贴和拖拽 PNG/JPEG/WebP;Agent 可用 `present_file` 回传图片
62
63
  - 本地工具:代码搜索、文件读写、补丁、命令执行、Git、网页搜索和抓取
63
64
  - 权限审批:危险命令在会话时间线中暂停,支持拒绝、允许一次、会话允许和永久允许
64
65
  - 上下文管理:自动压缩、原始流日志和跨会话历史检索
@@ -153,6 +154,8 @@ deepccc
153
154
 
154
155
  Web UI 支持新建、恢复、重命名和删除多会话,多个会话可以同时运行,即使它们指向同一个工作目录。每个会话可独立选择 model、subModel 和 effort,并持续复用 CLI 已保存的历史。注意:当前版本不自动创建 Git worktree;同目录的多个运行中 Agent 直接修改同一组文件,页面会提示覆盖与冲突风险。
155
156
 
157
+ 图片始终按本地附件处理,不转换为 Provider 原生多模态消息。Web 支持文件选择、剪贴板粘贴和拖拽,每条消息最多 10 张 PNG/JPEG/WebP、单张最大 20 MB;附件复制到 `~/.deepccc/attachments/<session-id>/`,Agent 收到本地绝对路径后使用可用工具自行处理。Agent 可调用 `present_file` 把当前工作目录或会话附件目录中的图片直接展示在会话中;删除会话时对应附件一并清理。
158
+
156
159
  API 设置采用单一 Provider 配置,支持 OpenAI-compatible 与 Anthropic Messages。完整 API Key 只保存在本机 `~/.deepccc/config.json`,浏览器读取设置时仅返回掩码。危险命令会在会话中暂停并请求“拒绝、允许一次、本会话允许、永久允许”,浏览器断开或审批超时默认拒绝。
157
160
 
158
161
  `git.coAuthor.enabled` 默认开启。DeepCCC 通过 `run_command` 创建 Git 提交时会保留用户为
@@ -217,11 +220,17 @@ deepccc-cli --base-url https://api.openai.com/v1 --api-key "$OPENAI_API_KEY" --m
217
220
  deepccc-cli --provider anthropic --base-url https://api.example.com --api-key "$API_KEY" --model claude-sonnet-4-6
218
221
  ```
219
222
 
220
- 指定工作目录:
223
+ 指定工作目录:
221
224
 
222
225
  ```bash
223
226
  deepccc-cli --cwd /path/to/project
224
- ```
227
+ ```
228
+
229
+ 附加一张或多张本地图片(可重复传入 `--image`,仍采用本地附件路径,不发送原生多模态内容):
230
+
231
+ ```bash
232
+ deepccc-cli --image ./error.png --image ./expected.webp
233
+ ```
225
234
 
226
235
  恢复当前工作目录最近一次会话:
227
236
 
@@ -0,0 +1,42 @@
1
+ export declare const MAX_ATTACHMENT_BYTES: number;
2
+ export declare const MAX_ATTACHMENTS_PER_MESSAGE = 10;
3
+ export declare const ATTACHMENT_PROMPT_START = "<deepccc-attachments>";
4
+ export declare const ATTACHMENT_PROMPT_END = "</deepccc-attachments>";
5
+ export interface AttachmentMeta {
6
+ attachmentId: string;
7
+ originalName: string;
8
+ mimeType: "image/png" | "image/jpeg" | "image/webp";
9
+ size: number;
10
+ absolutePath: string;
11
+ }
12
+ export interface SaveAttachmentInput {
13
+ originalName: string;
14
+ bytes: Uint8Array;
15
+ }
16
+ export interface AttachmentStoreOptions {
17
+ rootDir?: string;
18
+ idFactory?: () => string;
19
+ }
20
+ export declare class AttachmentStore {
21
+ readonly rootDir: string;
22
+ private readonly idFactory;
23
+ constructor(options?: AttachmentStoreOptions);
24
+ save(sessionId: string, input: SaveAttachmentInput): Promise<AttachmentMeta>;
25
+ importFile(sessionId: string, path: string): Promise<AttachmentMeta>;
26
+ get(sessionId: string, attachmentId: string): Promise<AttachmentMeta | null>;
27
+ read(sessionId: string, attachmentId: string): Promise<{
28
+ attachment: AttachmentMeta;
29
+ bytes: Buffer;
30
+ } | null>;
31
+ delete(sessionId: string, attachmentId: string): Promise<boolean>;
32
+ deleteSession(sessionId: string): Promise<void>;
33
+ private readStored;
34
+ private sessionDir;
35
+ private metaPath;
36
+ }
37
+ export declare function buildAttachmentPrompt(text: string, attachments: AttachmentMeta[]): string;
38
+ export declare function parseAttachmentPrompt(content: string): {
39
+ text: string;
40
+ attachments: AttachmentMeta[];
41
+ };
42
+ export declare function detectImageMime(bytes: Uint8Array): AttachmentMeta["mimeType"] | null;
@@ -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
+ }
package/dist/cli.js CHANGED
@@ -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([
@@ -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> or stdin input" });
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(prompt)) {
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(input, signal)) {
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,6 +598,13 @@ 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
609
  * skill create 子命令:deepccc-cli skill create <name> [--scope global|project] [--description "..."]
569
610
  * 默认创建为全局技能(~/.deepccc/skills/<name>/SKILL.md,Codex 结构);
@@ -141,12 +141,24 @@ export interface ApplyPatchFileChange {
141
141
  export interface ApplyPatchOutput {
142
142
  changedFiles: ApplyPatchFileChange[];
143
143
  }
144
+ export interface PresentFileInput {
145
+ path: string;
146
+ caption?: string;
147
+ }
148
+ export interface PresentFileOutput {
149
+ path: string;
150
+ name: string;
151
+ mimeType: "image/png" | "image/jpeg" | "image/webp";
152
+ size: number;
153
+ caption?: string;
154
+ }
144
155
  /**
145
156
  * 把 `~` / `~/x`(含反斜杠 `~\\x`)展开为用户主目录绝对路径。
146
157
  * `~user/x` 形式不展开(Node 无内置支持),保持原样交给后续解析。
147
158
  * 供路径工具共用:技能索引里 `~/...` 路径可直接传给 read_file 等工具。
148
159
  */
149
160
  export declare function expandHomePath(value: string): string;
161
+ export declare function presentFileForTool(cwd: string, input: PresentFileInput): Promise<PresentFileOutput>;
150
162
  export declare function readFileForTool(cwd: string, input: ReadFileInput): Promise<ReadFileOutput>;
151
163
  export declare function listDirForTool(cwd: string, input?: ListDirInput): Promise<ListDirOutput>;
152
164
  export declare function searchCodeForTool(cwd: string, input: SearchCodeInput, signal?: AbortSignal, runtimeOptions?: SearchCodeRuntimeOptions): Promise<SearchCodeOutput>;
@@ -8,6 +8,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
8
8
  import { createInterface } from "node:readline";
9
9
  import { jsonSchema, tool } from "ai";
10
10
  import { isDangerousCommand } from "./permissions.js";
11
+ import { detectImageMime, MAX_ATTACHMENT_BYTES } from "./attachments.js";
11
12
  import { killProcessTree } from "./proc-tree-kill.js";
12
13
  import { searchBuiltinSessions, } from "./session-search.js";
13
14
  import { webFetchForTool, webSearchForTool, } from "./web-tools.js";
@@ -74,6 +75,25 @@ async function pathExists(path) {
74
75
  return false;
75
76
  }
76
77
  }
78
+ export async function presentFileForTool(cwd, input) {
79
+ const path = resolveToolPath(cwd, input.path);
80
+ const info = await stat(path).catch(() => null);
81
+ if (!info?.isFile())
82
+ throw new Error(`Image file not found: ${input.path}`);
83
+ if (info.size > MAX_ATTACHMENT_BYTES)
84
+ throw new Error(`Image file exceeds the 20 MB limit: ${input.path}`);
85
+ const mimeType = detectImageMime(await readFile(path));
86
+ if (!mimeType)
87
+ throw new Error(`present_file only supports PNG, JPEG, or WebP images: ${input.path}`);
88
+ const caption = input.caption?.trim();
89
+ return {
90
+ path,
91
+ name: basename(path),
92
+ mimeType,
93
+ size: info.size,
94
+ ...(caption ? { caption: caption.slice(0, 500) } : {}),
95
+ };
96
+ }
77
97
  function sha256(buffer) {
78
98
  return createHash("sha256").update(buffer).digest("hex");
79
99
  }
@@ -1011,6 +1031,19 @@ export function createBuiltinFileTools(cwd, options = {}) {
1011
1031
  }),
1012
1032
  execute: (input, options) => searchCodeForTool(cwd, input, options.abortSignal),
1013
1033
  }),
1034
+ present_file: tool({
1035
+ description: "把本地 PNG、JPEG 或 WebP 图片作为会话图片展示给用户。仅在图片文件已经生成并需要交付时调用。",
1036
+ inputSchema: jsonSchema({
1037
+ type: "object",
1038
+ additionalProperties: false,
1039
+ properties: {
1040
+ path: { type: "string", description: "图片绝对路径或相对于会话工作目录的路径。" },
1041
+ caption: { type: "string", description: "可选的图片说明。" },
1042
+ },
1043
+ required: ["path"],
1044
+ }),
1045
+ execute: (input) => presentFileForTool(cwd, input),
1046
+ }),
1014
1047
  run_command: tool({
1015
1048
  description: "在本地工作区运行非交互式 shell 命令。用于测试、git 和包脚本。返回 stdout/stderr 和 exitCode;非零退出码不是工具错误。",
1016
1049
  inputSchema: jsonSchema({
@@ -1 +1 @@
1
- export declare const DEEPCCC_WEB_PAGE = "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>DeepCCC</title>\n<style>\n:root{color-scheme:light;--bg:#f5f7fb;--panel:#fff;--panel2:#fff;--soft:#f8f9fc;--line:#e3e7ef;--text:#182033;--muted:#737d91;--accent:#6758e8;--accent-soft:#eeeaff;--accent2:#23825d;--danger:#c2415a;--warning:#b7791f;--shadow:0 8px 28px rgba(26,35,58,.08)}\n*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 Inter,-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Microsoft YaHei\",sans-serif;overflow:hidden}button,input,select,textarea{font:inherit}button{cursor:pointer}.app{height:100%;display:grid;grid-template-columns:286px minmax(0,1fr)}\n.sidebar{min-width:0;background:var(--panel);border-right:1px solid var(--line);display:flex;flex-direction:column}.brand{height:70px;display:flex;align-items:center;gap:12px;padding:0 18px;border-bottom:1px solid var(--line)}.mark{width:36px;height:36px;border-radius:12px;display:grid;place-items:center;background:var(--accent);color:#fff;box-shadow:0 9px 25px rgba(103,88,232,.25);font-weight:900}.brand-copy strong{display:block;font-size:16px;letter-spacing:.01em}.brand-copy span{font-size:10px;color:var(--muted);letter-spacing:.13em}.sidebar-actions{display:flex;gap:8px;padding:14px}.primary,.ghost,.icon{border:1px solid var(--line);border-radius:10px;color:var(--text);background:var(--panel);font-weight:700}.primary{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:0 8px 22px rgba(103,88,232,.22)}.sidebar-actions .primary{flex:1;padding:10px}.icon{width:40px;padding:0;font-size:17px}.ghost{padding:9px 12px}.primary:hover,.ghost:hover,.icon:hover{filter:brightness(1.03)}\n.session-list{flex:1;overflow:auto;padding:0 10px 14px}.session-empty{padding:38px 18px;color:var(--muted);text-align:center}.session-item{width:100%;display:block;padding:12px;border:1px solid transparent;border-radius:12px;background:transparent;color:var(--text);text-align:left;margin:3px 0}.session-item:hover{background:var(--panel2)}.session-item.active{background:linear-gradient(135deg,rgba(124,105,241,.2),rgba(93,74,207,.08));border-color:rgba(139,124,246,.35)}.session-title{display:flex;align-items:center;gap:8px;font-weight:750}.status-dot{width:7px;height:7px;border-radius:50%;background:#626a78}.status-dot.running{background:var(--accent2);box-shadow:0 0 0 4px rgba(94,228,177,.1)}.session-meta{margin-top:5px;color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sidebar-foot{padding:12px 16px;border-top:1px solid var(--line);color:var(--muted);font-size:10px;display:flex;justify-content:space-between}\n.main{min-width:0;height:100%;display:flex;flex-direction:column}.topbar{height:70px;flex:none;display:flex;align-items:center;justify-content:space-between;padding:0 22px;border-bottom:1px solid var(--line);background:rgba(12,14,19,.7);backdrop-filter:blur(18px)}.session-heading{min-width:0}.session-heading h1{font-size:16px;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.session-heading p{font-size:11px;color:var(--muted);margin:3px 0 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.top-actions{display:flex;gap:8px}.top-actions button{height:38px}.content{flex:1;min-height:0;overflow-y:auto;scroll-behavior:smooth}.welcome{height:100%;display:grid;place-items:center;padding:30px}.welcome-card{max-width:650px;text-align:center}.welcome-mark{width:72px;height:72px;margin:0 auto 20px;border-radius:24px;display:grid;place-items:center;background:linear-gradient(135deg,#7764eb,#b45fd7);font-size:29px;box-shadow:0 20px 50px rgba(119,100,235,.28)}.welcome h2{font-size:28px;margin:0 0 10px;letter-spacing:-.02em}.welcome p{color:var(--muted);margin:0 auto;max-width:480px}.hint-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:28px}.hint{padding:14px;border:1px solid var(--line);border-radius:13px;background:rgba(24,28,37,.65);font-size:12px;color:var(--muted)}\n.thread{width:min(900px,calc(100% - 36px));margin:0 auto;padding:28px 0 150px}.empty-thread{margin:80px auto 0;max-width:520px;padding:28px;border:1px dashed #353c4a;border-radius:18px;background:rgba(20,23,31,.55);text-align:center}.empty-thread strong{display:block;font-size:18px;margin-bottom:7px}.empty-thread span{color:var(--muted);font-size:12px}.message{display:grid;grid-template-columns:30px minmax(0,1fr);gap:12px;margin:0 0 22px}.avatar{width:30px;height:30px;border-radius:10px;display:grid;place-items:center;background:var(--soft);font-size:11px;font-weight:900}.message.user .avatar{background:#304058}.message.assistant .avatar{background:linear-gradient(135deg,#7363e8,#9a61dc)}.bubble{min-width:0;padding:2px 0;color:#dfe3eb}.bubble .role{font-size:11px;color:var(--muted);font-weight:800;margin-bottom:7px}.markdown{font-size:14px;line-height:1.75;overflow-wrap:anywhere}.markdown p{margin:.35em 0}.markdown pre{padding:14px;border:1px solid var(--line);border-radius:11px;background:#0a0c11;overflow:auto;font:12px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace}.markdown code{padding:2px 5px;border-radius:5px;background:var(--soft);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.markdown pre code{padding:0;background:transparent}.tool-card{margin:8px 0;border:1px solid var(--line);border-radius:10px;background:var(--panel);overflow:hidden}.tool-card summary{padding:9px 11px;color:#bbc2d0;font-size:12px;font-weight:700}.tool-card pre{margin:0;border:0;border-top:1px solid var(--line);border-radius:0;max-height:260px}.run-state{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px;margin:8px 0 20px}.pulse{width:8px;height:8px;border-radius:50%;background:var(--accent2);animation:pulse 1.4s infinite}@keyframes pulse{50%{opacity:.35;transform:scale(.72)}}.workspace-warning{width:min(900px,calc(100% - 36px));margin:14px auto 0;padding:9px 12px;border:1px solid rgba(242,184,75,.3);background:rgba(242,184,75,.08);border-radius:10px;color:#e8c879;font-size:11px}\n.composer-shell{position:absolute;left:286px;right:0;bottom:0;padding:0 20px 18px;background:linear-gradient(transparent,var(--bg) 32%)}.composer{width:min(900px,100%);margin:auto;border:1px solid #343b49;border-radius:17px;background:rgba(25,29,38,.96);box-shadow:var(--shadow);overflow:hidden}.composer textarea{width:100%;min-height:68px;max-height:180px;resize:none;border:0;outline:0;padding:15px 16px 8px;background:transparent;color:var(--text)}.composer textarea::placeholder{color:#737b8b}.composer-bar{display:flex;align-items:center;gap:8px;padding:7px 9px}.composer-bar input,.composer-bar select{min-width:0;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--muted);padding:7px 9px;font-size:11px}.composer-bar input{width:180px}.composer-bar select{width:104px}.send{margin-left:auto;width:36px;height:36px;border:0;border-radius:10px;background:var(--accent);color:#fff;font-size:17px}.stop{margin-left:auto;height:36px;border:1px solid rgba(255,111,137,.4);border-radius:10px;background:rgba(255,111,137,.12);color:#ff91a5;padding:0 13px;font-weight:700}.composer-disabled{opacity:.5;pointer-events:none}\n.modal-backdrop{position:fixed;inset:0;z-index:30;display:grid;place-items:center;padding:20px;background:rgba(4,5,8,.66);backdrop-filter:blur(7px)}.modal-backdrop.hidden{display:none}.modal{width:min(560px,100%);max-height:calc(100vh - 40px);overflow:auto;border:1px solid #353c4a;border-radius:18px;background:#171a22;box-shadow:0 30px 100px rgba(0,0,0,.55);padding:22px}.modal h2{margin:0 0 5px;font-size:20px}.modal>.subtitle{margin:0 0 20px;color:var(--muted);font-size:12px}.field{display:grid;gap:7px;margin:13px 0}.field label{font-size:11px;color:#b7becb;font-weight:800}.field input,.field select{width:100%;border:1px solid var(--line);border-radius:10px;background:#10131a;color:var(--text);padding:10px 11px;outline:none}.field input:focus,.field select:focus{border-color:var(--accent)}.row{display:grid;grid-template-columns:1fr 1fr;gap:10px}.check{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px}.modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.danger{border:1px solid rgba(255,111,137,.35);background:rgba(255,111,137,.1);color:#ff849b;border-radius:10px;padding:9px 12px;font-weight:700}.approval{width:min(620px,100%)}.approval-badge{display:inline-flex;padding:5px 8px;border-radius:999px;background:rgba(242,184,75,.13);color:#f1c565;font-size:10px;font-weight:900}.approval pre{white-space:pre-wrap;overflow-wrap:anywhere;padding:13px;border:1px solid var(--line);border-radius:10px;background:#0d0f15;color:#dce0e9}.approval-actions{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.toast{position:fixed;right:20px;top:20px;z-index:50;max-width:420px;padding:11px 14px;border-radius:10px;background:#283143;border:1px solid #3b465b;box-shadow:var(--shadow)}.toast.error{background:#602536;border-color:#913a52}.hidden{display:none!important}\n.topbar{background:rgba(255,255,255,.86)}.welcome-mark{background:var(--accent);color:#fff}.hint,.empty-thread{background:var(--panel);border-color:var(--line)}.session-item.active{background:var(--accent-soft);border-color:#d8d1ff}.status-dot{background:#a3aabd}.status-dot.running{background:var(--accent2);box-shadow:0 0 0 4px rgba(35,130,93,.1)}.message.user .avatar{background:#e8edf6}.message.assistant .avatar{background:var(--accent);color:#fff}.bubble{color:var(--text)}.markdown pre,.tool-card pre{background:var(--soft);color:var(--text)}.tool-card summary{color:var(--muted)}.composer{border-color:var(--line);background:rgba(255,255,255,.98)}.composer textarea::placeholder{color:#9aa3b4}.composer-bar input,.composer-bar select{background:var(--soft)}.modal-backdrop{background:rgba(24,32,51,.36)}.modal{border-color:var(--line);background:var(--panel);box-shadow:0 28px 90px rgba(35,43,65,.24)}.field label{color:var(--text)}.field input,.field select{background:var(--soft);color:var(--text)}.danger{border-color:#f3c4ce;background:#fff1f3;color:var(--danger)}.toast{background:#253047;color:#fff}.toast.error{background:#9f2942}.approval-card{margin:16px 0;padding:14px;border:1px solid #ead49b;border-radius:13px;background:#fffaf0;box-shadow:0 5px 18px rgba(94,70,15,.06)}.approval-card.resolved{border-color:var(--line);background:var(--soft)}.approval-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}.approval-head strong{font-size:12px}.approval-status{padding:3px 7px;border-radius:999px;background:#fff0c2;color:#8a6110;font-size:10px;font-weight:800}.approval-card.resolved .approval-status{background:#e5f5ee;color:var(--accent2)}.approval-detail{color:var(--muted);font-size:11px;margin-bottom:8px}.approval-command{margin:0;padding:10px;border:1px solid var(--line);border-radius:9px;background:#fff;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace}.approval-inline-actions{display:flex;flex-wrap:wrap;gap:7px;margin-top:10px}.approval-inline-actions button{padding:7px 10px;font-size:11px}\n.sidebar{height:100%;overflow:hidden}.main{min-height:0!important;overflow:hidden}\n@media(max-width:760px){.app{grid-template-columns:78px minmax(0,1fr)}.brand{padding:0;justify-content:center}.brand-copy,.sidebar-actions .primary,.session-item>div:not(.session-title),.session-title span:last-child,.sidebar-foot{display:none}.sidebar-actions{justify-content:center;padding:12px 8px}.session-item{padding:11px;display:grid;place-items:center}.composer-shell{left:78px}.hint-grid{grid-template-columns:1fr}.topbar{padding:0 14px}.thread{width:calc(100% - 24px)}.row{grid-template-columns:1fr}.approval-inline-actions{display:grid;grid-template-columns:1fr 1fr}}\n</style>\n</head>\n<body>\n<div class=\"app\">\n <aside class=\"sidebar\">\n <div class=\"brand\"><div class=\"mark\">D</div><div class=\"brand-copy\"><strong>DeepCCC</strong><span>LOCAL AGENT</span></div></div>\n <div class=\"sidebar-actions\"><button id=\"new-session\" class=\"primary\">\uFF0B \u65B0\u5EFA\u4F1A\u8BDD</button><button id=\"open-settings\" class=\"icon\" title=\"API \u8BBE\u7F6E\">\u2699</button></div>\n <div id=\"session-list\" class=\"session-list\"></div>\n <div class=\"sidebar-foot\"><span>127.0.0.1</span><span id=\"server-port\">28080</span></div>\n </aside>\n <main class=\"main\">\n <header class=\"topbar\"><div class=\"session-heading\"><h1 id=\"session-title\">DeepCCC</h1><p id=\"session-cwd\">\u9009\u62E9\u6216\u521B\u5EFA\u4E00\u4E2A\u4F1A\u8BDD</p></div><div class=\"top-actions\"><button id=\"rename-session\" class=\"ghost hidden\">\u91CD\u547D\u540D</button><button id=\"delete-session\" class=\"danger hidden\">\u5220\u9664</button></div></header>\n <div id=\"warning\" class=\"workspace-warning hidden\"></div>\n <section id=\"content\" class=\"content\"><div class=\"welcome\"><div class=\"welcome-card\"><div class=\"welcome-mark\">\u2726</div><h2>\u628A\u5DE5\u4F5C\u4EA4\u7ED9 DeepCCC</h2><p>\u5728\u6D4F\u89C8\u5668\u91CC\u7BA1\u7406\u591A\u4E2A\u672C\u5730 Coding Agent \u4F1A\u8BDD\uFF0C\u5207\u6362\u6A21\u578B\u4E0E\u63A8\u7406\u5F3A\u5EA6\uFF0C\u5E76\u5728\u4F1A\u8BDD\u5185\u5BA1\u6279\u64CD\u4F5C\u3002</p><div class=\"hint-grid\"><div class=\"hint\">\u591A\u4F1A\u8BDD\u5E76\u884C\u8FD0\u884C</div><div class=\"hint\">\u672C\u5730\u6587\u4EF6\u4E0E\u5386\u53F2</div><div class=\"hint\">API \u81EA\u7531\u914D\u7F6E</div></div></div></div></section>\n </main>\n</div>\n<div id=\"composer-shell\" class=\"composer-shell hidden\"><div class=\"composer\"><textarea id=\"prompt\" placeholder=\"\u63CF\u8FF0\u4F60\u60F3\u5B8C\u6210\u7684\u4EFB\u52A1\u2026\"></textarea><div class=\"composer-bar\"><input id=\"session-model\" list=\"recent-models\" aria-label=\"\u6A21\u578B\"><input id=\"session-sub-model\" list=\"recent-models\" aria-label=\"\u5B50\u6A21\u578B\" placeholder=\"\u9ED8\u8BA4\u5B50\u6A21\u578B\"><datalist id=\"recent-models\"></datalist><select id=\"session-effort\" aria-label=\"\u63A8\u7406\u5F3A\u5EA6\"><option value=\"\">\u9ED8\u8BA4 effort</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select><button id=\"send\" class=\"send\" title=\"\u53D1\u9001\">\u2191</button><button id=\"stop\" class=\"stop hidden\">\u25A0 \u505C\u6B62</button></div></div></div>\n\n<div id=\"new-modal\" class=\"modal-backdrop hidden\"><form id=\"new-form\" class=\"modal\"><h2>\u65B0\u5EFA Agent \u4F1A\u8BDD</h2><p class=\"subtitle\">\u6BCF\u4E2A\u4F1A\u8BDD\u62E5\u6709\u72EC\u7ACB\u4E0A\u4E0B\u6587\uFF1B\u540C\u76EE\u5F55\u4F1A\u8BDD\u53EF\u4EE5\u5E76\u53D1\uFF0C\u4F46\u6587\u4EF6\u4FEE\u6539\u53EF\u80FD\u51B2\u7A81\u3002</p><div class=\"field\"><label>\u5DE5\u4F5C\u76EE\u5F55</label><input id=\"new-cwd\" required></div><div class=\"field\"><label>\u4F1A\u8BDD\u540D\u79F0\uFF08\u53EF\u9009\uFF09</label><input id=\"new-title\" placeholder=\"\u9ED8\u8BA4\u4F7F\u7528\u76EE\u5F55\u540D\"></div><div class=\"row\"><div class=\"field\"><label>\u6A21\u578B</label><input id=\"new-model\" list=\"recent-models\"></div><div class=\"field\"><label>\u5B50\u6A21\u578B\uFF08\u53EF\u9009\uFF09</label><input id=\"new-sub-model\" list=\"recent-models\"></div></div><div class=\"field\"><label>Effort</label><select id=\"new-effort\"><option value=\"\">\u9ED8\u8BA4</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select></div><div class=\"modal-actions\"><button type=\"button\" data-close=\"new-modal\" class=\"ghost\">\u53D6\u6D88</button><button class=\"primary\" type=\"submit\">\u521B\u5EFA\u4F1A\u8BDD</button></div></form></div>\n\n<div id=\"settings-modal\" class=\"modal-backdrop hidden\"><form id=\"settings-form\" class=\"modal\"><h2>API \u4E0E Web \u8BBE\u7F6E</h2><p class=\"subtitle\">\u5355\u4E00 API \u914D\u7F6E\u4F5C\u4E3A\u6240\u6709\u65B0\u4F1A\u8BDD\u7684\u9ED8\u8BA4\u503C\uFF1B\u4F1A\u8BDD\u53EF\u5355\u72EC\u8986\u76D6\u6A21\u578B\u548C effort\u3002</p><div class=\"row\"><div class=\"field\"><label>API \u534F\u8BAE</label><select id=\"cfg-provider\"><option value=\"openai\">OpenAI-compatible</option><option value=\"anthropic\">Anthropic Messages</option></select></div><div class=\"field\"><label>API Key</label><input id=\"cfg-api-key\" type=\"password\" placeholder=\"\u7559\u7A7A\u4FDD\u7559\u5F53\u524D Key\"></div></div><div class=\"field\"><label>Base URL</label><input id=\"cfg-base-url\" required></div><div class=\"row\"><div class=\"field\"><label>\u9ED8\u8BA4\u6A21\u578B</label><input id=\"cfg-model\" required></div><div class=\"field\"><label>\u5B50\u6A21\u578B\uFF08\u53EF\u9009\uFF09</label><input id=\"cfg-sub-model\"></div></div><div class=\"row\"><div class=\"field\"><label>\u9ED8\u8BA4 Effort</label><select id=\"cfg-effort\"><option value=\"\">\u4E0D\u4F20</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select></div><div class=\"field\"><label>Context Window</label><input id=\"cfg-context\" type=\"number\" min=\"1\"></div></div><div class=\"row\"><div class=\"field\"><label>Web \u7AEF\u53E3</label><input id=\"cfg-port\" type=\"number\" min=\"1\" max=\"65535\"></div><div class=\"field\"><label>Max Output Tokens\uFF08\u53EF\u9009\uFF09</label><input id=\"cfg-max-output\" type=\"number\" min=\"1\"></div></div><label class=\"check\"><input id=\"cfg-streaming\" type=\"checkbox\">\u542F\u7528\u6D41\u5F0F\u8BF7\u6C42</label><label class=\"check\"><input id=\"cfg-open\" type=\"checkbox\">\u8FD0\u884C deepccc web \u65F6\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668</label><div id=\"key-state\" class=\"subtitle\"></div><div class=\"modal-actions\"><button type=\"button\" data-close=\"settings-modal\" class=\"ghost\">\u53D6\u6D88</button><button class=\"primary\" type=\"submit\">\u4FDD\u5B58\u8BBE\u7F6E</button></div></form></div>\n\n<div id=\"rename-modal\" class=\"modal-backdrop hidden\"><form id=\"rename-form\" class=\"modal\"><h2>\u91CD\u547D\u540D\u4F1A\u8BDD</h2><div class=\"field\"><label>\u540D\u79F0</label><input id=\"rename-title\" required maxlength=\"120\"></div><div class=\"modal-actions\"><button type=\"button\" data-close=\"rename-modal\" class=\"ghost\">\u53D6\u6D88</button><button class=\"primary\">\u4FDD\u5B58</button></div></form></div>\n\n<div id=\"toast\" class=\"toast hidden\"></div>\n<script>\n(function(){\n var state={sessions:[],selectedId:null,detail:null,config:null,eventSource:null,globalEventSource:null,reloadTimer:null,sessionsReloadTimer:null,detailRequestToken:0,configDrafts:{},promptDrafts:{}};\n var $=function(id){return document.getElementById(id);};\n async function api(path,options){var response=await fetch(path,Object.assign({headers:{'content-type':'application/json; charset=utf-8'}},options||{}));var data=await response.json().catch(function(){return {error:'\u54CD\u5E94\u4E0D\u662F\u6709\u6548 JSON'};});if(!response.ok)throw new Error(data.error||('HTTP '+response.status));return data;}\n function toast(message,error){var el=$('toast');el.textContent=message;el.className='toast'+(error?' error':'');clearTimeout(toast.timer);toast.timer=setTimeout(function(){el.className='toast hidden';},3500);}\n function openModal(id){$(id).classList.remove('hidden');}function closeModal(id){$(id).classList.add('hidden');}\n function escapeHtml(value){return String(value||'').replace(/[&<>\"']/g,function(ch){return {'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[ch];});}\n function markdown(value){var text=escapeHtml(value);var blocks=[];var tick=String.fromCharCode(96);var fence=tick+tick+tick;var pattern=new RegExp(fence+'([a-z0-9_-]*)\\\\n([\\\\s\\\\S]*?)'+fence,'gi');text=text.replace(pattern,function(_,lang,code){blocks.push('<pre><code data-lang=\"'+lang+'\">'+code+'</code></pre>');return '@@BLOCK'+(blocks.length-1)+'@@';});text=text.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>').replace(new RegExp(tick+'([^'+tick+']+)'+tick,'g'),'<code>$1</code>').replace(/\\n/g,'<br>');text=text.replace(/@@BLOCK(\\d+)@@/g,function(_,index){return blocks[Number(index)];});return text;}\n function statusText(status){return status==='running'?'\u6267\u884C\u4E2D':'\u7A7A\u95F2';}\n function renderSessions(){var root=$('session-list');root.innerHTML='';if(!state.sessions.length){root.innerHTML='<div class=\"session-empty\">\u8FD8\u6CA1\u6709\u4F1A\u8BDD<br>\u70B9\u51FB\u4E0A\u65B9\u6309\u94AE\u5F00\u59CB</div>';return;}state.sessions.forEach(function(session){var button=document.createElement('button');button.className='session-item'+(session.sessionId===state.selectedId?' active':'');button.innerHTML='<div class=\"session-title\"><i class=\"status-dot '+session.status+'\"></i><span>'+escapeHtml(session.title)+'</span></div><div class=\"session-meta\">'+escapeHtml(session.model||'\u9ED8\u8BA4\u6A21\u578B')+' \u00B7 '+escapeHtml(session.effort||'\u9ED8\u8BA4 effort')+'</div><div class=\"session-meta\">'+escapeHtml(session.cwd)+'</div>';button.addEventListener('click',function(){selectSession(session.sessionId).catch(function(error){toast(error.message,true);});});root.appendChild(button);});}\n function messageHtml(role,content){return '<article class=\"message '+role+'\"><div class=\"avatar\">'+(role==='user'?'\u4F60':'D')+'</div><div class=\"bubble\"><div class=\"role\">'+(role==='user'?'YOU':'DEEPCCC')+'</div><div class=\"markdown\">'+markdown(content)+'</div></div></article>';}\n function toolHtml(event){var data=event.data||{};var item=data.type==='agent'?data.data:data;return '<details class=\"tool-card\"><summary>'+escapeHtml(item.type==='tool_use'?'\u8C03\u7528\u5DE5\u5177 \u00B7 '+(item.name||'tool'):'\u5DE5\u5177\u7ED3\u679C \u00B7 '+(item.name||item.tool_use_id||'tool'))+'</summary><pre>'+escapeHtml(JSON.stringify(item.type==='tool_use'?item.input:item.content,null,2))+'</pre></details>';}\n function approvalAnswerLabel(answer){var labels={allow:'\u5DF2\u5141\u8BB8\u4E00\u6B21','allow-session':'\u672C\u4F1A\u8BDD\u5DF2\u5141\u8BB8','allow-always':'\u5DF2\u6C38\u4E45\u5141\u8BB8',deny:'\u5DF2\u62D2\u7EDD'};return labels[answer]||'\u7B49\u5F85\u51B3\u5B9A';}\n function approvalHtml(approval){var resolved=approval.status==='resolved';var html='<section class=\"approval-card'+(resolved?' resolved':'')+'\"><div class=\"approval-head\"><strong>\u26A0 \u64CD\u4F5C\u5BA1\u6279</strong><span class=\"approval-status\">'+escapeHtml(resolved?approvalAnswerLabel(approval.answer):'\u7B49\u5F85\u6279\u51C6')+'</span></div><div class=\"approval-detail\">'+escapeHtml(approval.detail||('\u5DE5\u5177\uFF1A'+approval.tool))+'</div><pre class=\"approval-command\">'+escapeHtml(approval.action)+'</pre>';if(!resolved)html+='<div class=\"approval-inline-actions\"><button class=\"danger\" data-approval-answer=\"deny\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u62D2\u7EDD</button><button class=\"ghost\" data-approval-answer=\"allow\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u5141\u8BB8\u4E00\u6B21</button><button class=\"ghost\" data-approval-answer=\"allow-session\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u672C\u4F1A\u8BDD\u5141\u8BB8</button><button class=\"primary\" data-approval-answer=\"allow-always\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u6C38\u4E45\u5141\u8BB8</button></div>';return html+'</section>';}\n function sessionDraft(detail){return state.configDrafts[detail.sessionId]||{model:detail.model||'',subModel:detail.subModel||'',effort:detail.effort||''};}\n function rememberDraft(){if(!state.selectedId)return;state.configDrafts[state.selectedId]={model:$('session-model').value,subModel:$('session-sub-model').value,effort:$('session-effort').value};}\n function rememberPromptDraft(){if(!state.selectedId)return;state.promptDrafts[state.selectedId]=$('prompt').value;}\n function clearSelection(message){if(state.eventSource)state.eventSource.close();state.eventSource=null;state.detailRequestToken++;state.selectedId=null;state.detail=null;$('composer-shell').classList.add('hidden');$('rename-session').classList.add('hidden');$('delete-session').classList.add('hidden');$('warning').classList.add('hidden');$('session-title').textContent='DeepCCC';$('session-cwd').textContent='\u9009\u62E9\u6216\u521B\u5EFA\u4E00\u4E2A\u4F1A\u8BDD';$('content').innerHTML='<div class=\"welcome\"><div class=\"welcome-card\"><div class=\"welcome-mark\">\u2726</div><h2>'+escapeHtml(message||'\u9009\u62E9\u4E00\u4E2A\u4F1A\u8BDD')+'</h2><p>\u4ECE\u5DE6\u4FA7\u9009\u62E9\u5176\u4ED6\u4F1A\u8BDD\u6216\u521B\u5EFA\u4E00\u4E2A\u65B0\u7684\u4EFB\u52A1\u3002</p></div></div>';}\n function renderDetail(){var detail=state.detail;if(!detail)return;var draft=sessionDraft(detail);$('session-title').textContent=detail.title;$('session-cwd').textContent=detail.cwd+' \u00B7 '+statusText(detail.status);$('rename-session').classList.remove('hidden');$('delete-session').classList.remove('hidden');$('composer-shell').classList.remove('hidden');$('prompt').value=state.promptDrafts[detail.sessionId]||'';$('session-model').value=draft.model;$('session-sub-model').value=draft.subModel;$('session-effort').value=draft.effort;var html='<div class=\"thread\">';if(!(detail.messages||[]).length&&detail.status!=='running')html+='<div class=\"empty-thread\"><strong>\u51C6\u5907\u5F00\u59CB\u8FD9\u4E2A\u4F1A\u8BDD</strong><span>\u5728\u4E0B\u65B9\u8F93\u5165\u4EFB\u52A1\u3002DeepCCC \u4F1A\u8BFB\u53D6\u5F53\u524D\u9879\u76EE\u6307\u4EE4\u3001\u8C03\u7528\u5DE5\u5177\u5E76\u6301\u7EED\u4FDD\u5B58\u4E0A\u4E0B\u6587\u3002</span></div>';(detail.messages||[]).forEach(function(message){html+=messageHtml(message.role,message.content);});if(detail.status==='running'){var live=(detail.events||[]).slice().reverse().find(function(event){return event.type==='agent'&&event.data&&event.data.type==='text';});var current=(detail.events||[]).filter(function(event){return event.type==='agent'&&event.data&&(event.data.type==='tool_use'||event.data.type==='tool_result');});current.forEach(function(event){html+=toolHtml(event);});if(live)html+=messageHtml('assistant',live.data.accumulated||live.data.text||'');}var approvals=(detail.approvals||[]).slice();if(detail.pendingApproval&&!approvals.some(function(item){return item.approvalId===detail.pendingApproval.approvalId;}))approvals.push(Object.assign({status:'pending'},detail.pendingApproval));approvals.forEach(function(approval){html+=approvalHtml(approval);});if(detail.status==='running')html+='<div class=\"run-state\"><i class=\"pulse\"></i>Agent \u6B63\u5728\u5DE5\u4F5C</div>';html+='</div>';$('content').innerHTML=html;$('send').classList.toggle('hidden',detail.status==='running');$('stop').classList.toggle('hidden',detail.status!=='running');$('prompt').disabled=detail.status==='running';$('session-model').disabled=detail.status==='running';$('session-sub-model').disabled=detail.status==='running';$('session-effort').disabled=detail.status==='running';renderWarning();requestAnimationFrame(function(){$('content').scrollTop=$('content').scrollHeight;});}\n function cwdKey(value){var raw=String(value||'');var normalized=raw.replace(/\\\\/g,'/').replace(new RegExp('/+$'),'');var windowsStyle=new RegExp('^[a-z]:/','i').test(normalized)||raw.indexOf(String.fromCharCode(92))>=0;return windowsStyle?normalized.toLowerCase():normalized;}\n function renderWarning(){if(!state.detail)return;var selectedCwd=cwdKey(state.detail.cwd);var conflicts=state.sessions.filter(function(item){return item.sessionId!==state.detail.sessionId&&item.status==='running'&&cwdKey(item.cwd)===selectedCwd;});var el=$('warning');if(conflicts.length){el.textContent='\u26A0 \u5F53\u524D\u76EE\u5F55\u8FD8\u6709 '+conflicts.length+' \u4E2A Agent \u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u3002\u591A\u4E2A\u4F1A\u8BDD\u76F4\u63A5\u4FEE\u6539\u540C\u4E00\u76EE\u5F55\uFF0C\u53EF\u80FD\u4EA7\u751F\u8986\u76D6\u6216\u51B2\u7A81\u3002';el.classList.remove('hidden');}else el.classList.add('hidden');}\n async function loadSessions(){var result=await api('/api/sessions');state.sessions=result.sessions||[];if(state.selectedId&&!state.sessions.some(function(item){return item.sessionId===state.selectedId;}))clearSelection('\u4F1A\u8BDD\u5DF2\u5728\u5176\u4ED6\u9875\u9762\u5220\u9664');renderSessions();renderWarning();}\n async function loadDetail(id,token){var sessionId=id||state.selectedId;if(!sessionId)return false;var requestToken=token===undefined?++state.detailRequestToken:token;var result=await api('/api/sessions/'+encodeURIComponent(sessionId));if(state.selectedId!==sessionId||requestToken!==state.detailRequestToken)return false;await loadSessions();if(state.selectedId!==sessionId||requestToken!==state.detailRequestToken)return false;state.detail=result.session;renderDetail();return true;}\n function scheduleDetail(id){if(id&&id!==state.selectedId)return;clearTimeout(state.reloadTimer);var token=++state.detailRequestToken;state.reloadTimer=setTimeout(function(){loadDetail(id||state.selectedId,token).catch(function(error){toast(error.message,true);});},120);}\n function scheduleSessions(){clearTimeout(state.sessionsReloadTimer);state.sessionsReloadTimer=setTimeout(function(){loadSessions().catch(function(error){toast(error.message,true);});},80);}\n async function selectSession(id){state.selectedId=id;var token=++state.detailRequestToken;renderSessions();if(state.eventSource)state.eventSource.close();state.eventSource=new EventSource('/api/sessions/'+encodeURIComponent(id)+'/events');state.eventSource.onmessage=function(){scheduleDetail(id);};await loadDetail(id,token);}\n async function loadConfig(){var result=await api('/api/config');state.config=result.config;$('cfg-provider').value=state.config.provider;$('cfg-base-url').value=state.config.baseURL;$('cfg-model').value=state.config.model;$('cfg-sub-model').value=state.config.subModel||'';$('cfg-effort').value=state.config.effort||'';$('cfg-context').value=state.config.contextWindow;$('cfg-max-output').value=state.config.maxOutputTokens||'';$('cfg-streaming').checked=state.config.streaming!==false;$('cfg-port').value=state.config.web.port;$('cfg-open').checked=state.config.web.openOnStart!==false;$('server-port').textContent=state.config.web.port;$('key-state').textContent=state.config.apiKeyConfigured?'\u5F53\u524D API Key\uFF1A'+state.config.apiKeyMask:'\u5C1A\u672A\u914D\u7F6E API Key';$('new-cwd').value=state.config.defaultCwd||'';$('new-model').value=state.config.model;$('new-sub-model').value=state.config.subModel||'';var models=[state.config.model,state.config.subModel].concat(state.sessions.reduce(function(all,item){return all.concat([item.model,item.subModel]);},[])).filter(Boolean).filter(function(value,index,array){return array.indexOf(value)===index;});$('recent-models').innerHTML=models.map(function(model){return '<option value=\"'+escapeHtml(model)+'\">';}).join('');}\n $('new-session').addEventListener('click',function(){openModal('new-modal');setTimeout(function(){$('new-cwd').focus();},0);});$('open-settings').addEventListener('click',function(){loadConfig().then(function(){openModal('settings-modal');}).catch(function(error){toast(error.message,true);});});document.querySelectorAll('[data-close]').forEach(function(button){button.addEventListener('click',function(){closeModal(button.dataset.close);});});\n $('new-form').addEventListener('submit',async function(event){event.preventDefault();try{var result=await api('/api/sessions',{method:'POST',body:JSON.stringify({cwd:$('new-cwd').value,title:$('new-title').value,model:$('new-model').value,subModel:$('new-sub-model').value,effort:$('new-effort').value})});closeModal('new-modal');$('new-title').value='';await loadSessions();await selectSession(result.session.sessionId);}catch(error){toast(error.message,true);}});\n $('settings-form').addEventListener('submit',async function(event){event.preventDefault();var body={provider:$('cfg-provider').value,baseURL:$('cfg-base-url').value,model:$('cfg-model').value,subModel:$('cfg-sub-model').value,effort:$('cfg-effort').value,contextWindow:Number($('cfg-context').value),streaming:$('cfg-streaming').checked,web:{port:Number($('cfg-port').value),openOnStart:$('cfg-open').checked}};if($('cfg-max-output').value)body.maxOutputTokens=Number($('cfg-max-output').value);if($('cfg-api-key').value)body.apiKey=$('cfg-api-key').value;try{await api('/api/config',{method:'PUT',body:JSON.stringify(body)});$('cfg-api-key').value='';closeModal('settings-modal');toast('\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u65B0\u914D\u7F6E\u4ECE\u4E0B\u4E00\u6B21\u65B0\u5EFA\u4F1A\u8BDD\u751F\u6548');await loadConfig();}catch(error){toast(error.message,true);}});\n async function send(){var sessionId=state.selectedId;var text=$('prompt').value;if(!sessionId||!text.trim())return;try{var patch={model:$('session-model').value,subModel:$('session-sub-model').value,effort:$('session-effort').value};if(patch.model!==state.detail.model||patch.subModel!==state.detail.subModel||patch.effort!==state.detail.effort){var updated=await api('/api/sessions/'+encodeURIComponent(sessionId),{method:'PATCH',body:JSON.stringify(patch)});if(state.selectedId===sessionId){state.detail=Object.assign({},state.detail,updated.session);renderDetail();}await loadSessions();}await api('/api/sessions/'+encodeURIComponent(sessionId)+'/messages',{method:'POST',body:JSON.stringify({text:text})});delete state.configDrafts[sessionId];delete state.promptDrafts[sessionId];if(state.selectedId===sessionId)$('prompt').value='';var token=++state.detailRequestToken;await loadDetail(sessionId,token);}catch(error){toast(error.message,true);}}\n ['session-model','session-sub-model','session-effort'].forEach(function(id){$(id).addEventListener('input',rememberDraft);$(id).addEventListener('change',rememberDraft);});\n $('send').addEventListener('click',send);$('prompt').addEventListener('input',rememberPromptDraft);$('prompt').addEventListener('keydown',function(event){if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();send();}});$('stop').addEventListener('click',async function(){try{await api('/api/sessions/'+encodeURIComponent(state.selectedId)+'/stop',{method:'POST',body:'{}'});toast('\u5DF2\u8BF7\u6C42\u505C\u6B62');}catch(error){toast(error.message,true);}});\n $('rename-session').addEventListener('click',function(){$('rename-title').value=state.detail.title;openModal('rename-modal');});$('rename-form').addEventListener('submit',async function(event){event.preventDefault();try{await api('/api/sessions/'+encodeURIComponent(state.selectedId),{method:'PATCH',body:JSON.stringify({title:$('rename-title').value})});closeModal('rename-modal');var token=++state.detailRequestToken;await loadDetail(state.selectedId,token);}catch(error){toast(error.message,true);}});$('delete-session').addEventListener('click',async function(){if(!confirm('\u5220\u9664\u8FD9\u4E2A\u4F1A\u8BDD\u53CA\u5176\u672C\u5730\u5386\u53F2\uFF1F\u4E0D\u4F1A\u5220\u9664\u5DE5\u4F5C\u76EE\u5F55\u4E2D\u7684\u6587\u4EF6\u3002'))return;try{var deletedId=state.selectedId;await api('/api/sessions/'+encodeURIComponent(deletedId),{method:'DELETE'});delete state.configDrafts[deletedId];delete state.promptDrafts[deletedId];clearSelection('\u4F1A\u8BDD\u5DF2\u5220\u9664');await loadSessions();}catch(error){toast(error.message,true);}});\n $('content').addEventListener('click',async function(event){var button=event.target.closest('[data-approval-answer]');if(!button)return;button.disabled=true;try{await api('/api/approvals/'+encodeURIComponent(button.dataset.approvalId),{method:'POST',body:JSON.stringify({answer:button.dataset.approvalAnswer})});var token=++state.detailRequestToken;await loadDetail(state.selectedId,token);}catch(error){button.disabled=false;toast(error.message,true);}});\n function handleGlobalEvent(event){try{var update=JSON.parse(event.data);if(update.type==='session_deleted'){delete state.configDrafts[update.sessionId];delete state.promptDrafts[update.sessionId];}}catch(error){}scheduleSessions();}\n state.globalEventSource=new EventSource('/api/events');state.globalEventSource.onmessage=handleGlobalEvent;\n Promise.all([loadSessions(),loadConfig()]).then(function(){if(state.sessions[0])return selectSession(state.sessions[0].sessionId);}).catch(function(error){toast(error.message,true);});\n})();\n</script>\n</body></html>";
1
+ export declare const DEEPCCC_WEB_PAGE = "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>DeepCCC</title>\n<style>\n:root{color-scheme:light;--bg:#f5f7fb;--panel:#fff;--panel2:#fff;--soft:#f8f9fc;--line:#e3e7ef;--text:#182033;--muted:#737d91;--accent:#6758e8;--accent-soft:#eeeaff;--accent2:#23825d;--danger:#c2415a;--warning:#b7791f;--shadow:0 8px 28px rgba(26,35,58,.08)}\n*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 Inter,-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Microsoft YaHei\",sans-serif;overflow:hidden}button,input,select,textarea{font:inherit}button{cursor:pointer}.app{height:100%;display:grid;grid-template-columns:286px minmax(0,1fr)}\n.sidebar{min-width:0;background:var(--panel);border-right:1px solid var(--line);display:flex;flex-direction:column}.brand{height:70px;display:flex;align-items:center;gap:12px;padding:0 18px;border-bottom:1px solid var(--line)}.mark{width:36px;height:36px;border-radius:12px;display:grid;place-items:center;background:var(--accent);color:#fff;box-shadow:0 9px 25px rgba(103,88,232,.25);font-weight:900}.brand-copy strong{display:block;font-size:16px;letter-spacing:.01em}.brand-copy span{font-size:10px;color:var(--muted);letter-spacing:.13em}.sidebar-actions{display:flex;gap:8px;padding:14px}.primary,.ghost,.icon{border:1px solid var(--line);border-radius:10px;color:var(--text);background:var(--panel);font-weight:700}.primary{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:0 8px 22px rgba(103,88,232,.22)}.sidebar-actions .primary{flex:1;padding:10px}.icon{width:40px;padding:0;font-size:17px}.ghost{padding:9px 12px}.primary:hover,.ghost:hover,.icon:hover{filter:brightness(1.03)}\n.session-list{flex:1;overflow:auto;padding:0 10px 14px}.session-empty{padding:38px 18px;color:var(--muted);text-align:center}.session-item{width:100%;display:block;padding:12px;border:1px solid transparent;border-radius:12px;background:transparent;color:var(--text);text-align:left;margin:3px 0}.session-item:hover{background:var(--panel2)}.session-item.active{background:linear-gradient(135deg,rgba(124,105,241,.2),rgba(93,74,207,.08));border-color:rgba(139,124,246,.35)}.session-title{display:flex;align-items:center;gap:8px;font-weight:750}.status-dot{width:7px;height:7px;border-radius:50%;background:#626a78}.status-dot.running{background:var(--accent2);box-shadow:0 0 0 4px rgba(94,228,177,.1)}.session-meta{margin-top:5px;color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sidebar-foot{padding:12px 16px;border-top:1px solid var(--line);color:var(--muted);font-size:10px;display:flex;justify-content:space-between}\n.main{min-width:0;height:100%;display:flex;flex-direction:column}.topbar{height:70px;flex:none;display:flex;align-items:center;justify-content:space-between;padding:0 22px;border-bottom:1px solid var(--line);background:rgba(12,14,19,.7);backdrop-filter:blur(18px)}.session-heading{min-width:0}.session-heading h1{font-size:16px;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.session-heading p{font-size:11px;color:var(--muted);margin:3px 0 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.top-actions{display:flex;gap:8px}.top-actions button{height:38px}.content{flex:1;min-height:0;overflow-y:auto;scroll-behavior:smooth}.welcome{height:100%;display:grid;place-items:center;padding:30px}.welcome-card{max-width:650px;text-align:center}.welcome-mark{width:72px;height:72px;margin:0 auto 20px;border-radius:24px;display:grid;place-items:center;background:linear-gradient(135deg,#7764eb,#b45fd7);font-size:29px;box-shadow:0 20px 50px rgba(119,100,235,.28)}.welcome h2{font-size:28px;margin:0 0 10px;letter-spacing:-.02em}.welcome p{color:var(--muted);margin:0 auto;max-width:480px}.hint-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:28px}.hint{padding:14px;border:1px solid var(--line);border-radius:13px;background:rgba(24,28,37,.65);font-size:12px;color:var(--muted)}\n.thread{width:min(900px,calc(100% - 36px));margin:0 auto;padding:28px 0 150px}.empty-thread{margin:80px auto 0;max-width:520px;padding:28px;border:1px dashed #353c4a;border-radius:18px;background:rgba(20,23,31,.55);text-align:center}.empty-thread strong{display:block;font-size:18px;margin-bottom:7px}.empty-thread span{color:var(--muted);font-size:12px}.message{display:grid;grid-template-columns:30px minmax(0,1fr);gap:12px;margin:0 0 22px}.avatar{width:30px;height:30px;border-radius:10px;display:grid;place-items:center;background:var(--soft);font-size:11px;font-weight:900}.message.user .avatar{background:#304058}.message.assistant .avatar{background:linear-gradient(135deg,#7363e8,#9a61dc)}.bubble{min-width:0;padding:2px 0;color:#dfe3eb}.bubble .role{font-size:11px;color:var(--muted);font-weight:800;margin-bottom:7px}.markdown{font-size:14px;line-height:1.75;overflow-wrap:anywhere}.markdown p{margin:.35em 0}.markdown pre{padding:14px;border:1px solid var(--line);border-radius:11px;background:#0a0c11;overflow:auto;font:12px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace}.markdown code{padding:2px 5px;border-radius:5px;background:var(--soft);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.markdown pre code{padding:0;background:transparent}.tool-card{margin:8px 0;border:1px solid var(--line);border-radius:10px;background:var(--panel);overflow:hidden}.tool-card summary{padding:9px 11px;color:#bbc2d0;font-size:12px;font-weight:700}.tool-card pre{margin:0;border:0;border-top:1px solid var(--line);border-radius:0;max-height:260px}.run-state{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px;margin:8px 0 20px}.pulse{width:8px;height:8px;border-radius:50%;background:var(--accent2);animation:pulse 1.4s infinite}@keyframes pulse{50%{opacity:.35;transform:scale(.72)}}.workspace-warning{width:min(900px,calc(100% - 36px));margin:14px auto 0;padding:9px 12px;border:1px solid rgba(242,184,75,.3);background:rgba(242,184,75,.08);border-radius:10px;color:#e8c879;font-size:11px}\n.composer-shell{position:absolute;left:286px;right:0;bottom:0;padding:0 20px 18px;background:linear-gradient(transparent,var(--bg) 32%)}.composer{width:min(900px,100%);margin:auto;border:1px solid #343b49;border-radius:17px;background:rgba(25,29,38,.96);box-shadow:var(--shadow);overflow:hidden}.composer textarea{width:100%;min-height:68px;max-height:180px;resize:none;border:0;outline:0;padding:15px 16px 8px;background:transparent;color:var(--text)}.composer textarea::placeholder{color:#737b8b}.composer-bar{display:flex;align-items:center;gap:8px;padding:7px 9px}.composer-bar input,.composer-bar select{min-width:0;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--muted);padding:7px 9px;font-size:11px}.composer-bar input{width:180px}.composer-bar select{width:104px}.send{margin-left:auto;width:36px;height:36px;border:0;border-radius:10px;background:var(--accent);color:#fff;font-size:17px}.stop{margin-left:auto;height:36px;border:1px solid rgba(255,111,137,.4);border-radius:10px;background:rgba(255,111,137,.12);color:#ff91a5;padding:0 13px;font-weight:700}.composer-disabled{opacity:.5;pointer-events:none}\n.attachment-drafts{display:flex;gap:8px;padding:0 12px;overflow-x:auto}.attachment-draft{position:relative;flex:none;width:74px}.attachment-draft img{width:74px;height:54px;object-fit:cover;border:1px solid var(--line);border-radius:9px;background:var(--soft)}.attachment-draft button{position:absolute;right:-5px;top:-6px;width:20px;height:20px;padding:0;border:1px solid var(--line);border-radius:50%;background:#fff;color:var(--danger);font-weight:900}.attach{width:36px;height:36px;border:1px solid var(--line);border-radius:10px;background:var(--soft);color:var(--muted);font-size:17px}.image-gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:9px;margin-top:10px;max-width:650px}.image-card{display:block;border:1px solid var(--line);border-radius:11px;background:var(--soft);overflow:hidden;color:var(--text);text-decoration:none}.image-card img{display:block;width:100%;height:150px;object-fit:cover;background:#eef1f6}.image-card span{display:block;padding:7px 9px;color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.composer.dragging{outline:2px solid var(--accent);outline-offset:2px}\n.modal-backdrop{position:fixed;inset:0;z-index:30;display:grid;place-items:center;padding:20px;background:rgba(4,5,8,.66);backdrop-filter:blur(7px)}.modal-backdrop.hidden{display:none}.modal{width:min(560px,100%);max-height:calc(100vh - 40px);overflow:auto;border:1px solid #353c4a;border-radius:18px;background:#171a22;box-shadow:0 30px 100px rgba(0,0,0,.55);padding:22px}.modal h2{margin:0 0 5px;font-size:20px}.modal>.subtitle{margin:0 0 20px;color:var(--muted);font-size:12px}.field{display:grid;gap:7px;margin:13px 0}.field label{font-size:11px;color:#b7becb;font-weight:800}.field input,.field select{width:100%;border:1px solid var(--line);border-radius:10px;background:#10131a;color:var(--text);padding:10px 11px;outline:none}.field input:focus,.field select:focus{border-color:var(--accent)}.row{display:grid;grid-template-columns:1fr 1fr;gap:10px}.check{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px}.modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.danger{border:1px solid rgba(255,111,137,.35);background:rgba(255,111,137,.1);color:#ff849b;border-radius:10px;padding:9px 12px;font-weight:700}.approval{width:min(620px,100%)}.approval-badge{display:inline-flex;padding:5px 8px;border-radius:999px;background:rgba(242,184,75,.13);color:#f1c565;font-size:10px;font-weight:900}.approval pre{white-space:pre-wrap;overflow-wrap:anywhere;padding:13px;border:1px solid var(--line);border-radius:10px;background:#0d0f15;color:#dce0e9}.approval-actions{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.toast{position:fixed;right:20px;top:20px;z-index:50;max-width:420px;padding:11px 14px;border-radius:10px;background:#283143;border:1px solid #3b465b;box-shadow:var(--shadow)}.toast.error{background:#602536;border-color:#913a52}.hidden{display:none!important}\n.topbar{background:rgba(255,255,255,.86)}.welcome-mark{background:var(--accent);color:#fff}.hint,.empty-thread{background:var(--panel);border-color:var(--line)}.session-item.active{background:var(--accent-soft);border-color:#d8d1ff}.status-dot{background:#a3aabd}.status-dot.running{background:var(--accent2);box-shadow:0 0 0 4px rgba(35,130,93,.1)}.message.user .avatar{background:#e8edf6}.message.assistant .avatar{background:var(--accent);color:#fff}.bubble{color:var(--text)}.markdown pre,.tool-card pre{background:var(--soft);color:var(--text)}.tool-card summary{color:var(--muted)}.composer{border-color:var(--line);background:rgba(255,255,255,.98)}.composer textarea::placeholder{color:#9aa3b4}.composer-bar input,.composer-bar select{background:var(--soft)}.modal-backdrop{background:rgba(24,32,51,.36)}.modal{border-color:var(--line);background:var(--panel);box-shadow:0 28px 90px rgba(35,43,65,.24)}.field label{color:var(--text)}.field input,.field select{background:var(--soft);color:var(--text)}.danger{border-color:#f3c4ce;background:#fff1f3;color:var(--danger)}.toast{background:#253047;color:#fff}.toast.error{background:#9f2942}.approval-card{margin:16px 0;padding:14px;border:1px solid #ead49b;border-radius:13px;background:#fffaf0;box-shadow:0 5px 18px rgba(94,70,15,.06)}.approval-card.resolved{border-color:var(--line);background:var(--soft)}.approval-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}.approval-head strong{font-size:12px}.approval-status{padding:3px 7px;border-radius:999px;background:#fff0c2;color:#8a6110;font-size:10px;font-weight:800}.approval-card.resolved .approval-status{background:#e5f5ee;color:var(--accent2)}.approval-detail{color:var(--muted);font-size:11px;margin-bottom:8px}.approval-command{margin:0;padding:10px;border:1px solid var(--line);border-radius:9px;background:#fff;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace}.approval-inline-actions{display:flex;flex-wrap:wrap;gap:7px;margin-top:10px}.approval-inline-actions button{padding:7px 10px;font-size:11px}\n.sidebar{height:100%;overflow:hidden}.main{min-height:0!important;overflow:hidden}\n@media(max-width:760px){.app{grid-template-columns:78px minmax(0,1fr)}.brand{padding:0;justify-content:center}.brand-copy,.sidebar-actions .primary,.session-item>div:not(.session-title),.session-title span:last-child,.sidebar-foot{display:none}.sidebar-actions{justify-content:center;padding:12px 8px}.session-item{padding:11px;display:grid;place-items:center}.composer-shell{left:78px}.hint-grid{grid-template-columns:1fr}.topbar{padding:0 14px}.thread{width:calc(100% - 24px)}.row{grid-template-columns:1fr}.approval-inline-actions{display:grid;grid-template-columns:1fr 1fr}}\n</style>\n</head>\n<body>\n<div class=\"app\">\n <aside class=\"sidebar\">\n <div class=\"brand\"><div class=\"mark\">D</div><div class=\"brand-copy\"><strong>DeepCCC</strong><span>LOCAL AGENT</span></div></div>\n <div class=\"sidebar-actions\"><button id=\"new-session\" class=\"primary\">\uFF0B \u65B0\u5EFA\u4F1A\u8BDD</button><button id=\"open-settings\" class=\"icon\" title=\"API \u8BBE\u7F6E\">\u2699</button></div>\n <div id=\"session-list\" class=\"session-list\"></div>\n <div class=\"sidebar-foot\"><span>127.0.0.1</span><span id=\"server-port\">28080</span></div>\n </aside>\n <main class=\"main\">\n <header class=\"topbar\"><div class=\"session-heading\"><h1 id=\"session-title\">DeepCCC</h1><p id=\"session-cwd\">\u9009\u62E9\u6216\u521B\u5EFA\u4E00\u4E2A\u4F1A\u8BDD</p></div><div class=\"top-actions\"><button id=\"rename-session\" class=\"ghost hidden\">\u91CD\u547D\u540D</button><button id=\"delete-session\" class=\"danger hidden\">\u5220\u9664</button></div></header>\n <div id=\"warning\" class=\"workspace-warning hidden\"></div>\n <section id=\"content\" class=\"content\"><div class=\"welcome\"><div class=\"welcome-card\"><div class=\"welcome-mark\">\u2726</div><h2>\u628A\u5DE5\u4F5C\u4EA4\u7ED9 DeepCCC</h2><p>\u5728\u6D4F\u89C8\u5668\u91CC\u7BA1\u7406\u591A\u4E2A\u672C\u5730 Coding Agent \u4F1A\u8BDD\uFF0C\u5207\u6362\u6A21\u578B\u4E0E\u63A8\u7406\u5F3A\u5EA6\uFF0C\u5E76\u5728\u4F1A\u8BDD\u5185\u5BA1\u6279\u64CD\u4F5C\u3002</p><div class=\"hint-grid\"><div class=\"hint\">\u591A\u4F1A\u8BDD\u5E76\u884C\u8FD0\u884C</div><div class=\"hint\">\u672C\u5730\u6587\u4EF6\u4E0E\u5386\u53F2</div><div class=\"hint\">API \u81EA\u7531\u914D\u7F6E</div></div></div></div></section>\n </main>\n</div>\n<div id=\"composer-shell\" class=\"composer-shell hidden\"><div id=\"composer\" class=\"composer\"><textarea id=\"prompt\" placeholder=\"\u63CF\u8FF0\u4F60\u60F3\u5B8C\u6210\u7684\u4EFB\u52A1\u2026\u53EF\u7C98\u8D34\u6216\u62D6\u5165\u56FE\u7247\"></textarea><div id=\"attachment-drafts\" class=\"attachment-drafts\"></div><div class=\"composer-bar\"><input id=\"attachment-input\" type=\"file\" accept=\"image/png,image/jpeg,image/webp\" multiple hidden><button id=\"attach-image\" class=\"attach\" type=\"button\" title=\"\u6DFB\u52A0\u56FE\u7247\">\uFF0B</button><input id=\"session-model\" list=\"recent-models\" aria-label=\"\u6A21\u578B\"><input id=\"session-sub-model\" list=\"recent-models\" aria-label=\"\u5B50\u6A21\u578B\" placeholder=\"\u9ED8\u8BA4\u5B50\u6A21\u578B\"><datalist id=\"recent-models\"></datalist><select id=\"session-effort\" aria-label=\"\u63A8\u7406\u5F3A\u5EA6\"><option value=\"\">\u9ED8\u8BA4 effort</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select><button id=\"send\" class=\"send\" title=\"\u53D1\u9001\">\u2191</button><button id=\"stop\" class=\"stop hidden\">\u25A0 \u505C\u6B62</button></div></div></div>\n\n<div id=\"new-modal\" class=\"modal-backdrop hidden\"><form id=\"new-form\" class=\"modal\"><h2>\u65B0\u5EFA Agent \u4F1A\u8BDD</h2><p class=\"subtitle\">\u6BCF\u4E2A\u4F1A\u8BDD\u62E5\u6709\u72EC\u7ACB\u4E0A\u4E0B\u6587\uFF1B\u540C\u76EE\u5F55\u4F1A\u8BDD\u53EF\u4EE5\u5E76\u53D1\uFF0C\u4F46\u6587\u4EF6\u4FEE\u6539\u53EF\u80FD\u51B2\u7A81\u3002</p><div class=\"field\"><label>\u5DE5\u4F5C\u76EE\u5F55</label><input id=\"new-cwd\" required></div><div class=\"field\"><label>\u4F1A\u8BDD\u540D\u79F0\uFF08\u53EF\u9009\uFF09</label><input id=\"new-title\" placeholder=\"\u9ED8\u8BA4\u4F7F\u7528\u76EE\u5F55\u540D\"></div><div class=\"row\"><div class=\"field\"><label>\u6A21\u578B</label><input id=\"new-model\" list=\"recent-models\"></div><div class=\"field\"><label>\u5B50\u6A21\u578B\uFF08\u53EF\u9009\uFF09</label><input id=\"new-sub-model\" list=\"recent-models\"></div></div><div class=\"field\"><label>Effort</label><select id=\"new-effort\"><option value=\"\">\u9ED8\u8BA4</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select></div><div class=\"modal-actions\"><button type=\"button\" data-close=\"new-modal\" class=\"ghost\">\u53D6\u6D88</button><button class=\"primary\" type=\"submit\">\u521B\u5EFA\u4F1A\u8BDD</button></div></form></div>\n\n<div id=\"settings-modal\" class=\"modal-backdrop hidden\"><form id=\"settings-form\" class=\"modal\"><h2>API \u4E0E Web \u8BBE\u7F6E</h2><p class=\"subtitle\">\u5355\u4E00 API \u914D\u7F6E\u4F5C\u4E3A\u6240\u6709\u65B0\u4F1A\u8BDD\u7684\u9ED8\u8BA4\u503C\uFF1B\u4F1A\u8BDD\u53EF\u5355\u72EC\u8986\u76D6\u6A21\u578B\u548C effort\u3002</p><div class=\"row\"><div class=\"field\"><label>API \u534F\u8BAE</label><select id=\"cfg-provider\"><option value=\"openai\">OpenAI-compatible</option><option value=\"anthropic\">Anthropic Messages</option></select></div><div class=\"field\"><label>API Key</label><input id=\"cfg-api-key\" type=\"password\" placeholder=\"\u7559\u7A7A\u4FDD\u7559\u5F53\u524D Key\"></div></div><div class=\"field\"><label>Base URL</label><input id=\"cfg-base-url\" required></div><div class=\"row\"><div class=\"field\"><label>\u9ED8\u8BA4\u6A21\u578B</label><input id=\"cfg-model\" required></div><div class=\"field\"><label>\u5B50\u6A21\u578B\uFF08\u53EF\u9009\uFF09</label><input id=\"cfg-sub-model\"></div></div><div class=\"row\"><div class=\"field\"><label>\u9ED8\u8BA4 Effort</label><select id=\"cfg-effort\"><option value=\"\">\u4E0D\u4F20</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select></div><div class=\"field\"><label>Context Window</label><input id=\"cfg-context\" type=\"number\" min=\"1\"></div></div><div class=\"row\"><div class=\"field\"><label>Web \u7AEF\u53E3</label><input id=\"cfg-port\" type=\"number\" min=\"1\" max=\"65535\"></div><div class=\"field\"><label>Max Output Tokens\uFF08\u53EF\u9009\uFF09</label><input id=\"cfg-max-output\" type=\"number\" min=\"1\"></div></div><label class=\"check\"><input id=\"cfg-streaming\" type=\"checkbox\">\u542F\u7528\u6D41\u5F0F\u8BF7\u6C42</label><label class=\"check\"><input id=\"cfg-open\" type=\"checkbox\">\u8FD0\u884C deepccc web \u65F6\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668</label><div id=\"key-state\" class=\"subtitle\"></div><div class=\"modal-actions\"><button type=\"button\" data-close=\"settings-modal\" class=\"ghost\">\u53D6\u6D88</button><button class=\"primary\" type=\"submit\">\u4FDD\u5B58\u8BBE\u7F6E</button></div></form></div>\n\n<div id=\"rename-modal\" class=\"modal-backdrop hidden\"><form id=\"rename-form\" class=\"modal\"><h2>\u91CD\u547D\u540D\u4F1A\u8BDD</h2><div class=\"field\"><label>\u540D\u79F0</label><input id=\"rename-title\" required maxlength=\"120\"></div><div class=\"modal-actions\"><button type=\"button\" data-close=\"rename-modal\" class=\"ghost\">\u53D6\u6D88</button><button class=\"primary\">\u4FDD\u5B58</button></div></form></div>\n\n<div id=\"toast\" class=\"toast hidden\"></div>\n<script>\n(function(){\n var state={sessions:[],selectedId:null,detail:null,config:null,eventSource:null,globalEventSource:null,reloadTimer:null,sessionsReloadTimer:null,detailRequestToken:0,configDrafts:{},promptDrafts:{},attachmentDrafts:{},uploading:false};\n var $=function(id){return document.getElementById(id);};\n async function api(path,options){var response=await fetch(path,Object.assign({headers:{'content-type':'application/json; charset=utf-8'}},options||{}));var data=await response.json().catch(function(){return {error:'\u54CD\u5E94\u4E0D\u662F\u6709\u6548 JSON'};});if(!response.ok)throw new Error(data.error||('HTTP '+response.status));return data;}\n function toast(message,error){var el=$('toast');el.textContent=message;el.className='toast'+(error?' error':'');clearTimeout(toast.timer);toast.timer=setTimeout(function(){el.className='toast hidden';},3500);}\n function openModal(id){$(id).classList.remove('hidden');}function closeModal(id){$(id).classList.add('hidden');}\n function escapeHtml(value){return String(value||'').replace(/[&<>\"']/g,function(ch){return {'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[ch];});}\n function markdown(value){var text=escapeHtml(value);var blocks=[];var tick=String.fromCharCode(96);var fence=tick+tick+tick;var pattern=new RegExp(fence+'([a-z0-9_-]*)\\\\n([\\\\s\\\\S]*?)'+fence,'gi');text=text.replace(pattern,function(_,lang,code){blocks.push('<pre><code data-lang=\"'+lang+'\">'+code+'</code></pre>');return '@@BLOCK'+(blocks.length-1)+'@@';});text=text.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>').replace(new RegExp(tick+'([^'+tick+']+)'+tick,'g'),'<code>$1</code>').replace(/\\n/g,'<br>');text=text.replace(/@@BLOCK(\\d+)@@/g,function(_,index){return blocks[Number(index)];});return text;}\n function statusText(status){return status==='running'?'\u6267\u884C\u4E2D':'\u7A7A\u95F2';}\n function renderSessions(){var root=$('session-list');root.innerHTML='';if(!state.sessions.length){root.innerHTML='<div class=\"session-empty\">\u8FD8\u6CA1\u6709\u4F1A\u8BDD<br>\u70B9\u51FB\u4E0A\u65B9\u6309\u94AE\u5F00\u59CB</div>';return;}state.sessions.forEach(function(session){var button=document.createElement('button');button.className='session-item'+(session.sessionId===state.selectedId?' active':'');button.innerHTML='<div class=\"session-title\"><i class=\"status-dot '+session.status+'\"></i><span>'+escapeHtml(session.title)+'</span></div><div class=\"session-meta\">'+escapeHtml(session.model||'\u9ED8\u8BA4\u6A21\u578B')+' \u00B7 '+escapeHtml(session.effort||'\u9ED8\u8BA4 effort')+'</div><div class=\"session-meta\">'+escapeHtml(session.cwd)+'</div>';button.addEventListener('click',function(){selectSession(session.sessionId).catch(function(error){toast(error.message,true);});});root.appendChild(button);});}\n function decodeAttachmentMessage(content){var start=content.indexOf('<deepccc-attachments>');var end=content.indexOf('</deepccc-attachments>',start);if(start<0||end<0)return{text:content,attachments:[]};var attachments=[];try{attachments=JSON.parse(content.slice(start+21,end).trim());if(!Array.isArray(attachments))attachments=[];}catch(error){}return{text:content.slice(0,start).trim(),attachments:attachments};}\n function attachmentUrl(sessionId,id){return '/api/sessions/'+encodeURIComponent(sessionId)+'/attachments/'+encodeURIComponent(id);}\n function attachmentGallery(items,sessionId){if(!items||!items.length)return'';return '<div class=\"image-gallery\">'+items.map(function(item){var url=attachmentUrl(sessionId,item.attachmentId);return '<a class=\"image-card\" href=\"'+escapeHtml(url)+'\" target=\"_blank\"><img src=\"'+escapeHtml(url)+'\" alt=\"'+escapeHtml(item.originalName)+'\"><span>'+escapeHtml(item.originalName)+'</span></a>';}).join('')+'</div>';}\n function artifactFromOutput(output){try{var value=typeof output==='string'?JSON.parse(output):output;return value&&typeof value.path==='string'&&String(value.mimeType||'').indexOf('image/')===0?value:null;}catch(error){return null;}}\n function artifactGallery(toolCalls,sessionId){var items=(toolCalls||[]).filter(function(call){return call.name==='present_file';}).map(function(call){return artifactFromOutput(call.output);}).filter(Boolean);if(!items.length)return'';return '<div class=\"image-gallery\">'+items.map(function(item){var url='/api/sessions/'+encodeURIComponent(sessionId)+'/artifact?path='+encodeURIComponent(item.path);return '<a class=\"image-card\" href=\"'+escapeHtml(url)+'\" target=\"_blank\"><img src=\"'+escapeHtml(url)+'\" alt=\"'+escapeHtml(item.caption||item.name)+'\"><span>'+escapeHtml(item.caption||item.name)+'</span></a>';}).join('')+'</div>';}\n function withoutToolTranscript(content){var marker='\\n\\n[\u5DE5\u5177\u8BB0\u5F55]\\n';var index=content.indexOf(marker);return index>=0?content.slice(0,index):content;}\n function messageHtml(role,content,toolCalls){var decoded=role==='user'?decodeAttachmentMessage(content):{text:withoutToolTranscript(content),attachments:[]};return '<article class=\"message '+role+'\"><div class=\"avatar\">'+(role==='user'?'\u4F60':'D')+'</div><div class=\"bubble\"><div class=\"role\">'+(role==='user'?'YOU':'DEEPCCC')+'</div><div class=\"markdown\">'+markdown(decoded.text)+'</div>'+attachmentGallery(decoded.attachments,state.detail.sessionId)+artifactGallery(toolCalls,state.detail.sessionId)+'</div></article>';}\n function toolHtml(event){var data=event.data||{};var item=data.type==='agent'?data.data:data;var artifact=item.type==='tool_result'&&item.name==='present_file'?artifactFromOutput(item.content):null;if(artifact)return artifactGallery([{name:'present_file',output:JSON.stringify(artifact)}],state.detail.sessionId);return '<details class=\"tool-card\"><summary>'+escapeHtml(item.type==='tool_use'?'\u8C03\u7528\u5DE5\u5177 \u00B7 '+(item.name||'tool'):'\u5DE5\u5177\u7ED3\u679C \u00B7 '+(item.name||item.tool_use_id||'tool'))+'</summary><pre>'+escapeHtml(JSON.stringify(item.type==='tool_use'?item.input:item.content,null,2))+'</pre></details>';}\n function approvalAnswerLabel(answer){var labels={allow:'\u5DF2\u5141\u8BB8\u4E00\u6B21','allow-session':'\u672C\u4F1A\u8BDD\u5DF2\u5141\u8BB8','allow-always':'\u5DF2\u6C38\u4E45\u5141\u8BB8',deny:'\u5DF2\u62D2\u7EDD'};return labels[answer]||'\u7B49\u5F85\u51B3\u5B9A';}\n function approvalHtml(approval){var resolved=approval.status==='resolved';var html='<section class=\"approval-card'+(resolved?' resolved':'')+'\"><div class=\"approval-head\"><strong>\u26A0 \u64CD\u4F5C\u5BA1\u6279</strong><span class=\"approval-status\">'+escapeHtml(resolved?approvalAnswerLabel(approval.answer):'\u7B49\u5F85\u6279\u51C6')+'</span></div><div class=\"approval-detail\">'+escapeHtml(approval.detail||('\u5DE5\u5177\uFF1A'+approval.tool))+'</div><pre class=\"approval-command\">'+escapeHtml(approval.action)+'</pre>';if(!resolved)html+='<div class=\"approval-inline-actions\"><button class=\"danger\" data-approval-answer=\"deny\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u62D2\u7EDD</button><button class=\"ghost\" data-approval-answer=\"allow\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u5141\u8BB8\u4E00\u6B21</button><button class=\"ghost\" data-approval-answer=\"allow-session\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u672C\u4F1A\u8BDD\u5141\u8BB8</button><button class=\"primary\" data-approval-answer=\"allow-always\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u6C38\u4E45\u5141\u8BB8</button></div>';return html+'</section>';}\n function sessionDraft(detail){return state.configDrafts[detail.sessionId]||{model:detail.model||'',subModel:detail.subModel||'',effort:detail.effort||''};}\n function rememberDraft(){if(!state.selectedId)return;state.configDrafts[state.selectedId]={model:$('session-model').value,subModel:$('session-sub-model').value,effort:$('session-effort').value};}\n function rememberPromptDraft(){if(!state.selectedId)return;state.promptDrafts[state.selectedId]=$('prompt').value;}\n function currentAttachments(){return state.attachmentDrafts[state.selectedId]||[];}\n function renderAttachmentDrafts(){var root=$('attachment-drafts');if(!root)return;root.innerHTML=currentAttachments().map(function(item){return '<div class=\"attachment-draft\"><img src=\"'+escapeHtml(attachmentUrl(state.selectedId,item.attachmentId))+'\" alt=\"'+escapeHtml(item.originalName)+'\"><button type=\"button\" data-remove-attachment=\"'+escapeHtml(item.attachmentId)+'\">\u00D7</button></div>';}).join('');}\n function clearSelection(message){if(state.eventSource)state.eventSource.close();state.eventSource=null;state.detailRequestToken++;state.selectedId=null;state.detail=null;$('composer-shell').classList.add('hidden');$('rename-session').classList.add('hidden');$('delete-session').classList.add('hidden');$('warning').classList.add('hidden');$('session-title').textContent='DeepCCC';$('session-cwd').textContent='\u9009\u62E9\u6216\u521B\u5EFA\u4E00\u4E2A\u4F1A\u8BDD';$('content').innerHTML='<div class=\"welcome\"><div class=\"welcome-card\"><div class=\"welcome-mark\">\u2726</div><h2>'+escapeHtml(message||'\u9009\u62E9\u4E00\u4E2A\u4F1A\u8BDD')+'</h2><p>\u4ECE\u5DE6\u4FA7\u9009\u62E9\u5176\u4ED6\u4F1A\u8BDD\u6216\u521B\u5EFA\u4E00\u4E2A\u65B0\u7684\u4EFB\u52A1\u3002</p></div></div>';}\n function renderDetail(){var detail=state.detail;if(!detail)return;var draft=sessionDraft(detail);$('session-title').textContent=detail.title;$('session-cwd').textContent=detail.cwd+' \u00B7 '+statusText(detail.status);$('rename-session').classList.remove('hidden');$('delete-session').classList.remove('hidden');$('composer-shell').classList.remove('hidden');$('prompt').value=state.promptDrafts[detail.sessionId]||'';$('session-model').value=draft.model;$('session-sub-model').value=draft.subModel;$('session-effort').value=draft.effort;renderAttachmentDrafts();var html='<div class=\"thread\">';if(!(detail.messages||[]).length&&detail.status!=='running')html+='<div class=\"empty-thread\"><strong>\u51C6\u5907\u5F00\u59CB\u8FD9\u4E2A\u4F1A\u8BDD</strong><span>\u5728\u4E0B\u65B9\u8F93\u5165\u4EFB\u52A1\uFF0C\u53EF\u9644\u52A0\u622A\u56FE\u3002DeepCCC \u4F1A\u8BFB\u53D6\u5F53\u524D\u9879\u76EE\u6307\u4EE4\u3001\u8C03\u7528\u5DE5\u5177\u5E76\u6301\u7EED\u4FDD\u5B58\u4E0A\u4E0B\u6587\u3002</span></div>';(detail.messages||[]).forEach(function(message){html+=messageHtml(message.role,message.content,message.toolCalls);});if(detail.status==='running'){var live=(detail.events||[]).slice().reverse().find(function(event){return event.type==='agent'&&event.data&&event.data.type==='text';});var current=(detail.events||[]).filter(function(event){return event.type==='agent'&&event.data&&(event.data.type==='tool_use'||event.data.type==='tool_result');});current.forEach(function(event){html+=toolHtml(event);});if(live)html+=messageHtml('assistant',live.data.accumulated||live.data.text||'',[]);}var approvals=(detail.approvals||[]).slice();if(detail.pendingApproval&&!approvals.some(function(item){return item.approvalId===detail.pendingApproval.approvalId;}))approvals.push(Object.assign({status:'pending'},detail.pendingApproval));approvals.forEach(function(approval){html+=approvalHtml(approval);});if(detail.status==='running')html+='<div class=\"run-state\"><i class=\"pulse\"></i>Agent \u6B63\u5728\u5DE5\u4F5C</div>';html+='</div>';$('content').innerHTML=html;$('send').classList.toggle('hidden',detail.status==='running');$('stop').classList.toggle('hidden',detail.status!=='running');$('prompt').disabled=detail.status==='running';$('session-model').disabled=detail.status==='running';$('session-sub-model').disabled=detail.status==='running';$('session-effort').disabled=detail.status==='running';$('attach-image').disabled=detail.status==='running'||state.uploading;renderWarning();requestAnimationFrame(function(){$('content').scrollTop=$('content').scrollHeight;});}\n function cwdKey(value){var raw=String(value||'');var normalized=raw.replace(/\\\\/g,'/').replace(new RegExp('/+$'),'');var windowsStyle=new RegExp('^[a-z]:/','i').test(normalized)||raw.indexOf(String.fromCharCode(92))>=0;return windowsStyle?normalized.toLowerCase():normalized;}\n function renderWarning(){if(!state.detail)return;var selectedCwd=cwdKey(state.detail.cwd);var conflicts=state.sessions.filter(function(item){return item.sessionId!==state.detail.sessionId&&item.status==='running'&&cwdKey(item.cwd)===selectedCwd;});var el=$('warning');if(conflicts.length){el.textContent='\u26A0 \u5F53\u524D\u76EE\u5F55\u8FD8\u6709 '+conflicts.length+' \u4E2A Agent \u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u3002\u591A\u4E2A\u4F1A\u8BDD\u76F4\u63A5\u4FEE\u6539\u540C\u4E00\u76EE\u5F55\uFF0C\u53EF\u80FD\u4EA7\u751F\u8986\u76D6\u6216\u51B2\u7A81\u3002';el.classList.remove('hidden');}else el.classList.add('hidden');}\n async function loadSessions(){var result=await api('/api/sessions');state.sessions=result.sessions||[];if(state.selectedId&&!state.sessions.some(function(item){return item.sessionId===state.selectedId;}))clearSelection('\u4F1A\u8BDD\u5DF2\u5728\u5176\u4ED6\u9875\u9762\u5220\u9664');renderSessions();renderWarning();}\n async function loadDetail(id,token){var sessionId=id||state.selectedId;if(!sessionId)return false;var requestToken=token===undefined?++state.detailRequestToken:token;var result=await api('/api/sessions/'+encodeURIComponent(sessionId));if(state.selectedId!==sessionId||requestToken!==state.detailRequestToken)return false;await loadSessions();if(state.selectedId!==sessionId||requestToken!==state.detailRequestToken)return false;state.detail=result.session;renderDetail();return true;}\n function scheduleDetail(id){if(id&&id!==state.selectedId)return;clearTimeout(state.reloadTimer);var token=++state.detailRequestToken;state.reloadTimer=setTimeout(function(){loadDetail(id||state.selectedId,token).catch(function(error){toast(error.message,true);});},120);}\n function scheduleSessions(){clearTimeout(state.sessionsReloadTimer);state.sessionsReloadTimer=setTimeout(function(){loadSessions().catch(function(error){toast(error.message,true);});},80);}\n async function selectSession(id){state.selectedId=id;var token=++state.detailRequestToken;renderSessions();if(state.eventSource)state.eventSource.close();state.eventSource=new EventSource('/api/sessions/'+encodeURIComponent(id)+'/events');state.eventSource.onmessage=function(){scheduleDetail(id);};await loadDetail(id,token);}\n async function loadConfig(){var result=await api('/api/config');state.config=result.config;$('cfg-provider').value=state.config.provider;$('cfg-base-url').value=state.config.baseURL;$('cfg-model').value=state.config.model;$('cfg-sub-model').value=state.config.subModel||'';$('cfg-effort').value=state.config.effort||'';$('cfg-context').value=state.config.contextWindow;$('cfg-max-output').value=state.config.maxOutputTokens||'';$('cfg-streaming').checked=state.config.streaming!==false;$('cfg-port').value=state.config.web.port;$('cfg-open').checked=state.config.web.openOnStart!==false;$('server-port').textContent=state.config.web.port;$('key-state').textContent=state.config.apiKeyConfigured?'\u5F53\u524D API Key\uFF1A'+state.config.apiKeyMask:'\u5C1A\u672A\u914D\u7F6E API Key';$('new-cwd').value=state.config.defaultCwd||'';$('new-model').value=state.config.model;$('new-sub-model').value=state.config.subModel||'';var models=[state.config.model,state.config.subModel].concat(state.sessions.reduce(function(all,item){return all.concat([item.model,item.subModel]);},[])).filter(Boolean).filter(function(value,index,array){return array.indexOf(value)===index;});$('recent-models').innerHTML=models.map(function(model){return '<option value=\"'+escapeHtml(model)+'\">';}).join('');}\n $('new-session').addEventListener('click',function(){openModal('new-modal');setTimeout(function(){$('new-cwd').focus();},0);});$('open-settings').addEventListener('click',function(){loadConfig().then(function(){openModal('settings-modal');}).catch(function(error){toast(error.message,true);});});document.querySelectorAll('[data-close]').forEach(function(button){button.addEventListener('click',function(){closeModal(button.dataset.close);});});\n $('new-form').addEventListener('submit',async function(event){event.preventDefault();try{var result=await api('/api/sessions',{method:'POST',body:JSON.stringify({cwd:$('new-cwd').value,title:$('new-title').value,model:$('new-model').value,subModel:$('new-sub-model').value,effort:$('new-effort').value})});closeModal('new-modal');$('new-title').value='';await loadSessions();await selectSession(result.session.sessionId);}catch(error){toast(error.message,true);}});\n $('settings-form').addEventListener('submit',async function(event){event.preventDefault();var body={provider:$('cfg-provider').value,baseURL:$('cfg-base-url').value,model:$('cfg-model').value,subModel:$('cfg-sub-model').value,effort:$('cfg-effort').value,contextWindow:Number($('cfg-context').value),streaming:$('cfg-streaming').checked,web:{port:Number($('cfg-port').value),openOnStart:$('cfg-open').checked}};if($('cfg-max-output').value)body.maxOutputTokens=Number($('cfg-max-output').value);if($('cfg-api-key').value)body.apiKey=$('cfg-api-key').value;try{await api('/api/config',{method:'PUT',body:JSON.stringify(body)});$('cfg-api-key').value='';closeModal('settings-modal');toast('\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u65B0\u914D\u7F6E\u4ECE\u4E0B\u4E00\u6B21\u65B0\u5EFA\u4F1A\u8BDD\u751F\u6548');await loadConfig();}catch(error){toast(error.message,true);}});\n async function uploadAttachment(sessionId,file){var response=await fetch('/api/sessions/'+encodeURIComponent(sessionId)+'/attachments?name='+encodeURIComponent(file.name||'image'),{method:'POST',headers:{'content-type':file.type||'application/octet-stream'},body:file});var result=await response.json().catch(function(){return{error:'\u4E0A\u4F20\u54CD\u5E94\u4E0D\u662F\u6709\u6548 JSON'};});if(!response.ok)throw new Error(result.error||('HTTP '+response.status));return result.attachment;}\n async function handleAttachmentFiles(files){var sessionId=state.selectedId;if(!sessionId||state.uploading)return;var candidates=[].slice.call(files||[]).filter(function(file){return ['image/png','image/jpeg','image/webp'].includes(file.type);});var existing=state.attachmentDrafts[sessionId]||[];if(existing.length+candidates.length>10){toast('\u6BCF\u6761\u6D88\u606F\u6700\u591A\u6DFB\u52A0 10 \u5F20\u56FE\u7247',true);return;}if(!candidates.length){toast('\u4EC5\u652F\u6301 PNG\u3001JPEG\u3001WebP \u56FE\u7247',true);return;}state.uploading=true;if(state.detail)renderDetail();try{for(var i=0;i<candidates.length;i++){if(candidates[i].size>20*1024*1024)throw new Error('\u5355\u5F20\u56FE\u7247\u4E0D\u80FD\u8D85\u8FC7 20 MB\uFF1A'+candidates[i].name);existing.push(await uploadAttachment(sessionId,candidates[i]));state.attachmentDrafts[sessionId]=existing;renderAttachmentDrafts();}}catch(error){toast(error.message,true);}finally{state.uploading=false;if(state.detail)renderDetail();$('attachment-input').value='';}}\n async function removeAttachment(id){var sessionId=state.selectedId;if(!sessionId)return;try{await api('/api/sessions/'+encodeURIComponent(sessionId)+'/attachments/'+encodeURIComponent(id),{method:'DELETE'});state.attachmentDrafts[sessionId]=(state.attachmentDrafts[sessionId]||[]).filter(function(item){return item.attachmentId!==id;});renderAttachmentDrafts();}catch(error){toast(error.message,true);}}\n async function send(){var sessionId=state.selectedId;var text=$('prompt').value;var attachments=currentAttachments();if(!sessionId||(!text.trim()&&!attachments.length)||state.uploading)return;try{var patch={model:$('session-model').value,subModel:$('session-sub-model').value,effort:$('session-effort').value};if(patch.model!==state.detail.model||patch.subModel!==state.detail.subModel||patch.effort!==state.detail.effort){var updated=await api('/api/sessions/'+encodeURIComponent(sessionId),{method:'PATCH',body:JSON.stringify(patch)});if(state.selectedId===sessionId){state.detail=Object.assign({},state.detail,updated.session);renderDetail();}await loadSessions();}await api('/api/sessions/'+encodeURIComponent(sessionId)+'/messages',{method:'POST',body:JSON.stringify({text:text,attachmentIds:attachments.map(function(item){return item.attachmentId;})})});delete state.configDrafts[sessionId];delete state.promptDrafts[sessionId];delete state.attachmentDrafts[sessionId];if(state.selectedId===sessionId){$('prompt').value='';renderAttachmentDrafts();}var token=++state.detailRequestToken;await loadDetail(sessionId,token);}catch(error){toast(error.message,true);}}\n ['session-model','session-sub-model','session-effort'].forEach(function(id){$(id).addEventListener('input',rememberDraft);$(id).addEventListener('change',rememberDraft);});\n $('attach-image').addEventListener('click',function(){$('attachment-input').click();});$('attachment-input').addEventListener('change',function(){handleAttachmentFiles(this.files);});$('attachment-drafts').addEventListener('click',function(event){var button=event.target.closest('[data-remove-attachment]');if(button)removeAttachment(button.dataset.removeAttachment);});document.addEventListener('paste',function(event){var files=[].slice.call(event.clipboardData&&event.clipboardData.files||[]).filter(function(file){return file.type.indexOf('image/')===0;});if(files.length){event.preventDefault();handleAttachmentFiles(files);}});$('composer').addEventListener('dragover',function(event){event.preventDefault();this.classList.add('dragging');});$('composer').addEventListener('dragleave',function(){this.classList.remove('dragging');});$('composer').addEventListener('drop',function(event){event.preventDefault();this.classList.remove('dragging');handleAttachmentFiles(event.dataTransfer.files);});\n $('send').addEventListener('click',send);$('prompt').addEventListener('input',rememberPromptDraft);$('prompt').addEventListener('keydown',function(event){if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();send();}});$('stop').addEventListener('click',async function(){try{await api('/api/sessions/'+encodeURIComponent(state.selectedId)+'/stop',{method:'POST',body:'{}'});toast('\u5DF2\u8BF7\u6C42\u505C\u6B62');}catch(error){toast(error.message,true);}});\n $('rename-session').addEventListener('click',function(){$('rename-title').value=state.detail.title;openModal('rename-modal');});$('rename-form').addEventListener('submit',async function(event){event.preventDefault();try{await api('/api/sessions/'+encodeURIComponent(state.selectedId),{method:'PATCH',body:JSON.stringify({title:$('rename-title').value})});closeModal('rename-modal');var token=++state.detailRequestToken;await loadDetail(state.selectedId,token);}catch(error){toast(error.message,true);}});$('delete-session').addEventListener('click',async function(){if(!confirm('\u5220\u9664\u8FD9\u4E2A\u4F1A\u8BDD\u53CA\u5176\u672C\u5730\u5386\u53F2\uFF1F\u4E0D\u4F1A\u5220\u9664\u5DE5\u4F5C\u76EE\u5F55\u4E2D\u7684\u6587\u4EF6\u3002'))return;try{var deletedId=state.selectedId;await api('/api/sessions/'+encodeURIComponent(deletedId),{method:'DELETE'});delete state.configDrafts[deletedId];delete state.promptDrafts[deletedId];delete state.attachmentDrafts[deletedId];clearSelection('\u4F1A\u8BDD\u5DF2\u5220\u9664');await loadSessions();}catch(error){toast(error.message,true);}});\n $('content').addEventListener('click',async function(event){var button=event.target.closest('[data-approval-answer]');if(!button)return;button.disabled=true;try{await api('/api/approvals/'+encodeURIComponent(button.dataset.approvalId),{method:'POST',body:JSON.stringify({answer:button.dataset.approvalAnswer})});var token=++state.detailRequestToken;await loadDetail(state.selectedId,token);}catch(error){button.disabled=false;toast(error.message,true);}});\n function handleGlobalEvent(event){try{var update=JSON.parse(event.data);if(update.type==='session_deleted'){delete state.configDrafts[update.sessionId];delete state.promptDrafts[update.sessionId];delete state.attachmentDrafts[update.sessionId];}}catch(error){}scheduleSessions();}\n state.globalEventSource=new EventSource('/api/events');state.globalEventSource.onmessage=handleGlobalEvent;\n Promise.all([loadSessions(),loadConfig()]).then(function(){if(state.sessions[0])return selectSession(state.sessions[0].sessionId);}).catch(function(error){toast(error.message,true);});\n})();\n</script>\n</body></html>";
package/dist/web-page.js CHANGED
@@ -12,6 +12,7 @@ export const DEEPCCC_WEB_PAGE = `<!DOCTYPE html>
12
12
  .main{min-width:0;height:100%;display:flex;flex-direction:column}.topbar{height:70px;flex:none;display:flex;align-items:center;justify-content:space-between;padding:0 22px;border-bottom:1px solid var(--line);background:rgba(12,14,19,.7);backdrop-filter:blur(18px)}.session-heading{min-width:0}.session-heading h1{font-size:16px;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.session-heading p{font-size:11px;color:var(--muted);margin:3px 0 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.top-actions{display:flex;gap:8px}.top-actions button{height:38px}.content{flex:1;min-height:0;overflow-y:auto;scroll-behavior:smooth}.welcome{height:100%;display:grid;place-items:center;padding:30px}.welcome-card{max-width:650px;text-align:center}.welcome-mark{width:72px;height:72px;margin:0 auto 20px;border-radius:24px;display:grid;place-items:center;background:linear-gradient(135deg,#7764eb,#b45fd7);font-size:29px;box-shadow:0 20px 50px rgba(119,100,235,.28)}.welcome h2{font-size:28px;margin:0 0 10px;letter-spacing:-.02em}.welcome p{color:var(--muted);margin:0 auto;max-width:480px}.hint-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:28px}.hint{padding:14px;border:1px solid var(--line);border-radius:13px;background:rgba(24,28,37,.65);font-size:12px;color:var(--muted)}
13
13
  .thread{width:min(900px,calc(100% - 36px));margin:0 auto;padding:28px 0 150px}.empty-thread{margin:80px auto 0;max-width:520px;padding:28px;border:1px dashed #353c4a;border-radius:18px;background:rgba(20,23,31,.55);text-align:center}.empty-thread strong{display:block;font-size:18px;margin-bottom:7px}.empty-thread span{color:var(--muted);font-size:12px}.message{display:grid;grid-template-columns:30px minmax(0,1fr);gap:12px;margin:0 0 22px}.avatar{width:30px;height:30px;border-radius:10px;display:grid;place-items:center;background:var(--soft);font-size:11px;font-weight:900}.message.user .avatar{background:#304058}.message.assistant .avatar{background:linear-gradient(135deg,#7363e8,#9a61dc)}.bubble{min-width:0;padding:2px 0;color:#dfe3eb}.bubble .role{font-size:11px;color:var(--muted);font-weight:800;margin-bottom:7px}.markdown{font-size:14px;line-height:1.75;overflow-wrap:anywhere}.markdown p{margin:.35em 0}.markdown pre{padding:14px;border:1px solid var(--line);border-radius:11px;background:#0a0c11;overflow:auto;font:12px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace}.markdown code{padding:2px 5px;border-radius:5px;background:var(--soft);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.markdown pre code{padding:0;background:transparent}.tool-card{margin:8px 0;border:1px solid var(--line);border-radius:10px;background:var(--panel);overflow:hidden}.tool-card summary{padding:9px 11px;color:#bbc2d0;font-size:12px;font-weight:700}.tool-card pre{margin:0;border:0;border-top:1px solid var(--line);border-radius:0;max-height:260px}.run-state{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px;margin:8px 0 20px}.pulse{width:8px;height:8px;border-radius:50%;background:var(--accent2);animation:pulse 1.4s infinite}@keyframes pulse{50%{opacity:.35;transform:scale(.72)}}.workspace-warning{width:min(900px,calc(100% - 36px));margin:14px auto 0;padding:9px 12px;border:1px solid rgba(242,184,75,.3);background:rgba(242,184,75,.08);border-radius:10px;color:#e8c879;font-size:11px}
14
14
  .composer-shell{position:absolute;left:286px;right:0;bottom:0;padding:0 20px 18px;background:linear-gradient(transparent,var(--bg) 32%)}.composer{width:min(900px,100%);margin:auto;border:1px solid #343b49;border-radius:17px;background:rgba(25,29,38,.96);box-shadow:var(--shadow);overflow:hidden}.composer textarea{width:100%;min-height:68px;max-height:180px;resize:none;border:0;outline:0;padding:15px 16px 8px;background:transparent;color:var(--text)}.composer textarea::placeholder{color:#737b8b}.composer-bar{display:flex;align-items:center;gap:8px;padding:7px 9px}.composer-bar input,.composer-bar select{min-width:0;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--muted);padding:7px 9px;font-size:11px}.composer-bar input{width:180px}.composer-bar select{width:104px}.send{margin-left:auto;width:36px;height:36px;border:0;border-radius:10px;background:var(--accent);color:#fff;font-size:17px}.stop{margin-left:auto;height:36px;border:1px solid rgba(255,111,137,.4);border-radius:10px;background:rgba(255,111,137,.12);color:#ff91a5;padding:0 13px;font-weight:700}.composer-disabled{opacity:.5;pointer-events:none}
15
+ .attachment-drafts{display:flex;gap:8px;padding:0 12px;overflow-x:auto}.attachment-draft{position:relative;flex:none;width:74px}.attachment-draft img{width:74px;height:54px;object-fit:cover;border:1px solid var(--line);border-radius:9px;background:var(--soft)}.attachment-draft button{position:absolute;right:-5px;top:-6px;width:20px;height:20px;padding:0;border:1px solid var(--line);border-radius:50%;background:#fff;color:var(--danger);font-weight:900}.attach{width:36px;height:36px;border:1px solid var(--line);border-radius:10px;background:var(--soft);color:var(--muted);font-size:17px}.image-gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:9px;margin-top:10px;max-width:650px}.image-card{display:block;border:1px solid var(--line);border-radius:11px;background:var(--soft);overflow:hidden;color:var(--text);text-decoration:none}.image-card img{display:block;width:100%;height:150px;object-fit:cover;background:#eef1f6}.image-card span{display:block;padding:7px 9px;color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.composer.dragging{outline:2px solid var(--accent);outline-offset:2px}
15
16
  .modal-backdrop{position:fixed;inset:0;z-index:30;display:grid;place-items:center;padding:20px;background:rgba(4,5,8,.66);backdrop-filter:blur(7px)}.modal-backdrop.hidden{display:none}.modal{width:min(560px,100%);max-height:calc(100vh - 40px);overflow:auto;border:1px solid #353c4a;border-radius:18px;background:#171a22;box-shadow:0 30px 100px rgba(0,0,0,.55);padding:22px}.modal h2{margin:0 0 5px;font-size:20px}.modal>.subtitle{margin:0 0 20px;color:var(--muted);font-size:12px}.field{display:grid;gap:7px;margin:13px 0}.field label{font-size:11px;color:#b7becb;font-weight:800}.field input,.field select{width:100%;border:1px solid var(--line);border-radius:10px;background:#10131a;color:var(--text);padding:10px 11px;outline:none}.field input:focus,.field select:focus{border-color:var(--accent)}.row{display:grid;grid-template-columns:1fr 1fr;gap:10px}.check{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px}.modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.danger{border:1px solid rgba(255,111,137,.35);background:rgba(255,111,137,.1);color:#ff849b;border-radius:10px;padding:9px 12px;font-weight:700}.approval{width:min(620px,100%)}.approval-badge{display:inline-flex;padding:5px 8px;border-radius:999px;background:rgba(242,184,75,.13);color:#f1c565;font-size:10px;font-weight:900}.approval pre{white-space:pre-wrap;overflow-wrap:anywhere;padding:13px;border:1px solid var(--line);border-radius:10px;background:#0d0f15;color:#dce0e9}.approval-actions{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.toast{position:fixed;right:20px;top:20px;z-index:50;max-width:420px;padding:11px 14px;border-radius:10px;background:#283143;border:1px solid #3b465b;box-shadow:var(--shadow)}.toast.error{background:#602536;border-color:#913a52}.hidden{display:none!important}
16
17
  .topbar{background:rgba(255,255,255,.86)}.welcome-mark{background:var(--accent);color:#fff}.hint,.empty-thread{background:var(--panel);border-color:var(--line)}.session-item.active{background:var(--accent-soft);border-color:#d8d1ff}.status-dot{background:#a3aabd}.status-dot.running{background:var(--accent2);box-shadow:0 0 0 4px rgba(35,130,93,.1)}.message.user .avatar{background:#e8edf6}.message.assistant .avatar{background:var(--accent);color:#fff}.bubble{color:var(--text)}.markdown pre,.tool-card pre{background:var(--soft);color:var(--text)}.tool-card summary{color:var(--muted)}.composer{border-color:var(--line);background:rgba(255,255,255,.98)}.composer textarea::placeholder{color:#9aa3b4}.composer-bar input,.composer-bar select{background:var(--soft)}.modal-backdrop{background:rgba(24,32,51,.36)}.modal{border-color:var(--line);background:var(--panel);box-shadow:0 28px 90px rgba(35,43,65,.24)}.field label{color:var(--text)}.field input,.field select{background:var(--soft);color:var(--text)}.danger{border-color:#f3c4ce;background:#fff1f3;color:var(--danger)}.toast{background:#253047;color:#fff}.toast.error{background:#9f2942}.approval-card{margin:16px 0;padding:14px;border:1px solid #ead49b;border-radius:13px;background:#fffaf0;box-shadow:0 5px 18px rgba(94,70,15,.06)}.approval-card.resolved{border-color:var(--line);background:var(--soft)}.approval-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}.approval-head strong{font-size:12px}.approval-status{padding:3px 7px;border-radius:999px;background:#fff0c2;color:#8a6110;font-size:10px;font-weight:800}.approval-card.resolved .approval-status{background:#e5f5ee;color:var(--accent2)}.approval-detail{color:var(--muted);font-size:11px;margin-bottom:8px}.approval-command{margin:0;padding:10px;border:1px solid var(--line);border-radius:9px;background:#fff;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace}.approval-inline-actions{display:flex;flex-wrap:wrap;gap:7px;margin-top:10px}.approval-inline-actions button{padding:7px 10px;font-size:11px}
17
18
  .sidebar{height:100%;overflow:hidden}.main{min-height:0!important;overflow:hidden}
@@ -32,7 +33,7 @@ export const DEEPCCC_WEB_PAGE = `<!DOCTYPE html>
32
33
  <section id="content" class="content"><div class="welcome"><div class="welcome-card"><div class="welcome-mark">✦</div><h2>把工作交给 DeepCCC</h2><p>在浏览器里管理多个本地 Coding Agent 会话,切换模型与推理强度,并在会话内审批操作。</p><div class="hint-grid"><div class="hint">多会话并行运行</div><div class="hint">本地文件与历史</div><div class="hint">API 自由配置</div></div></div></div></section>
33
34
  </main>
34
35
  </div>
35
- <div id="composer-shell" class="composer-shell hidden"><div class="composer"><textarea id="prompt" placeholder="描述你想完成的任务…"></textarea><div class="composer-bar"><input id="session-model" list="recent-models" aria-label="模型"><input id="session-sub-model" list="recent-models" aria-label="子模型" placeholder="默认子模型"><datalist id="recent-models"></datalist><select id="session-effort" aria-label="推理强度"><option value="">默认 effort</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select><button id="send" class="send" title="发送">↑</button><button id="stop" class="stop hidden">■ 停止</button></div></div></div>
36
+ <div id="composer-shell" class="composer-shell hidden"><div id="composer" class="composer"><textarea id="prompt" placeholder="描述你想完成的任务…可粘贴或拖入图片"></textarea><div id="attachment-drafts" class="attachment-drafts"></div><div class="composer-bar"><input id="attachment-input" type="file" accept="image/png,image/jpeg,image/webp" multiple hidden><button id="attach-image" class="attach" type="button" title="添加图片">+</button><input id="session-model" list="recent-models" aria-label="模型"><input id="session-sub-model" list="recent-models" aria-label="子模型" placeholder="默认子模型"><datalist id="recent-models"></datalist><select id="session-effort" aria-label="推理强度"><option value="">默认 effort</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select><button id="send" class="send" title="发送">↑</button><button id="stop" class="stop hidden">■ 停止</button></div></div></div>
36
37
 
37
38
  <div id="new-modal" class="modal-backdrop hidden"><form id="new-form" class="modal"><h2>新建 Agent 会话</h2><p class="subtitle">每个会话拥有独立上下文;同目录会话可以并发,但文件修改可能冲突。</p><div class="field"><label>工作目录</label><input id="new-cwd" required></div><div class="field"><label>会话名称(可选)</label><input id="new-title" placeholder="默认使用目录名"></div><div class="row"><div class="field"><label>模型</label><input id="new-model" list="recent-models"></div><div class="field"><label>子模型(可选)</label><input id="new-sub-model" list="recent-models"></div></div><div class="field"><label>Effort</label><select id="new-effort"><option value="">默认</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select></div><div class="modal-actions"><button type="button" data-close="new-modal" class="ghost">取消</button><button class="primary" type="submit">创建会话</button></div></form></div>
38
39
 
@@ -43,7 +44,7 @@ export const DEEPCCC_WEB_PAGE = `<!DOCTYPE html>
43
44
  <div id="toast" class="toast hidden"></div>
44
45
  <script>
45
46
  (function(){
46
- var state={sessions:[],selectedId:null,detail:null,config:null,eventSource:null,globalEventSource:null,reloadTimer:null,sessionsReloadTimer:null,detailRequestToken:0,configDrafts:{},promptDrafts:{}};
47
+ var state={sessions:[],selectedId:null,detail:null,config:null,eventSource:null,globalEventSource:null,reloadTimer:null,sessionsReloadTimer:null,detailRequestToken:0,configDrafts:{},promptDrafts:{},attachmentDrafts:{},uploading:false};
47
48
  var $=function(id){return document.getElementById(id);};
48
49
  async function api(path,options){var response=await fetch(path,Object.assign({headers:{'content-type':'application/json; charset=utf-8'}},options||{}));var data=await response.json().catch(function(){return {error:'响应不是有效 JSON'};});if(!response.ok)throw new Error(data.error||('HTTP '+response.status));return data;}
49
50
  function toast(message,error){var el=$('toast');el.textContent=message;el.className='toast'+(error?' error':'');clearTimeout(toast.timer);toast.timer=setTimeout(function(){el.className='toast hidden';},3500);}
@@ -52,15 +53,23 @@ export const DEEPCCC_WEB_PAGE = `<!DOCTYPE html>
52
53
  function markdown(value){var text=escapeHtml(value);var blocks=[];var tick=String.fromCharCode(96);var fence=tick+tick+tick;var pattern=new RegExp(fence+'([a-z0-9_-]*)\\\\n([\\\\s\\\\S]*?)'+fence,'gi');text=text.replace(pattern,function(_,lang,code){blocks.push('<pre><code data-lang="'+lang+'">'+code+'</code></pre>');return '@@BLOCK'+(blocks.length-1)+'@@';});text=text.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>').replace(new RegExp(tick+'([^'+tick+']+)'+tick,'g'),'<code>$1</code>').replace(/\\n/g,'<br>');text=text.replace(/@@BLOCK(\\d+)@@/g,function(_,index){return blocks[Number(index)];});return text;}
53
54
  function statusText(status){return status==='running'?'执行中':'空闲';}
54
55
  function renderSessions(){var root=$('session-list');root.innerHTML='';if(!state.sessions.length){root.innerHTML='<div class="session-empty">还没有会话<br>点击上方按钮开始</div>';return;}state.sessions.forEach(function(session){var button=document.createElement('button');button.className='session-item'+(session.sessionId===state.selectedId?' active':'');button.innerHTML='<div class="session-title"><i class="status-dot '+session.status+'"></i><span>'+escapeHtml(session.title)+'</span></div><div class="session-meta">'+escapeHtml(session.model||'默认模型')+' · '+escapeHtml(session.effort||'默认 effort')+'</div><div class="session-meta">'+escapeHtml(session.cwd)+'</div>';button.addEventListener('click',function(){selectSession(session.sessionId).catch(function(error){toast(error.message,true);});});root.appendChild(button);});}
55
- function messageHtml(role,content){return '<article class="message '+role+'"><div class="avatar">'+(role==='user'?'你':'D')+'</div><div class="bubble"><div class="role">'+(role==='user'?'YOU':'DEEPCCC')+'</div><div class="markdown">'+markdown(content)+'</div></div></article>';}
56
- function toolHtml(event){var data=event.data||{};var item=data.type==='agent'?data.data:data;return '<details class="tool-card"><summary>'+escapeHtml(item.type==='tool_use'?'调用工具 · '+(item.name||'tool'):'工具结果 · '+(item.name||item.tool_use_id||'tool'))+'</summary><pre>'+escapeHtml(JSON.stringify(item.type==='tool_use'?item.input:item.content,null,2))+'</pre></details>';}
56
+ function decodeAttachmentMessage(content){var start=content.indexOf('<deepccc-attachments>');var end=content.indexOf('</deepccc-attachments>',start);if(start<0||end<0)return{text:content,attachments:[]};var attachments=[];try{attachments=JSON.parse(content.slice(start+21,end).trim());if(!Array.isArray(attachments))attachments=[];}catch(error){}return{text:content.slice(0,start).trim(),attachments:attachments};}
57
+ function attachmentUrl(sessionId,id){return '/api/sessions/'+encodeURIComponent(sessionId)+'/attachments/'+encodeURIComponent(id);}
58
+ function attachmentGallery(items,sessionId){if(!items||!items.length)return'';return '<div class="image-gallery">'+items.map(function(item){var url=attachmentUrl(sessionId,item.attachmentId);return '<a class="image-card" href="'+escapeHtml(url)+'" target="_blank"><img src="'+escapeHtml(url)+'" alt="'+escapeHtml(item.originalName)+'"><span>'+escapeHtml(item.originalName)+'</span></a>';}).join('')+'</div>';}
59
+ function artifactFromOutput(output){try{var value=typeof output==='string'?JSON.parse(output):output;return value&&typeof value.path==='string'&&String(value.mimeType||'').indexOf('image/')===0?value:null;}catch(error){return null;}}
60
+ function artifactGallery(toolCalls,sessionId){var items=(toolCalls||[]).filter(function(call){return call.name==='present_file';}).map(function(call){return artifactFromOutput(call.output);}).filter(Boolean);if(!items.length)return'';return '<div class="image-gallery">'+items.map(function(item){var url='/api/sessions/'+encodeURIComponent(sessionId)+'/artifact?path='+encodeURIComponent(item.path);return '<a class="image-card" href="'+escapeHtml(url)+'" target="_blank"><img src="'+escapeHtml(url)+'" alt="'+escapeHtml(item.caption||item.name)+'"><span>'+escapeHtml(item.caption||item.name)+'</span></a>';}).join('')+'</div>';}
61
+ function withoutToolTranscript(content){var marker='\\n\\n[工具记录]\\n';var index=content.indexOf(marker);return index>=0?content.slice(0,index):content;}
62
+ function messageHtml(role,content,toolCalls){var decoded=role==='user'?decodeAttachmentMessage(content):{text:withoutToolTranscript(content),attachments:[]};return '<article class="message '+role+'"><div class="avatar">'+(role==='user'?'你':'D')+'</div><div class="bubble"><div class="role">'+(role==='user'?'YOU':'DEEPCCC')+'</div><div class="markdown">'+markdown(decoded.text)+'</div>'+attachmentGallery(decoded.attachments,state.detail.sessionId)+artifactGallery(toolCalls,state.detail.sessionId)+'</div></article>';}
63
+ function toolHtml(event){var data=event.data||{};var item=data.type==='agent'?data.data:data;var artifact=item.type==='tool_result'&&item.name==='present_file'?artifactFromOutput(item.content):null;if(artifact)return artifactGallery([{name:'present_file',output:JSON.stringify(artifact)}],state.detail.sessionId);return '<details class="tool-card"><summary>'+escapeHtml(item.type==='tool_use'?'调用工具 · '+(item.name||'tool'):'工具结果 · '+(item.name||item.tool_use_id||'tool'))+'</summary><pre>'+escapeHtml(JSON.stringify(item.type==='tool_use'?item.input:item.content,null,2))+'</pre></details>';}
57
64
  function approvalAnswerLabel(answer){var labels={allow:'已允许一次','allow-session':'本会话已允许','allow-always':'已永久允许',deny:'已拒绝'};return labels[answer]||'等待决定';}
58
65
  function approvalHtml(approval){var resolved=approval.status==='resolved';var html='<section class="approval-card'+(resolved?' resolved':'')+'"><div class="approval-head"><strong>⚠ 操作审批</strong><span class="approval-status">'+escapeHtml(resolved?approvalAnswerLabel(approval.answer):'等待批准')+'</span></div><div class="approval-detail">'+escapeHtml(approval.detail||('工具:'+approval.tool))+'</div><pre class="approval-command">'+escapeHtml(approval.action)+'</pre>';if(!resolved)html+='<div class="approval-inline-actions"><button class="danger" data-approval-answer="deny" data-approval-id="'+escapeHtml(approval.approvalId)+'">拒绝</button><button class="ghost" data-approval-answer="allow" data-approval-id="'+escapeHtml(approval.approvalId)+'">允许一次</button><button class="ghost" data-approval-answer="allow-session" data-approval-id="'+escapeHtml(approval.approvalId)+'">本会话允许</button><button class="primary" data-approval-answer="allow-always" data-approval-id="'+escapeHtml(approval.approvalId)+'">永久允许</button></div>';return html+'</section>';}
59
66
  function sessionDraft(detail){return state.configDrafts[detail.sessionId]||{model:detail.model||'',subModel:detail.subModel||'',effort:detail.effort||''};}
60
67
  function rememberDraft(){if(!state.selectedId)return;state.configDrafts[state.selectedId]={model:$('session-model').value,subModel:$('session-sub-model').value,effort:$('session-effort').value};}
61
68
  function rememberPromptDraft(){if(!state.selectedId)return;state.promptDrafts[state.selectedId]=$('prompt').value;}
69
+ function currentAttachments(){return state.attachmentDrafts[state.selectedId]||[];}
70
+ function renderAttachmentDrafts(){var root=$('attachment-drafts');if(!root)return;root.innerHTML=currentAttachments().map(function(item){return '<div class="attachment-draft"><img src="'+escapeHtml(attachmentUrl(state.selectedId,item.attachmentId))+'" alt="'+escapeHtml(item.originalName)+'"><button type="button" data-remove-attachment="'+escapeHtml(item.attachmentId)+'">×</button></div>';}).join('');}
62
71
  function clearSelection(message){if(state.eventSource)state.eventSource.close();state.eventSource=null;state.detailRequestToken++;state.selectedId=null;state.detail=null;$('composer-shell').classList.add('hidden');$('rename-session').classList.add('hidden');$('delete-session').classList.add('hidden');$('warning').classList.add('hidden');$('session-title').textContent='DeepCCC';$('session-cwd').textContent='选择或创建一个会话';$('content').innerHTML='<div class="welcome"><div class="welcome-card"><div class="welcome-mark">✦</div><h2>'+escapeHtml(message||'选择一个会话')+'</h2><p>从左侧选择其他会话或创建一个新的任务。</p></div></div>';}
63
- function renderDetail(){var detail=state.detail;if(!detail)return;var draft=sessionDraft(detail);$('session-title').textContent=detail.title;$('session-cwd').textContent=detail.cwd+' · '+statusText(detail.status);$('rename-session').classList.remove('hidden');$('delete-session').classList.remove('hidden');$('composer-shell').classList.remove('hidden');$('prompt').value=state.promptDrafts[detail.sessionId]||'';$('session-model').value=draft.model;$('session-sub-model').value=draft.subModel;$('session-effort').value=draft.effort;var html='<div class="thread">';if(!(detail.messages||[]).length&&detail.status!=='running')html+='<div class="empty-thread"><strong>准备开始这个会话</strong><span>在下方输入任务。DeepCCC 会读取当前项目指令、调用工具并持续保存上下文。</span></div>';(detail.messages||[]).forEach(function(message){html+=messageHtml(message.role,message.content);});if(detail.status==='running'){var live=(detail.events||[]).slice().reverse().find(function(event){return event.type==='agent'&&event.data&&event.data.type==='text';});var current=(detail.events||[]).filter(function(event){return event.type==='agent'&&event.data&&(event.data.type==='tool_use'||event.data.type==='tool_result');});current.forEach(function(event){html+=toolHtml(event);});if(live)html+=messageHtml('assistant',live.data.accumulated||live.data.text||'');}var approvals=(detail.approvals||[]).slice();if(detail.pendingApproval&&!approvals.some(function(item){return item.approvalId===detail.pendingApproval.approvalId;}))approvals.push(Object.assign({status:'pending'},detail.pendingApproval));approvals.forEach(function(approval){html+=approvalHtml(approval);});if(detail.status==='running')html+='<div class="run-state"><i class="pulse"></i>Agent 正在工作</div>';html+='</div>';$('content').innerHTML=html;$('send').classList.toggle('hidden',detail.status==='running');$('stop').classList.toggle('hidden',detail.status!=='running');$('prompt').disabled=detail.status==='running';$('session-model').disabled=detail.status==='running';$('session-sub-model').disabled=detail.status==='running';$('session-effort').disabled=detail.status==='running';renderWarning();requestAnimationFrame(function(){$('content').scrollTop=$('content').scrollHeight;});}
72
+ function renderDetail(){var detail=state.detail;if(!detail)return;var draft=sessionDraft(detail);$('session-title').textContent=detail.title;$('session-cwd').textContent=detail.cwd+' · '+statusText(detail.status);$('rename-session').classList.remove('hidden');$('delete-session').classList.remove('hidden');$('composer-shell').classList.remove('hidden');$('prompt').value=state.promptDrafts[detail.sessionId]||'';$('session-model').value=draft.model;$('session-sub-model').value=draft.subModel;$('session-effort').value=draft.effort;renderAttachmentDrafts();var html='<div class="thread">';if(!(detail.messages||[]).length&&detail.status!=='running')html+='<div class="empty-thread"><strong>准备开始这个会话</strong><span>在下方输入任务,可附加截图。DeepCCC 会读取当前项目指令、调用工具并持续保存上下文。</span></div>';(detail.messages||[]).forEach(function(message){html+=messageHtml(message.role,message.content,message.toolCalls);});if(detail.status==='running'){var live=(detail.events||[]).slice().reverse().find(function(event){return event.type==='agent'&&event.data&&event.data.type==='text';});var current=(detail.events||[]).filter(function(event){return event.type==='agent'&&event.data&&(event.data.type==='tool_use'||event.data.type==='tool_result');});current.forEach(function(event){html+=toolHtml(event);});if(live)html+=messageHtml('assistant',live.data.accumulated||live.data.text||'',[]);}var approvals=(detail.approvals||[]).slice();if(detail.pendingApproval&&!approvals.some(function(item){return item.approvalId===detail.pendingApproval.approvalId;}))approvals.push(Object.assign({status:'pending'},detail.pendingApproval));approvals.forEach(function(approval){html+=approvalHtml(approval);});if(detail.status==='running')html+='<div class="run-state"><i class="pulse"></i>Agent 正在工作</div>';html+='</div>';$('content').innerHTML=html;$('send').classList.toggle('hidden',detail.status==='running');$('stop').classList.toggle('hidden',detail.status!=='running');$('prompt').disabled=detail.status==='running';$('session-model').disabled=detail.status==='running';$('session-sub-model').disabled=detail.status==='running';$('session-effort').disabled=detail.status==='running';$('attach-image').disabled=detail.status==='running'||state.uploading;renderWarning();requestAnimationFrame(function(){$('content').scrollTop=$('content').scrollHeight;});}
64
73
  function cwdKey(value){var raw=String(value||'');var normalized=raw.replace(/\\\\/g,'/').replace(new RegExp('/+$'),'');var windowsStyle=new RegExp('^[a-z]:/','i').test(normalized)||raw.indexOf(String.fromCharCode(92))>=0;return windowsStyle?normalized.toLowerCase():normalized;}
65
74
  function renderWarning(){if(!state.detail)return;var selectedCwd=cwdKey(state.detail.cwd);var conflicts=state.sessions.filter(function(item){return item.sessionId!==state.detail.sessionId&&item.status==='running'&&cwdKey(item.cwd)===selectedCwd;});var el=$('warning');if(conflicts.length){el.textContent='⚠ 当前目录还有 '+conflicts.length+' 个 Agent 会话正在运行。多个会话直接修改同一目录,可能产生覆盖或冲突。';el.classList.remove('hidden');}else el.classList.add('hidden');}
66
75
  async function loadSessions(){var result=await api('/api/sessions');state.sessions=result.sessions||[];if(state.selectedId&&!state.sessions.some(function(item){return item.sessionId===state.selectedId;}))clearSelection('会话已在其他页面删除');renderSessions();renderWarning();}
@@ -72,12 +81,16 @@ export const DEEPCCC_WEB_PAGE = `<!DOCTYPE html>
72
81
  $('new-session').addEventListener('click',function(){openModal('new-modal');setTimeout(function(){$('new-cwd').focus();},0);});$('open-settings').addEventListener('click',function(){loadConfig().then(function(){openModal('settings-modal');}).catch(function(error){toast(error.message,true);});});document.querySelectorAll('[data-close]').forEach(function(button){button.addEventListener('click',function(){closeModal(button.dataset.close);});});
73
82
  $('new-form').addEventListener('submit',async function(event){event.preventDefault();try{var result=await api('/api/sessions',{method:'POST',body:JSON.stringify({cwd:$('new-cwd').value,title:$('new-title').value,model:$('new-model').value,subModel:$('new-sub-model').value,effort:$('new-effort').value})});closeModal('new-modal');$('new-title').value='';await loadSessions();await selectSession(result.session.sessionId);}catch(error){toast(error.message,true);}});
74
83
  $('settings-form').addEventListener('submit',async function(event){event.preventDefault();var body={provider:$('cfg-provider').value,baseURL:$('cfg-base-url').value,model:$('cfg-model').value,subModel:$('cfg-sub-model').value,effort:$('cfg-effort').value,contextWindow:Number($('cfg-context').value),streaming:$('cfg-streaming').checked,web:{port:Number($('cfg-port').value),openOnStart:$('cfg-open').checked}};if($('cfg-max-output').value)body.maxOutputTokens=Number($('cfg-max-output').value);if($('cfg-api-key').value)body.apiKey=$('cfg-api-key').value;try{await api('/api/config',{method:'PUT',body:JSON.stringify(body)});$('cfg-api-key').value='';closeModal('settings-modal');toast('设置已保存,新配置从下一次新建会话生效');await loadConfig();}catch(error){toast(error.message,true);}});
75
- async function send(){var sessionId=state.selectedId;var text=$('prompt').value;if(!sessionId||!text.trim())return;try{var patch={model:$('session-model').value,subModel:$('session-sub-model').value,effort:$('session-effort').value};if(patch.model!==state.detail.model||patch.subModel!==state.detail.subModel||patch.effort!==state.detail.effort){var updated=await api('/api/sessions/'+encodeURIComponent(sessionId),{method:'PATCH',body:JSON.stringify(patch)});if(state.selectedId===sessionId){state.detail=Object.assign({},state.detail,updated.session);renderDetail();}await loadSessions();}await api('/api/sessions/'+encodeURIComponent(sessionId)+'/messages',{method:'POST',body:JSON.stringify({text:text})});delete state.configDrafts[sessionId];delete state.promptDrafts[sessionId];if(state.selectedId===sessionId)$('prompt').value='';var token=++state.detailRequestToken;await loadDetail(sessionId,token);}catch(error){toast(error.message,true);}}
84
+ async function uploadAttachment(sessionId,file){var response=await fetch('/api/sessions/'+encodeURIComponent(sessionId)+'/attachments?name='+encodeURIComponent(file.name||'image'),{method:'POST',headers:{'content-type':file.type||'application/octet-stream'},body:file});var result=await response.json().catch(function(){return{error:'上传响应不是有效 JSON'};});if(!response.ok)throw new Error(result.error||('HTTP '+response.status));return result.attachment;}
85
+ async function handleAttachmentFiles(files){var sessionId=state.selectedId;if(!sessionId||state.uploading)return;var candidates=[].slice.call(files||[]).filter(function(file){return ['image/png','image/jpeg','image/webp'].includes(file.type);});var existing=state.attachmentDrafts[sessionId]||[];if(existing.length+candidates.length>10){toast('每条消息最多添加 10 张图片',true);return;}if(!candidates.length){toast('仅支持 PNG、JPEG、WebP 图片',true);return;}state.uploading=true;if(state.detail)renderDetail();try{for(var i=0;i<candidates.length;i++){if(candidates[i].size>20*1024*1024)throw new Error('单张图片不能超过 20 MB:'+candidates[i].name);existing.push(await uploadAttachment(sessionId,candidates[i]));state.attachmentDrafts[sessionId]=existing;renderAttachmentDrafts();}}catch(error){toast(error.message,true);}finally{state.uploading=false;if(state.detail)renderDetail();$('attachment-input').value='';}}
86
+ async function removeAttachment(id){var sessionId=state.selectedId;if(!sessionId)return;try{await api('/api/sessions/'+encodeURIComponent(sessionId)+'/attachments/'+encodeURIComponent(id),{method:'DELETE'});state.attachmentDrafts[sessionId]=(state.attachmentDrafts[sessionId]||[]).filter(function(item){return item.attachmentId!==id;});renderAttachmentDrafts();}catch(error){toast(error.message,true);}}
87
+ async function send(){var sessionId=state.selectedId;var text=$('prompt').value;var attachments=currentAttachments();if(!sessionId||(!text.trim()&&!attachments.length)||state.uploading)return;try{var patch={model:$('session-model').value,subModel:$('session-sub-model').value,effort:$('session-effort').value};if(patch.model!==state.detail.model||patch.subModel!==state.detail.subModel||patch.effort!==state.detail.effort){var updated=await api('/api/sessions/'+encodeURIComponent(sessionId),{method:'PATCH',body:JSON.stringify(patch)});if(state.selectedId===sessionId){state.detail=Object.assign({},state.detail,updated.session);renderDetail();}await loadSessions();}await api('/api/sessions/'+encodeURIComponent(sessionId)+'/messages',{method:'POST',body:JSON.stringify({text:text,attachmentIds:attachments.map(function(item){return item.attachmentId;})})});delete state.configDrafts[sessionId];delete state.promptDrafts[sessionId];delete state.attachmentDrafts[sessionId];if(state.selectedId===sessionId){$('prompt').value='';renderAttachmentDrafts();}var token=++state.detailRequestToken;await loadDetail(sessionId,token);}catch(error){toast(error.message,true);}}
76
88
  ['session-model','session-sub-model','session-effort'].forEach(function(id){$(id).addEventListener('input',rememberDraft);$(id).addEventListener('change',rememberDraft);});
89
+ $('attach-image').addEventListener('click',function(){$('attachment-input').click();});$('attachment-input').addEventListener('change',function(){handleAttachmentFiles(this.files);});$('attachment-drafts').addEventListener('click',function(event){var button=event.target.closest('[data-remove-attachment]');if(button)removeAttachment(button.dataset.removeAttachment);});document.addEventListener('paste',function(event){var files=[].slice.call(event.clipboardData&&event.clipboardData.files||[]).filter(function(file){return file.type.indexOf('image/')===0;});if(files.length){event.preventDefault();handleAttachmentFiles(files);}});$('composer').addEventListener('dragover',function(event){event.preventDefault();this.classList.add('dragging');});$('composer').addEventListener('dragleave',function(){this.classList.remove('dragging');});$('composer').addEventListener('drop',function(event){event.preventDefault();this.classList.remove('dragging');handleAttachmentFiles(event.dataTransfer.files);});
77
90
  $('send').addEventListener('click',send);$('prompt').addEventListener('input',rememberPromptDraft);$('prompt').addEventListener('keydown',function(event){if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();send();}});$('stop').addEventListener('click',async function(){try{await api('/api/sessions/'+encodeURIComponent(state.selectedId)+'/stop',{method:'POST',body:'{}'});toast('已请求停止');}catch(error){toast(error.message,true);}});
78
- $('rename-session').addEventListener('click',function(){$('rename-title').value=state.detail.title;openModal('rename-modal');});$('rename-form').addEventListener('submit',async function(event){event.preventDefault();try{await api('/api/sessions/'+encodeURIComponent(state.selectedId),{method:'PATCH',body:JSON.stringify({title:$('rename-title').value})});closeModal('rename-modal');var token=++state.detailRequestToken;await loadDetail(state.selectedId,token);}catch(error){toast(error.message,true);}});$('delete-session').addEventListener('click',async function(){if(!confirm('删除这个会话及其本地历史?不会删除工作目录中的文件。'))return;try{var deletedId=state.selectedId;await api('/api/sessions/'+encodeURIComponent(deletedId),{method:'DELETE'});delete state.configDrafts[deletedId];delete state.promptDrafts[deletedId];clearSelection('会话已删除');await loadSessions();}catch(error){toast(error.message,true);}});
91
+ $('rename-session').addEventListener('click',function(){$('rename-title').value=state.detail.title;openModal('rename-modal');});$('rename-form').addEventListener('submit',async function(event){event.preventDefault();try{await api('/api/sessions/'+encodeURIComponent(state.selectedId),{method:'PATCH',body:JSON.stringify({title:$('rename-title').value})});closeModal('rename-modal');var token=++state.detailRequestToken;await loadDetail(state.selectedId,token);}catch(error){toast(error.message,true);}});$('delete-session').addEventListener('click',async function(){if(!confirm('删除这个会话及其本地历史?不会删除工作目录中的文件。'))return;try{var deletedId=state.selectedId;await api('/api/sessions/'+encodeURIComponent(deletedId),{method:'DELETE'});delete state.configDrafts[deletedId];delete state.promptDrafts[deletedId];delete state.attachmentDrafts[deletedId];clearSelection('会话已删除');await loadSessions();}catch(error){toast(error.message,true);}});
79
92
  $('content').addEventListener('click',async function(event){var button=event.target.closest('[data-approval-answer]');if(!button)return;button.disabled=true;try{await api('/api/approvals/'+encodeURIComponent(button.dataset.approvalId),{method:'POST',body:JSON.stringify({answer:button.dataset.approvalAnswer})});var token=++state.detailRequestToken;await loadDetail(state.selectedId,token);}catch(error){button.disabled=false;toast(error.message,true);}});
80
- function handleGlobalEvent(event){try{var update=JSON.parse(event.data);if(update.type==='session_deleted'){delete state.configDrafts[update.sessionId];delete state.promptDrafts[update.sessionId];}}catch(error){}scheduleSessions();}
93
+ function handleGlobalEvent(event){try{var update=JSON.parse(event.data);if(update.type==='session_deleted'){delete state.configDrafts[update.sessionId];delete state.promptDrafts[update.sessionId];delete state.attachmentDrafts[update.sessionId];}}catch(error){}scheduleSessions();}
81
94
  state.globalEventSource=new EventSource('/api/events');state.globalEventSource.onmessage=handleGlobalEvent;
82
95
  Promise.all([loadSessions(),loadConfig()]).then(function(){if(state.sessions[0])return selectSession(state.sessions[0].sessionId);}).catch(function(error){toast(error.message,true);});
83
96
  })();
@@ -1,3 +1,4 @@
1
+ import { AttachmentStore, type AttachmentMeta, type SaveAttachmentInput } from "./attachments.js";
1
2
  import { type ChatEvent, type ChatSessionConfig } from "./index.js";
2
3
  import type { PermissionAnswer, PermissionRequest, PermissionResolver } from "./permissions.js";
3
4
  import { WebSessionStore, type CreateWebSessionInput, type UpdateWebSessionInput, type WebSessionMeta } from "./web-session-store.js";
@@ -33,6 +34,7 @@ export interface WebSessionAgent {
33
34
  }
34
35
  export interface DeepCccWebRuntimeOptions {
35
36
  store?: WebSessionStore;
37
+ attachmentStore?: AttachmentStore;
36
38
  loadConfig: () => WebRuntimeConfig;
37
39
  sessionFactory?: (input: WebSessionFactoryInput) => WebSessionAgent;
38
40
  approvalTimeoutMs?: number;
@@ -41,6 +43,7 @@ export interface DeepCccWebRuntimeOptions {
41
43
  }
42
44
  export declare class DeepCccWebRuntime {
43
45
  readonly store: WebSessionStore;
46
+ readonly attachmentStore: AttachmentStore;
44
47
  private readonly loadConfig;
45
48
  private readonly sessionFactory;
46
49
  private readonly approvalTimeoutMs;
@@ -90,10 +93,22 @@ export declare class DeepCccWebRuntime {
90
93
  }>;
91
94
  updateSession(sessionId: string, patch: UpdateWebSessionInput): Promise<WebSessionMeta>;
92
95
  deleteSession(sessionId: string): Promise<boolean>;
93
- sendMessage(sessionId: string, text: string): Promise<{
96
+ sendMessage(sessionId: string, text: string, attachmentIds?: string[]): Promise<{
94
97
  runId: string;
95
98
  }>;
96
99
  stopSession(sessionId: string): Promise<boolean>;
100
+ addAttachment(sessionId: string, input: SaveAttachmentInput): Promise<AttachmentMeta>;
101
+ readAttachment(sessionId: string, attachmentId: string): Promise<{
102
+ attachment: AttachmentMeta;
103
+ bytes: Buffer;
104
+ } | null>;
105
+ deleteAttachment(sessionId: string, attachmentId: string): Promise<boolean>;
106
+ readArtifact(sessionId: string, value: string): Promise<{
107
+ path: string;
108
+ mimeType: "image/png" | "image/jpeg" | "image/webp";
109
+ size: number;
110
+ bytes: NonSharedBuffer;
111
+ } | null>;
97
112
  waitForIdle(sessionId: string): Promise<void>;
98
113
  resolveApproval(approvalId: string, answer: PermissionAnswer): Promise<boolean>;
99
114
  subscribe(sessionId: string, listener: (event: WebRuntimeEvent) => void): () => void;
@@ -1,9 +1,13 @@
1
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";
2
5
  import { ChatSession } from "./index.js";
3
- import { readBuiltinContextState } from "./context.js";
6
+ import { DEFAULT_BUILTIN_CONTEXT_DIR, normalizeBuiltinSessionId, readBuiltinContextState } from "./context.js";
4
7
  import { WebSessionStore, } from "./web-session-store.js";
5
8
  export class DeepCccWebRuntime {
6
9
  store;
10
+ attachmentStore;
7
11
  loadConfig;
8
12
  sessionFactory;
9
13
  approvalTimeoutMs;
@@ -20,6 +24,9 @@ export class DeepCccWebRuntime {
20
24
  nextEventId = 1;
21
25
  constructor(options) {
22
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") });
23
30
  this.loadConfig = options.loadConfig;
24
31
  this.approvalTimeoutMs = options.approvalTimeoutMs ?? 5 * 60_000;
25
32
  this.now = options.now ?? (() => new Date());
@@ -96,6 +103,7 @@ export class DeepCccWebRuntime {
96
103
  this.agents.delete(sessionId);
97
104
  const deleted = await this.store.delete(sessionId);
98
105
  if (deleted) {
106
+ await this.attachmentStore.deleteSession(sessionId);
99
107
  this.emit(sessionId, "session_deleted", { sessionId });
100
108
  this.events.delete(sessionId);
101
109
  }
@@ -105,9 +113,13 @@ export class DeepCccWebRuntime {
105
113
  this.sessionMutations.delete(sessionId);
106
114
  }
107
115
  }
108
- async sendMessage(sessionId, text) {
109
- const prompt = text.trim();
110
- if (!prompt)
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)
111
123
  throw new Error("Message must not be empty");
112
124
  const controller = new AbortController();
113
125
  if (this.activeRuns.has(sessionId) || this.startingRuns.has(sessionId) || this.sessionMutations.has(sessionId)) {
@@ -116,14 +128,23 @@ export class DeepCccWebRuntime {
116
128
  this.startingRuns.set(sessionId, controller);
117
129
  try {
118
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);
119
139
  if (meta.title === "新会话" || meta.title === "New session" || meta.title === meta.cwd.split(/[\\/]/).at(-1)) {
120
- meta = await this.store.update(sessionId, { title: prompt.replace(/\s+/g, " ").slice(0, 42) });
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) });
121
142
  }
122
143
  if (controller.signal.aborted)
123
144
  throw new DOMException("The session start was stopped", "AbortError");
124
145
  const runId = `run-${this.idFactory()}`;
125
146
  this.events.set(sessionId, []);
126
- this.emit(sessionId, "user", { text: prompt });
147
+ this.emit(sessionId, "user", { text: displayPrompt || "请分析这些图片。", attachments });
127
148
  const promise = Promise.resolve()
128
149
  .then(() => this.run(meta, prompt, runId, controller.signal))
129
150
  .finally(() => {
@@ -153,6 +174,40 @@ export class DeepCccWebRuntime {
153
174
  await Promise.all(pending.map(([approvalId, waiter]) => this.finishApproval(approvalId, waiter, "deny", { stopped: true }).catch(() => { })));
154
175
  return true;
155
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
+ }
156
211
  async waitForIdle(sessionId) {
157
212
  await this.activeRuns.get(sessionId)?.promise;
158
213
  }
@@ -270,3 +325,7 @@ export class DeepCccWebRuntime {
270
325
  return meta;
271
326
  }
272
327
  }
328
+ function isPathInside(root, candidate) {
329
+ const rel = relative(resolve(root), resolve(candidate));
330
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
331
+ }
@@ -19,7 +19,7 @@ export interface PublicDeepCccConfig {
19
19
  defaultCwd?: string;
20
20
  }
21
21
  export interface DeepCccWebRequestHandlerOptions {
22
- runtime: Pick<DeepCccWebRuntime, "listSessions" | "createSession" | "getSession" | "updateSession" | "deleteSession" | "sendMessage" | "stopSession" | "resolveApproval" | "subscribe" | "subscribeAll">;
22
+ runtime: Pick<DeepCccWebRuntime, "listSessions" | "createSession" | "getSession" | "updateSession" | "deleteSession" | "sendMessage" | "stopSession" | "resolveApproval" | "subscribe" | "subscribeAll" | "addAttachment" | "readAttachment" | "deleteAttachment" | "readArtifact">;
23
23
  getPublicConfig: () => PublicDeepCccConfig;
24
24
  saveConfig: (patch: DeepCccConfigPatch) => Promise<PublicDeepCccConfig> | PublicDeepCccConfig;
25
25
  defaultCwd: string;
@@ -6,6 +6,7 @@ import { stat } from "node:fs/promises";
6
6
  import { createRequire } from "node:module";
7
7
  import { dirname, join } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
+ import { MAX_ATTACHMENT_BYTES } from "./attachments.js";
9
10
  import { DEEPCCC_HOME, loadConfig, saveConfigPatch, } from "./config.js";
10
11
  import { killProcessTree } from "./proc-tree-kill.js";
11
12
  import { DeepCccWebRuntime } from "./web-runtime.js";
@@ -94,6 +95,37 @@ export function createDeepCccWebRequestHandler(options) {
94
95
  const session = await options.runtime.createSession({ ...body, cwd });
95
96
  return jsonReply(res, 201, { ok: true, session });
96
97
  }
98
+ const attachmentMatch = path.match(/^\/api\/sessions\/([^/]+)\/attachments(?:\/([^/]+))?$/);
99
+ if (attachmentMatch) {
100
+ const sessionId = decodeURIComponent(attachmentMatch[1]);
101
+ const attachmentId = attachmentMatch[2] ? decodeURIComponent(attachmentMatch[2]) : "";
102
+ if (method === "POST" && !attachmentId) {
103
+ const originalName = url.searchParams.get("name")?.trim() || "image";
104
+ const bytes = await readBinary(req, MAX_ATTACHMENT_BYTES);
105
+ const attachment = await options.runtime.addAttachment(sessionId, { originalName, bytes });
106
+ return jsonReply(res, 201, { ok: true, attachment });
107
+ }
108
+ if (method === "GET" && attachmentId) {
109
+ const stored = await options.runtime.readAttachment(sessionId, attachmentId);
110
+ if (!stored)
111
+ return jsonReply(res, 404, { ok: false, error: "Image attachment not found" });
112
+ return binaryReply(res, 200, stored.bytes, stored.attachment.mimeType, stored.attachment.originalName);
113
+ }
114
+ if (method === "DELETE" && attachmentId) {
115
+ const deleted = await options.runtime.deleteAttachment(sessionId, attachmentId);
116
+ return jsonReply(res, deleted ? 200 : 404, { ok: deleted });
117
+ }
118
+ }
119
+ const artifactMatch = path.match(/^\/api\/sessions\/([^/]+)\/artifact$/);
120
+ if (artifactMatch && method === "GET") {
121
+ const artifactPath = url.searchParams.get("path") ?? "";
122
+ if (!artifactPath)
123
+ throw new WebHttpError(400, "Artifact path is required");
124
+ const artifact = await options.runtime.readArtifact(decodeURIComponent(artifactMatch[1]), artifactPath);
125
+ if (!artifact)
126
+ return jsonReply(res, 404, { ok: false, error: "Artifact image not found" });
127
+ return binaryReply(res, 200, artifact.bytes, artifact.mimeType, artifact.path.split(/[\\/]/).at(-1) ?? "image");
128
+ }
97
129
  const sessionMatch = path.match(/^\/api\/sessions\/([^/]+)$/);
98
130
  if (sessionMatch && method === "GET") {
99
131
  return jsonReply(res, 200, { ok: true, session: await options.runtime.getSession(decodeURIComponent(sessionMatch[1])) });
@@ -115,7 +147,10 @@ export function createDeepCccWebRequestHandler(options) {
115
147
  const messagesMatch = path.match(/^\/api\/sessions\/([^/]+)\/messages$/);
116
148
  if (messagesMatch && method === "POST") {
117
149
  const body = await readJson(req);
118
- const result = await options.runtime.sendMessage(decodeURIComponent(messagesMatch[1]), body.text ?? "");
150
+ const attachmentIds = Array.isArray(body.attachmentIds)
151
+ ? body.attachmentIds.filter((value) => typeof value === "string")
152
+ : [];
153
+ const result = await options.runtime.sendMessage(decodeURIComponent(messagesMatch[1]), body.text ?? "", attachmentIds);
119
154
  return jsonReply(res, 202, { ok: true, ...result });
120
155
  }
121
156
  const stopMatch = path.match(/^\/api\/sessions\/([^/]+)\/stop$/);
@@ -301,6 +336,20 @@ async function readJson(req) {
301
336
  throw new WebHttpError(400, "Invalid JSON body");
302
337
  }
303
338
  }
339
+ async function readBinary(req, maxBytes) {
340
+ const chunks = [];
341
+ let bytes = 0;
342
+ for await (const chunk of req) {
343
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
344
+ bytes += buffer.length;
345
+ if (bytes > maxBytes)
346
+ throw new WebHttpError(413, "Image attachment exceeds the 20 MB limit");
347
+ chunks.push(buffer);
348
+ }
349
+ if (!chunks.length)
350
+ throw new WebHttpError(400, "Image attachment must not be empty");
351
+ return Buffer.concat(chunks);
352
+ }
304
353
  async function assertDirectory(path) {
305
354
  try {
306
355
  if (!(await stat(path)).isDirectory())
@@ -324,6 +373,16 @@ function textReply(res, status, value, contentType) {
324
373
  res.writeHead(status, { "content-type": contentType, "cache-control": "no-store" });
325
374
  res.end(value);
326
375
  }
376
+ function binaryReply(res, status, value, contentType, fileName) {
377
+ res.writeHead(status, {
378
+ "content-type": contentType,
379
+ "content-length": String(value.byteLength),
380
+ "content-disposition": `inline; filename*=UTF-8''${encodeURIComponent(fileName)}`,
381
+ "cache-control": "no-store",
382
+ "x-content-type-options": "nosniff",
383
+ });
384
+ res.end(value);
385
+ }
327
386
  class WebHttpError extends Error {
328
387
  status;
329
388
  constructor(status, message) {
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "A lightweight coding agent with OpenAI-compatible and Anthropic Messages API support.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [