chatccc 0.2.265 → 0.2.267
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 +8 -3
- package/config.sample.json +1 -0
- package/deepccc-agent/README.md +12 -0
- package/dist/deepccc-agent/src/config.js +16 -0
- package/dist/deepccc-agent/src/file-tools.js +11 -4
- package/dist/deepccc-agent/src/index.js +6 -0
- package/dist/src/adapters/ccc-adapter.js +1 -0
- package/dist/src/agent-team/application/main-agent-service.js +9 -2
- package/dist/src/agent-team/application/task-execution-service.js +206 -0
- package/dist/src/agent-team/domain/task-run.js +29 -0
- package/dist/src/agent-team/http/board-routes.js +36 -0
- package/dist/src/agent-team/infrastructure/task-execution-runtime.js +21 -0
- package/dist/src/agent-team/main-agent-bootstrap.js +19 -4
- package/dist/src/agent-team/repositories/feishu-p2p-contact-store.js +13 -2
- package/dist/src/agent-team/repositories/json-task-run-repository.js +98 -0
- package/dist/src/agent-team/web/agent-team-page.js +30 -19
- package/dist/src/config.js +2 -0
- package/dist/src/orchestrator.js +11 -3
- package/dist/src/session.js +1 -0
- package/dist/src/web-ui.js +32 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -155,9 +155,11 @@ chatccc
|
|
|
155
155
|
|
|
156
156
|
#### 本地任务看板(Agent Team)
|
|
157
157
|
|
|
158
|
-
Agent Team 提供本地任务看板和项目主 Agent
|
|
158
|
+
Agent Team 提供本地任务看板和项目主 Agent 入口。每个规范化后的本机目录对应一个独立项目和看板,固定包含“头脑风暴、Todo、Doing、Done、搁置”五列,支持新增、单击卡片编辑、拖动排序和删除任务。页面会自动保存,不需要手动提交。当前按路径识别项目,不检查 Git 仓库或 worktree 关系,任意子目录都可以作为独立项目。
|
|
159
159
|
|
|
160
|
-
打开项目后可以选择 CCC、Claude、Cursor 或 Codex 作为主 Agent。首次设置会创建固定命名为 `主Agent-<目录短名>` 的飞书群和空 Agent Session;后续切换 Agent 或重新关联目录会复用原群,运行中的主 Agent 不允许切换。建群成员取自机器人最近一次收到的飞书私聊;如果尚无私聊记录,网页会提示先给机器人发送任意私聊消息并自动检测。项目群名不会随第一句话或 `/forget` 改变。
|
|
160
|
+
打开项目后可以选择 CCC、Claude、Cursor 或 Codex 作为主 Agent。首次设置会创建固定命名为 `主Agent-<目录短名>` 的飞书群和空 Agent Session;后续切换 Agent 或重新关联目录会复用原群,运行中的主 Agent 不允许切换。建群成员取自机器人最近一次收到的飞书私聊;如果尚无私聊记录,网页会提示先给机器人发送任意私聊消息并自动检测。项目群名不会随第一句话或 `/forget` 改变。
|
|
161
|
+
|
|
162
|
+
任务卡片可通过“交给主 Agent”启动真实执行:任务会从 Todo 移到 Doing,成功后自动移到 Done,失败则保留在 Doing 并显示错误,支持停止与重试。同一项目同一时间只运行一个看板任务;运行记录持久化在本地 JSON 中,服务异常退出后会把未完成执行标记为中断,避免错误显示为仍在运行。
|
|
161
163
|
|
|
162
164
|
看板数据默认保存在 `~/.chatccc/agent-team/`,其中 `workspaces.json` 保存最近工作目录索引,`boards/` 保存按稳定 ID 分隔的看板 JSON,`main-agent-bindings/` 单独保存本机飞书群与 Session 绑定。目录移动或重命名后,可从最近目录列表重新关联。数据访问通过仓储接口隔离,后续接入飞书多维表格时可增加双向同步适配器,无需把本机群聊和 Session 状态混入任务同步模型。选择目录、创建群聊和启动 Agent 等操作都由本机 Node 后端执行;当前实现不依赖 Electron。
|
|
163
165
|
|
|
@@ -226,6 +228,8 @@ ChatCCC 会把 `ccc.DEEPSEEK_API_KEY`、`ccc.DEEPSEEK_BASE_URL`、模型和 effo
|
|
|
226
228
|
|
|
227
229
|
**协议 override:** 也可以在 ChatCCC 的配置(`config.json` 的 `ccc.provider`,或 Web 管理页的「CCC Agent → API 协议」)显式指定 `openai` / `anthropic` 覆盖内核配置;留空(默认)时跟随 `~/.deepccc/config.json` / `DEEPCCC_PROVIDER`。`streaming` 不做 override,始终由 DeepCCC 内核配置控制。
|
|
228
230
|
|
|
231
|
+
**Git 共同作者:** DeepCCC 默认给它创建的提交追加 `Co-authored-by: DeepCCC <20184052+wzj998@users.noreply.github.com>`,保留用户为主 Author。全局开关是 `~/.deepccc/config.json` 的 `git.coAuthor.enabled`(缺省 `true`);ChatCCC 的 `ccc.gitCoAuthor` 为三态 override:`null`/缺失跟随全局,`true` 强制开启,`false` 强制关闭。Web 管理页的「CCC Agent → Git 提交共同作者」可设置同样的三种状态。
|
|
232
|
+
|
|
229
233
|
**API 支持不限于 DeepSeek。** 默认的 `provider: "openai"` 使用 OpenAI 兼容协议(`@ai-sdk/openai-compatible`),DeepSeek 只是出厂默认端点;设置 `provider: "anthropic"` 后改用 Anthropic Messages 协议(`@ai-sdk/anthropic`)。两种模式都支持流式输出,并复用相同的 API Key、Base URL 和模型配置。
|
|
230
234
|
|
|
231
235
|
| 服务 | Base URL 示例 | 说明 |
|
|
@@ -347,7 +351,8 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
347
351
|
"DEEPSEEK_API_KEY": "",
|
|
348
352
|
"DEEPSEEK_BASE_URL": "https://api.deepseek.com/v1",
|
|
349
353
|
"model": "deepseek-v4-pro",
|
|
350
|
-
"alternativeModel": ""
|
|
354
|
+
"alternativeModel": "",
|
|
355
|
+
"gitCoAuthor": null
|
|
351
356
|
}
|
|
352
357
|
}
|
|
353
358
|
```
|
package/config.sample.json
CHANGED
package/deepccc-agent/README.md
CHANGED
|
@@ -79,6 +79,13 @@ $env:DEEPCCC_STREAMING="true"
|
|
|
79
79
|
"effort": "",
|
|
80
80
|
"streaming": true,
|
|
81
81
|
"contextWindow": 1048576,
|
|
82
|
+
"git": {
|
|
83
|
+
"coAuthor": {
|
|
84
|
+
"enabled": true,
|
|
85
|
+
"name": "DeepCCC",
|
|
86
|
+
"email": "20184052+wzj998@users.noreply.github.com"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
82
89
|
"rawStreamLogs": {
|
|
83
90
|
"enabled": true,
|
|
84
91
|
"maxBytesPerTurn": 1048576,
|
|
@@ -88,6 +95,11 @@ $env:DEEPCCC_STREAMING="true"
|
|
|
88
95
|
}
|
|
89
96
|
```
|
|
90
97
|
|
|
98
|
+
`git.coAuthor.enabled` 默认开启。DeepCCC 通过 `run_command` 创建 Git 提交时会保留用户为
|
|
99
|
+
主 Author,并追加 `Co-authored-by: DeepCCC <20184052+wzj998@users.noreply.github.com>`。
|
|
100
|
+
可设为 `false` 或用 `DEEPCCC_GIT_COAUTHOR=false` 全局关闭。ChatCCC 的
|
|
101
|
+
`ccc.gitCoAuthor` 是三态 override:`null`/缺失跟随这里,`true` 强制开启,`false` 强制关闭。
|
|
102
|
+
|
|
91
103
|
`provider` 可选 `openai` 或 `anthropic`,默认 `openai`,也可以通过
|
|
92
104
|
`DEEPCCC_PROVIDER` 或命令行 `--provider` 覆盖:
|
|
93
105
|
|
|
@@ -16,6 +16,13 @@ export const DEFAULT_CONFIG = {
|
|
|
16
16
|
effort: "",
|
|
17
17
|
streaming: true,
|
|
18
18
|
contextWindow: 1_048_576,
|
|
19
|
+
git: {
|
|
20
|
+
coAuthor: {
|
|
21
|
+
enabled: true,
|
|
22
|
+
name: "DeepCCC",
|
|
23
|
+
email: "20184052+wzj998@users.noreply.github.com",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
19
26
|
rawStreamLogs: {
|
|
20
27
|
// 默认开启:压缩后可通过 session_search(include_raw_logs=true)找回原文。
|
|
21
28
|
// 如需关闭,在 ~/.deepccc/config.json 中设置 rawStreamLogs.enabled=false。
|
|
@@ -62,6 +69,8 @@ function loadConfig() {
|
|
|
62
69
|
const rawLogs = file.rawStreamLogs && typeof file.rawStreamLogs === "object"
|
|
63
70
|
? file.rawStreamLogs
|
|
64
71
|
: {};
|
|
72
|
+
const git = file.git && typeof file.git === "object" ? file.git : {};
|
|
73
|
+
const coAuthor = git.coAuthor && typeof git.coAuthor === "object" ? git.coAuthor : {};
|
|
65
74
|
return {
|
|
66
75
|
provider: normalizeDeepCccProvider(env("DEEPCCC_PROVIDER") ?? file.provider ?? DEFAULT_CONFIG.provider),
|
|
67
76
|
apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
|
|
@@ -71,6 +80,13 @@ function loadConfig() {
|
|
|
71
80
|
effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
|
|
72
81
|
streaming: boolEnv("DEEPCCC_STREAMING") ?? file.streaming ?? DEFAULT_CONFIG.streaming,
|
|
73
82
|
contextWindow: numberEnv("DEEPCCC_CONTEXT_WINDOW") ?? file.contextWindow ?? DEFAULT_CONFIG.contextWindow,
|
|
83
|
+
git: {
|
|
84
|
+
coAuthor: {
|
|
85
|
+
enabled: boolEnv("DEEPCCC_GIT_COAUTHOR") ?? coAuthor.enabled ?? DEFAULT_CONFIG.git.coAuthor.enabled,
|
|
86
|
+
name: coAuthor.name?.trim() || DEFAULT_CONFIG.git.coAuthor.name,
|
|
87
|
+
email: coAuthor.email?.trim() || DEFAULT_CONFIG.git.coAuthor.email,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
74
90
|
rawStreamLogs: {
|
|
75
91
|
enabled: boolEnv("DEEPCCC_RAW_STREAM_LOGS") ?? rawLogs.enabled ?? DEFAULT_CONFIG.rawStreamLogs.enabled,
|
|
76
92
|
maxBytesPerTurn: numberEnv("DEEPCCC_RAW_STREAM_MAX_BYTES") ?? rawLogs.maxBytesPerTurn ?? DEFAULT_CONFIG.rawStreamLogs.maxBytesPerTurn,
|
|
@@ -26,6 +26,13 @@ const MAX_COMMAND_TIMEOUT_MS = 900_000;
|
|
|
26
26
|
export const MAX_TASK_OUTPUT_CHARS = 32_000;
|
|
27
27
|
const requireFromHere = createRequire(import.meta.url);
|
|
28
28
|
const FALLBACK_SKIPPED_DIRECTORIES = new Set([".git", "node_modules"]);
|
|
29
|
+
/** Adds DeepCCC attribution to git commit commands without replacing the user's author. */
|
|
30
|
+
export function withGitCoAuthor(command, coAuthor) {
|
|
31
|
+
if (!coAuthor?.enabled || command.includes(coAuthor.email))
|
|
32
|
+
return command;
|
|
33
|
+
const trailer = ` --trailer "Co-authored-by: ${coAuthor.name} <${coAuthor.email}>"`;
|
|
34
|
+
return command.replace(/(^|(?:&&|\|\||;|\|)\s*)(git\s+commit)\b/g, (_match, prefix, gitCommit) => `${prefix}${gitCommit}${trailer}`);
|
|
35
|
+
}
|
|
29
36
|
/**
|
|
30
37
|
* 把 `~` / `~/x`(含反斜杠 `~\\x`)展开为用户主目录绝对路径。
|
|
31
38
|
* `~user/x` 形式不展开(Node 无内置支持),保持原样交给后续解析。
|
|
@@ -676,8 +683,8 @@ export async function searchCodeForTool(cwd, input, signal, runtimeOptions = {})
|
|
|
676
683
|
: fallback.truncated,
|
|
677
684
|
};
|
|
678
685
|
}
|
|
679
|
-
export async function runCommandForTool(cwd, input, abortSignal) {
|
|
680
|
-
const command = input.command?.trim();
|
|
686
|
+
export async function runCommandForTool(cwd, input, abortSignal, coAuthor) {
|
|
687
|
+
const command = withGitCoAuthor(input.command?.trim(), coAuthor);
|
|
681
688
|
if (!command)
|
|
682
689
|
throw new Error("command is required");
|
|
683
690
|
const commandCwd = resolveToolPath(cwd, input.cwd);
|
|
@@ -1016,14 +1023,14 @@ export function createBuiltinFileTools(cwd, options = {}) {
|
|
|
1016
1023
|
},
|
|
1017
1024
|
required: ["command"],
|
|
1018
1025
|
}),
|
|
1019
|
-
execute: async (input,
|
|
1026
|
+
execute: async (input, execOptions) => {
|
|
1020
1027
|
await guard({
|
|
1021
1028
|
tool: "run_command",
|
|
1022
1029
|
action: input.command,
|
|
1023
1030
|
reason: isDangerousCommand(input.command) ? "high-risk" : "rule",
|
|
1024
1031
|
detail: `运行命令: ${input.command}`,
|
|
1025
1032
|
});
|
|
1026
|
-
return runCommandForTool(cwd, input, options.
|
|
1033
|
+
return runCommandForTool(cwd, input, execOptions.abortSignal, options.gitCoAuthor);
|
|
1027
1034
|
},
|
|
1028
1035
|
}),
|
|
1029
1036
|
task: tool({
|
|
@@ -242,6 +242,7 @@ export class ChatSession {
|
|
|
242
242
|
permissionMode;
|
|
243
243
|
permissionResolver;
|
|
244
244
|
permissionGate;
|
|
245
|
+
gitCoAuthorEnabled;
|
|
245
246
|
skillDirs;
|
|
246
247
|
customSystemPrompt;
|
|
247
248
|
/** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
|
|
@@ -325,6 +326,7 @@ export class ChatSession {
|
|
|
325
326
|
this.customSystemPrompt = options.systemPrompt ?? "";
|
|
326
327
|
this.permissionMode = options.permissionMode ?? "ask";
|
|
327
328
|
this.permissionResolver = options.permissionResolver;
|
|
329
|
+
this.gitCoAuthorEnabled = options.gitCoAuthor ?? appConfig.git.coAuthor.enabled;
|
|
328
330
|
// 技能目录在构造时确定;技能内容在每次 chat() 前重新扫描(mtime 热加载),
|
|
329
331
|
// 因此创建/修改技能后下一次对话自动生效,无需重启。
|
|
330
332
|
this.skillDirs =
|
|
@@ -429,6 +431,10 @@ export class ChatSession {
|
|
|
429
431
|
tools: createBuiltinFileTools(this.cwd, {
|
|
430
432
|
permissionGate: this.permissionGate,
|
|
431
433
|
runTask: this.runTask,
|
|
434
|
+
gitCoAuthor: {
|
|
435
|
+
...appConfig.git.coAuthor,
|
|
436
|
+
enabled: this.gitCoAuthorEnabled,
|
|
437
|
+
},
|
|
432
438
|
}),
|
|
433
439
|
stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
|
|
434
440
|
abortSignal: signal,
|
|
@@ -16,6 +16,7 @@ function toChatSessionOptions(sessionId, cwd, options) {
|
|
|
16
16
|
// chatccc 无终端可交互,且对齐 claude/codex 适配器的 bypass 模式:
|
|
17
17
|
// 高危命令不询问,全部放行(与独立 deepccc CLI 的 ask 模式不同)
|
|
18
18
|
permissionMode: "bypass",
|
|
19
|
+
...(options.gitCoAuthor !== undefined ? { gitCoAuthor: options.gitCoAuthor } : {}),
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
export function createCccAdapter(options = {}) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAgentTool } from "../../agent-tool.js";
|
|
2
2
|
import { sessionChatName } from "../../session-name.js";
|
|
3
|
+
import { isValidFeishuOpenId, } from "../repositories/feishu-p2p-contact-store.js";
|
|
3
4
|
import { BoardStoreError } from "../repositories/board-repository.js";
|
|
4
5
|
export class MainAgentService {
|
|
5
6
|
options;
|
|
@@ -56,9 +57,15 @@ export class MainAgentService {
|
|
|
56
57
|
: await this.options.boardService.setPrimaryAgent(board.boardId, agentId, board.revision);
|
|
57
58
|
return { board: savedBoard, binding: existing };
|
|
58
59
|
}
|
|
59
|
-
const
|
|
60
|
+
const latestContact = await this.options.contactStore.get();
|
|
61
|
+
const existingOwner = isValidFeishuOpenId(existing?.ownerOpenId)
|
|
60
62
|
? { openId: existing.ownerOpenId }
|
|
61
|
-
:
|
|
63
|
+
: null;
|
|
64
|
+
// A failed/provisioning binding may contain a stale simulator identity. On retry,
|
|
65
|
+
// prefer the most recent real private-message sender; ready bindings keep ownership stable.
|
|
66
|
+
const contact = existing?.status === "ready"
|
|
67
|
+
? existingOwner ?? latestContact
|
|
68
|
+
: latestContact ?? existingOwner;
|
|
62
69
|
if (!contact) {
|
|
63
70
|
throw new BoardStoreError("feishu_dm_required", "请先给飞书机器人私聊发送任意消息,再回到网页重试", 409);
|
|
64
71
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isActiveTaskRun } from "../domain/task-run.js";
|
|
3
|
+
import { BoardStoreError } from "../repositories/board-repository.js";
|
|
4
|
+
export class TaskExecutionService {
|
|
5
|
+
options;
|
|
6
|
+
now;
|
|
7
|
+
idFactory;
|
|
8
|
+
projectOperations = new Map();
|
|
9
|
+
executions = new Map();
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
this.now = options.now ?? (() => new Date());
|
|
13
|
+
this.idFactory = options.idFactory ?? randomUUID;
|
|
14
|
+
}
|
|
15
|
+
listRuns(projectId) {
|
|
16
|
+
return this.options.repository.listByProject(projectId);
|
|
17
|
+
}
|
|
18
|
+
async startTask(projectId, taskId, expectedRevision) {
|
|
19
|
+
return this.exclusive(projectId, async () => {
|
|
20
|
+
let board = await this.options.boardService.getBoard(projectId);
|
|
21
|
+
const active = (await this.options.repository.listByProject(projectId)).find(isActiveTaskRun);
|
|
22
|
+
if (active) {
|
|
23
|
+
if (active.taskId === taskId)
|
|
24
|
+
return { board, run: active };
|
|
25
|
+
throw new BoardStoreError("task_run_busy", "该项目已有任务正在由主 Agent 执行", 409);
|
|
26
|
+
}
|
|
27
|
+
if (board.revision !== expectedRevision) {
|
|
28
|
+
throw new BoardStoreError("revision_conflict", `Board changed in another page (expected revision ${expectedRevision}, current ${board.revision})`, 409);
|
|
29
|
+
}
|
|
30
|
+
const task = board.tasks.find((candidate) => candidate.id === taskId && !candidate.deletedAt);
|
|
31
|
+
if (!task)
|
|
32
|
+
throw new BoardStoreError("not_found", `Task not found: ${taskId}`, 404);
|
|
33
|
+
const binding = await this.options.getBinding(projectId);
|
|
34
|
+
if (!binding || binding.status !== "ready" || !binding.chatId || !binding.sessionId) {
|
|
35
|
+
throw new BoardStoreError("main_agent_unavailable", "请先为项目设置可用的主 Agent", 409);
|
|
36
|
+
}
|
|
37
|
+
if (this.options.runtime.isSessionRunning(binding.sessionId)) {
|
|
38
|
+
throw new BoardStoreError("task_run_busy", "主 Agent 正在处理其他消息,请稍后再试", 409);
|
|
39
|
+
}
|
|
40
|
+
const priorRuns = await this.options.repository.listByProject(projectId);
|
|
41
|
+
const timestamp = this.now().toISOString();
|
|
42
|
+
let run = {
|
|
43
|
+
schemaVersion: 1,
|
|
44
|
+
runId: this.idFactory(),
|
|
45
|
+
projectId,
|
|
46
|
+
taskId,
|
|
47
|
+
taskTitle: task.title,
|
|
48
|
+
taskDescription: task.description,
|
|
49
|
+
attempt: priorRuns.filter((candidate) => candidate.taskId === taskId).length + 1,
|
|
50
|
+
state: "queued",
|
|
51
|
+
agentId: binding.agentId,
|
|
52
|
+
chatId: binding.chatId,
|
|
53
|
+
sessionId: binding.sessionId,
|
|
54
|
+
createdAt: timestamp,
|
|
55
|
+
updatedAt: timestamp,
|
|
56
|
+
};
|
|
57
|
+
await this.options.repository.save(run);
|
|
58
|
+
try {
|
|
59
|
+
if (task.columnId !== "doing") {
|
|
60
|
+
const doingCount = board.tasks.filter((candidate) => !candidate.deletedAt && candidate.columnId === "doing").length;
|
|
61
|
+
board = await this.options.boardService.moveTask(projectId, taskId, {
|
|
62
|
+
expectedRevision: board.revision,
|
|
63
|
+
columnId: "doing",
|
|
64
|
+
index: doingCount,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
run = await this.finishRun(run, "failed", { error: err.message });
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
const startedAt = this.now().toISOString();
|
|
73
|
+
run = { ...run, state: "running", startedAt, updatedAt: startedAt };
|
|
74
|
+
await this.options.repository.save(run);
|
|
75
|
+
const execution = this.execute(run).finally(() => {
|
|
76
|
+
this.executions.delete(run.runId);
|
|
77
|
+
});
|
|
78
|
+
this.executions.set(run.runId, execution);
|
|
79
|
+
void execution.catch((err) => {
|
|
80
|
+
console.error(`[Agent Team] Task run ${run.runId} failed to persist its terminal state: ${err.message}`);
|
|
81
|
+
});
|
|
82
|
+
return { board, run };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async stopRun(projectId, runId) {
|
|
86
|
+
return this.exclusive(projectId, async () => {
|
|
87
|
+
const run = await this.options.repository.get(runId);
|
|
88
|
+
if (!run || run.projectId !== projectId)
|
|
89
|
+
throw new BoardStoreError("task_run_not_found", "找不到这次任务执行记录", 404);
|
|
90
|
+
if (!isActiveTaskRun(run))
|
|
91
|
+
return run;
|
|
92
|
+
const now = this.now().toISOString();
|
|
93
|
+
const updated = { ...run, stopRequestedAt: run.stopRequestedAt ?? now, updatedAt: now };
|
|
94
|
+
await this.options.repository.save(updated);
|
|
95
|
+
const stopped = this.options.runtime.stop(run.sessionId);
|
|
96
|
+
if (!stopped && !this.options.runtime.isSessionRunning(run.sessionId)) {
|
|
97
|
+
return this.finishRun(updated, "interrupted", { error: "Task process was no longer running" });
|
|
98
|
+
}
|
|
99
|
+
return updated;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async waitForRun(runId) {
|
|
103
|
+
const execution = this.executions.get(runId);
|
|
104
|
+
if (execution)
|
|
105
|
+
return execution;
|
|
106
|
+
const run = await this.options.repository.get(runId);
|
|
107
|
+
if (!run)
|
|
108
|
+
throw new BoardStoreError("task_run_not_found", "找不到这次任务执行记录", 404);
|
|
109
|
+
return run;
|
|
110
|
+
}
|
|
111
|
+
async recoverInterruptedRuns() {
|
|
112
|
+
const active = await this.options.repository.listActive();
|
|
113
|
+
await Promise.all(active.map((run) => this.finishRun(run, "interrupted", {
|
|
114
|
+
error: "ChatCCC restarted while this task was running",
|
|
115
|
+
})));
|
|
116
|
+
return active.length;
|
|
117
|
+
}
|
|
118
|
+
async execute(run) {
|
|
119
|
+
try {
|
|
120
|
+
const result = await this.options.runtime.run({
|
|
121
|
+
sessionId: run.sessionId,
|
|
122
|
+
chatId: run.chatId,
|
|
123
|
+
agentId: run.agentId,
|
|
124
|
+
traceId: `agent-team-${run.runId}`,
|
|
125
|
+
prompt: taskPrompt(run),
|
|
126
|
+
});
|
|
127
|
+
if (result.outcome === "done") {
|
|
128
|
+
return this.finishRun(run, "succeeded", { result: result.result, moveTask: true });
|
|
129
|
+
}
|
|
130
|
+
if (result.outcome === "stopped") {
|
|
131
|
+
return this.finishRun(run, "canceled", { error: result.error, moveTask: true });
|
|
132
|
+
}
|
|
133
|
+
return this.finishRun(run, "failed", {
|
|
134
|
+
error: result.error || (result.outcome === "auto_ended" ? "Agent response timed out" : "Agent execution failed"),
|
|
135
|
+
result: result.result,
|
|
136
|
+
moveTask: true,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
return this.finishRun(run, "failed", { error: err.message, moveTask: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async finishRun(run, state, details = {}) {
|
|
144
|
+
const now = this.now().toISOString();
|
|
145
|
+
const latest = await this.options.repository.get(run.runId) ?? run;
|
|
146
|
+
if (!isActiveTaskRun(latest))
|
|
147
|
+
return latest;
|
|
148
|
+
const updated = {
|
|
149
|
+
...latest,
|
|
150
|
+
state,
|
|
151
|
+
updatedAt: now,
|
|
152
|
+
finishedAt: now,
|
|
153
|
+
...(details.result ? { result: details.result } : {}),
|
|
154
|
+
...(details.error ? { error: details.error } : {}),
|
|
155
|
+
};
|
|
156
|
+
await this.options.repository.save(updated);
|
|
157
|
+
if (details.moveTask !== false) {
|
|
158
|
+
await this.moveFinishedTask(updated).catch(async (err) => {
|
|
159
|
+
await this.options.repository.save({
|
|
160
|
+
...updated,
|
|
161
|
+
error: updated.error || `Task completed but board update failed: ${err.message}`,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return await this.options.repository.get(run.runId) ?? updated;
|
|
166
|
+
}
|
|
167
|
+
async moveFinishedTask(run) {
|
|
168
|
+
const board = await this.options.boardService.getBoard(run.projectId);
|
|
169
|
+
const task = board.tasks.find((candidate) => candidate.id === run.taskId && !candidate.deletedAt);
|
|
170
|
+
if (!task)
|
|
171
|
+
return;
|
|
172
|
+
const target = run.state === "succeeded" ? "done" : "on_hold";
|
|
173
|
+
if (task.columnId === target)
|
|
174
|
+
return;
|
|
175
|
+
const targetCount = board.tasks.filter((candidate) => !candidate.deletedAt && candidate.columnId === target).length;
|
|
176
|
+
await this.options.boardService.moveTask(run.projectId, run.taskId, {
|
|
177
|
+
expectedRevision: board.revision,
|
|
178
|
+
columnId: target,
|
|
179
|
+
index: targetCount,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async exclusive(projectId, operation) {
|
|
183
|
+
const previous = this.projectOperations.get(projectId) ?? Promise.resolve();
|
|
184
|
+
let release;
|
|
185
|
+
const current = new Promise((resolve) => { release = resolve; });
|
|
186
|
+
this.projectOperations.set(projectId, current);
|
|
187
|
+
await previous;
|
|
188
|
+
try {
|
|
189
|
+
return await operation();
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
release();
|
|
193
|
+
if (this.projectOperations.get(projectId) === current)
|
|
194
|
+
this.projectOperations.delete(projectId);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function taskPrompt(run) {
|
|
199
|
+
return [
|
|
200
|
+
"你正在执行 Agent Team 看板中的一个明确任务。请在当前工作目录完成任务,进行必要验证,并用简洁结果说明完成内容。",
|
|
201
|
+
"",
|
|
202
|
+
`任务 ID:${run.taskId}`,
|
|
203
|
+
`任务标题:${run.taskTitle}`,
|
|
204
|
+
run.taskDescription ? `任务说明:\n${run.taskDescription}` : "任务说明:(无)",
|
|
205
|
+
].join("\n");
|
|
206
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isAgentTool } from "../../agent-tool.js";
|
|
2
|
+
export const TASK_RUN_SCHEMA_VERSION = 1;
|
|
3
|
+
export function isActiveTaskRun(run) {
|
|
4
|
+
return run.state === "queued" || run.state === "running";
|
|
5
|
+
}
|
|
6
|
+
export function parseTaskRun(value) {
|
|
7
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
8
|
+
throw new Error("Invalid task run");
|
|
9
|
+
const run = value;
|
|
10
|
+
if (run.schemaVersion !== TASK_RUN_SCHEMA_VERSION)
|
|
11
|
+
throw new Error("Unsupported task run schema");
|
|
12
|
+
for (const field of ["runId", "projectId", "taskId", "taskTitle", "taskDescription", "chatId", "sessionId", "createdAt", "updatedAt"]) {
|
|
13
|
+
if (typeof run[field] !== "string" || (field !== "taskDescription" && !run[field])) {
|
|
14
|
+
throw new Error(`Task run is missing ${field}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (!Number.isInteger(run.attempt) || run.attempt < 1)
|
|
18
|
+
throw new Error("Invalid task run attempt");
|
|
19
|
+
if (!["queued", "running", "succeeded", "failed", "canceled", "interrupted"].includes(run.state)) {
|
|
20
|
+
throw new Error("Invalid task run state");
|
|
21
|
+
}
|
|
22
|
+
if (!isAgentTool(run.agentId))
|
|
23
|
+
throw new Error("Invalid task run Agent");
|
|
24
|
+
for (const field of ["startedAt", "stopRequestedAt", "finishedAt", "result", "error"]) {
|
|
25
|
+
if (run[field] !== undefined && typeof run[field] !== "string")
|
|
26
|
+
throw new Error(`Invalid task run ${field}`);
|
|
27
|
+
}
|
|
28
|
+
return run;
|
|
29
|
+
}
|
|
@@ -14,6 +14,11 @@ export function createAgentTeamRequestHandler(options) {
|
|
|
14
14
|
return options.mainAgentService();
|
|
15
15
|
return options.mainAgentService ?? null;
|
|
16
16
|
};
|
|
17
|
+
const taskExecutionService = () => {
|
|
18
|
+
if (typeof options.taskExecutionService === "function")
|
|
19
|
+
return options.taskExecutionService();
|
|
20
|
+
return options.taskExecutionService ?? null;
|
|
21
|
+
};
|
|
17
22
|
return async function handleAgentTeamRequest(req, res) {
|
|
18
23
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
19
24
|
const pathname = url.pathname;
|
|
@@ -97,6 +102,26 @@ export function createAgentTeamRequestHandler(options) {
|
|
|
97
102
|
jsonReply(res, 200, { ok: true, ...result });
|
|
98
103
|
return true;
|
|
99
104
|
}
|
|
105
|
+
const runsMatch = pathname.match(/^\/api\/agent-team\/boards\/([^/]+)\/runs$/);
|
|
106
|
+
if (runsMatch && method === "GET") {
|
|
107
|
+
const runs = await requireTaskExecutionService(taskExecutionService())
|
|
108
|
+
.listRuns(decodeURIComponent(runsMatch[1]));
|
|
109
|
+
jsonReply(res, 200, { ok: true, runs });
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const runTaskMatch = pathname.match(/^\/api\/agent-team\/boards\/([^/]+)\/tasks\/([^/]+)\/run$/);
|
|
113
|
+
if (runTaskMatch && method === "POST") {
|
|
114
|
+
const body = await bodyJson(req);
|
|
115
|
+
const result = await requireTaskExecutionService(taskExecutionService()).startTask(decodeURIComponent(runTaskMatch[1]), decodeURIComponent(runTaskMatch[2]), revisionField(body.expectedRevision));
|
|
116
|
+
jsonReply(res, 202, { ok: true, ...result });
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
const stopRunMatch = pathname.match(/^\/api\/agent-team\/boards\/([^/]+)\/runs\/([^/]+)\/stop$/);
|
|
120
|
+
if (stopRunMatch && method === "POST") {
|
|
121
|
+
const run = await requireTaskExecutionService(taskExecutionService()).stopRun(decodeURIComponent(stopRunMatch[1]), decodeURIComponent(stopRunMatch[2]));
|
|
122
|
+
jsonReply(res, 200, { ok: true, run });
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
100
125
|
const tasksMatch = pathname.match(/^\/api\/agent-team\/boards\/([^/]+)\/tasks$/);
|
|
101
126
|
if (tasksMatch && method === "POST") {
|
|
102
127
|
const body = await bodyJson(req);
|
|
@@ -151,19 +176,30 @@ export function createAgentTeamRequestHandler(options) {
|
|
|
151
176
|
}
|
|
152
177
|
export const defaultAgentTeamBoardService = new BoardService(new JsonBoardRepository());
|
|
153
178
|
let defaultMainAgentService = null;
|
|
179
|
+
let defaultTaskExecutionService = null;
|
|
154
180
|
export const handleAgentTeamRequest = createAgentTeamRequestHandler({
|
|
155
181
|
service: defaultAgentTeamBoardService,
|
|
156
182
|
mainAgentService: () => defaultMainAgentService,
|
|
183
|
+
taskExecutionService: () => defaultTaskExecutionService,
|
|
157
184
|
});
|
|
158
185
|
export function setDefaultAgentTeamMainAgentService(service) {
|
|
159
186
|
defaultMainAgentService = service;
|
|
160
187
|
}
|
|
188
|
+
export function setDefaultAgentTeamTaskExecutionService(service) {
|
|
189
|
+
defaultTaskExecutionService = service;
|
|
190
|
+
}
|
|
161
191
|
function requireMainAgentService(service) {
|
|
162
192
|
if (!service) {
|
|
163
193
|
throw new BoardStoreError("main_agent_unavailable", "飞书主 Agent 服务尚未启动", 503);
|
|
164
194
|
}
|
|
165
195
|
return service;
|
|
166
196
|
}
|
|
197
|
+
function requireTaskExecutionService(service) {
|
|
198
|
+
if (!service) {
|
|
199
|
+
throw new BoardStoreError("main_agent_unavailable", "主 Agent 任务执行服务尚未启动", 503);
|
|
200
|
+
}
|
|
201
|
+
return service;
|
|
202
|
+
}
|
|
167
203
|
async function bodyJson(req) {
|
|
168
204
|
try {
|
|
169
205
|
return await readUtf8JsonBody(req, MAX_REQUEST_BYTES);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { resumeAndPrompt, stopSession } from "../../session.js";
|
|
2
|
+
import { isSessionRunning } from "../../session-chat-binding.js";
|
|
3
|
+
import { readStreamState } from "../../stream-state.js";
|
|
4
|
+
export function createTaskExecutionRuntime(platform) {
|
|
5
|
+
return {
|
|
6
|
+
async run(input) {
|
|
7
|
+
const outcome = await resumeAndPrompt(input.sessionId, input.prompt, platform, input.chatId, Date.now(), input.agentId, input.traceId);
|
|
8
|
+
const stream = await readStreamState(input.sessionId);
|
|
9
|
+
if (outcome === "busy") {
|
|
10
|
+
return { outcome: "error", error: "Main Agent session became busy before the task started" };
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
outcome,
|
|
14
|
+
...(stream?.finalReply ? { result: stream.finalReply } : {}),
|
|
15
|
+
...(stream?.terminalError?.message ? { error: stream.terminalError.message } : {}),
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
stop: stopSession,
|
|
19
|
+
isSessionRunning,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
import { MainAgentService } from "./application/main-agent-service.js";
|
|
2
|
-
import {
|
|
2
|
+
import { TaskExecutionService } from "./application/task-execution-service.js";
|
|
3
|
+
import { defaultAgentTeamBoardService, setDefaultAgentTeamMainAgentService, setDefaultAgentTeamTaskExecutionService, } from "./http/board-routes.js";
|
|
3
4
|
import { createMainAgentSessionRuntime } from "./infrastructure/main-agent-session-runtime.js";
|
|
5
|
+
import { createTaskExecutionRuntime } from "./infrastructure/task-execution-runtime.js";
|
|
4
6
|
import { feishuP2pContactStore } from "./repositories/feishu-p2p-contact-store.js";
|
|
5
7
|
import { JsonMainAgentBindingRepository } from "./repositories/main-agent-binding-repository.js";
|
|
8
|
+
import { JsonTaskRunRepository } from "./repositories/json-task-run-repository.js";
|
|
6
9
|
/** Wire runtime dependencies only from index.ts, keeping the standalone Web UI import side-effect free. */
|
|
7
10
|
export function configureAgentTeamMainAgent(platform) {
|
|
8
|
-
|
|
11
|
+
const bindingRepository = new JsonMainAgentBindingRepository();
|
|
12
|
+
const mainAgentService = new MainAgentService({
|
|
9
13
|
boardService: defaultAgentTeamBoardService,
|
|
10
|
-
bindingRepository
|
|
14
|
+
bindingRepository,
|
|
11
15
|
contactStore: feishuP2pContactStore,
|
|
12
16
|
platform,
|
|
13
17
|
runtime: createMainAgentSessionRuntime(platform),
|
|
14
|
-
})
|
|
18
|
+
});
|
|
19
|
+
const taskExecutionService = new TaskExecutionService({
|
|
20
|
+
boardService: defaultAgentTeamBoardService,
|
|
21
|
+
repository: new JsonTaskRunRepository(),
|
|
22
|
+
runtime: createTaskExecutionRuntime(platform),
|
|
23
|
+
getBinding: (projectId) => bindingRepository.get(projectId),
|
|
24
|
+
});
|
|
25
|
+
setDefaultAgentTeamMainAgentService(mainAgentService);
|
|
26
|
+
setDefaultAgentTeamTaskExecutionService(taskExecutionService);
|
|
27
|
+
void taskExecutionService.recoverInterruptedRuns().catch((err) => {
|
|
28
|
+
console.error(`[Agent Team] Failed to recover interrupted task runs: ${err.message}`);
|
|
29
|
+
});
|
|
15
30
|
}
|
|
@@ -15,6 +15,8 @@ export class FeishuP2pContactStore {
|
|
|
15
15
|
catch (err) {
|
|
16
16
|
if (err.code === "ENOENT")
|
|
17
17
|
return null;
|
|
18
|
+
if (err instanceof InvalidFeishuOpenIdError)
|
|
19
|
+
return null;
|
|
18
20
|
throw err;
|
|
19
21
|
}
|
|
20
22
|
}
|
|
@@ -41,14 +43,23 @@ function parseContact(value) {
|
|
|
41
43
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
42
44
|
throw new Error("Invalid Feishu P2P contact");
|
|
43
45
|
const contact = value;
|
|
44
|
-
if (
|
|
45
|
-
throw new
|
|
46
|
+
if (!isValidFeishuOpenId(contact.openId))
|
|
47
|
+
throw new InvalidFeishuOpenIdError(String(contact.openId));
|
|
46
48
|
if (typeof contact.chatId !== "string" || !contact.chatId)
|
|
47
49
|
throw new Error("Feishu P2P contact is missing chatId");
|
|
48
50
|
if (typeof contact.receivedAt !== "string" || !contact.receivedAt)
|
|
49
51
|
throw new Error("Feishu P2P contact is missing receivedAt");
|
|
50
52
|
return { openId: contact.openId, chatId: contact.chatId, receivedAt: contact.receivedAt };
|
|
51
53
|
}
|
|
54
|
+
export function isValidFeishuOpenId(value) {
|
|
55
|
+
return typeof value === "string" && /^ou_[A-Za-z0-9]+$/.test(value);
|
|
56
|
+
}
|
|
57
|
+
class InvalidFeishuOpenIdError extends Error {
|
|
58
|
+
constructor(value) {
|
|
59
|
+
super(`Invalid Feishu openId: ${value}`);
|
|
60
|
+
this.name = "InvalidFeishuOpenIdError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
52
63
|
async function writeJsonAtomic(path, value) {
|
|
53
64
|
await mkdir(dirname(path), { recursive: true });
|
|
54
65
|
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { isActiveTaskRun, parseTaskRun, } from "../domain/task-run.js";
|
|
6
|
+
export class JsonTaskRunRepository {
|
|
7
|
+
rootDir;
|
|
8
|
+
operationQueue = Promise.resolve();
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.rootDir = options.rootDir ?? join(homedir(), ".chatccc", "agent-team", "task-runs");
|
|
11
|
+
}
|
|
12
|
+
async get(runId) {
|
|
13
|
+
const files = await this.projectDirectories();
|
|
14
|
+
for (const projectId of files) {
|
|
15
|
+
try {
|
|
16
|
+
return parseTaskRun(JSON.parse(await readFile(this.pathFor(projectId, runId), "utf8")));
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
if (err.code !== "ENOENT")
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
async save(run) {
|
|
26
|
+
const parsed = parseTaskRun(run);
|
|
27
|
+
await this.exclusive(async () => {
|
|
28
|
+
await writeJsonAtomic(this.pathFor(parsed.projectId, parsed.runId), parsed);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async listByProject(projectId) {
|
|
32
|
+
const directory = this.projectPath(projectId);
|
|
33
|
+
let entries;
|
|
34
|
+
try {
|
|
35
|
+
entries = await readdir(directory);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err.code === "ENOENT")
|
|
39
|
+
return [];
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
const runs = await Promise.all(entries
|
|
43
|
+
.filter((entry) => entry.endsWith(".json"))
|
|
44
|
+
.map(async (entry) => parseTaskRun(JSON.parse(await readFile(join(directory, entry), "utf8")))));
|
|
45
|
+
return runs.sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.runId.localeCompare(a.runId));
|
|
46
|
+
}
|
|
47
|
+
async listActive() {
|
|
48
|
+
const projectIds = await this.projectDirectories();
|
|
49
|
+
const runs = (await Promise.all(projectIds.map((projectId) => this.listByProject(projectId)))).flat();
|
|
50
|
+
return runs.filter(isActiveTaskRun);
|
|
51
|
+
}
|
|
52
|
+
async projectDirectories() {
|
|
53
|
+
try {
|
|
54
|
+
return await readdir(this.rootDir);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err.code === "ENOENT")
|
|
58
|
+
return [];
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
projectPath(projectId) {
|
|
63
|
+
assertId(projectId, "project");
|
|
64
|
+
return join(this.rootDir, projectId);
|
|
65
|
+
}
|
|
66
|
+
pathFor(projectId, runId) {
|
|
67
|
+
assertId(runId, "run");
|
|
68
|
+
return join(this.projectPath(projectId), `${runId}.json`);
|
|
69
|
+
}
|
|
70
|
+
async exclusive(operation) {
|
|
71
|
+
const previous = this.operationQueue;
|
|
72
|
+
let release;
|
|
73
|
+
this.operationQueue = new Promise((resolve) => { release = resolve; });
|
|
74
|
+
await previous;
|
|
75
|
+
try {
|
|
76
|
+
return await operation();
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
release();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function assertId(value, kind) {
|
|
84
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(value))
|
|
85
|
+
throw new Error(`Invalid ${kind} id: ${value}`);
|
|
86
|
+
}
|
|
87
|
+
async function writeJsonAtomic(path, value) {
|
|
88
|
+
await mkdir(dirname(path), { recursive: true });
|
|
89
|
+
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
90
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
91
|
+
try {
|
|
92
|
+
await rename(tempPath, path);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
await unlink(tempPath).catch(() => { });
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -10,12 +10,13 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
10
10
|
.sidebar{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;gap:24px;padding:24px 18px;background:var(--panel);border-right:1px solid var(--line)}.brand{display:flex;align-items:center;gap:11px;color:var(--text);text-decoration:none;font-size:17px;font-weight:800}.brand-icon{width:34px;height:34px;display:grid;place-items:center;border-radius:11px;background:var(--accent);color:#fff;box-shadow:0 8px 18px rgba(103,88,232,.25)}.nav{display:grid;gap:7px}.nav-item{width:100%;display:flex;align-items:center;gap:11px;padding:10px 12px;border:0;border-radius:10px;background:transparent;color:var(--muted);font-size:13px;font-weight:650;text-align:left}.nav-item:hover{border:0;background:var(--soft)}.nav-item.active{background:var(--accent-soft);color:var(--accent)}.nav-icon{width:18px;text-align:center}.side-note{margin-top:auto;padding:13px;border:1px solid var(--line);background:var(--soft);border-radius:12px;font-size:11px;line-height:1.55;color:var(--muted)}.side-note strong{color:var(--success)}
|
|
11
11
|
.shell{min-width:320px;padding:24px}.top{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;margin-bottom:18px}.eyebrow{font-size:11px;font-weight:800;letter-spacing:.12em;color:var(--accent);margin-bottom:6px}.top h1{font-size:27px;line-height:1.2;letter-spacing:-.025em;margin:0}.top p{font-size:12px;color:var(--muted);margin:6px 0 0}.top a{height:38px;display:inline-flex;align-items:center;padding:0 13px;border:1px solid var(--line);border-radius:10px;background:var(--panel);color:var(--text);text-decoration:none;font-size:12px;font-weight:700}
|
|
12
12
|
.workspace-bar{display:grid;grid-template-columns:minmax(180px,220px) minmax(280px,1fr) auto auto;gap:9px;padding:11px;background:var(--panel);border:1px solid var(--line);border-radius:15px;margin-bottom:12px;box-shadow:var(--shadow)}input,textarea,select{border:1px solid var(--line);background:var(--soft);color:var(--text);border-radius:9px;padding:10px 12px;outline:none}input:focus,textarea:focus,select:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(103,88,232,.13)}button{cursor:pointer;border:1px solid var(--line);border-radius:9px;padding:10px 14px;background:var(--panel);color:var(--text);font-weight:700}button:hover{border-color:#c8cde0;background:#fbfbfe}button.primary{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:0 7px 17px rgba(103,88,232,.22)}button.primary:hover{background:#5949d6;border-color:#5949d6}button.ghost{background:var(--soft)}button.danger{background:#fff1f3;border-color:#f3c4ce;color:var(--danger)}.hidden{display:none!important}
|
|
13
|
-
.main-agent-bar{display:grid;grid-template-columns:minmax(180px,1fr)
|
|
13
|
+
.main-agent-bar{display:grid;grid-template-columns:minmax(180px,1fr) auto;align-items:center;gap:12px;padding:13px 15px;background:var(--panel);border:1px solid var(--line);border-radius:15px;margin-bottom:12px;box-shadow:var(--shadow)}.main-agent-copy{min-width:0}.main-agent-copy strong{display:block;font-size:13px;margin-bottom:3px}.main-agent-copy span{display:block;color:var(--muted);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.main-agent-controls{display:flex;align-items:center;gap:8px}.main-agent-controls select{min-width:130px}.main-agent-controls button{min-width:120px;white-space:nowrap}.agent-status{display:inline-flex;align-items:center;gap:6px;color:var(--muted)}.agent-status::before{content:"●";font-size:9px;color:#a3aabd}.agent-status.ready::before{color:var(--success)}.agent-status.error::before{color:var(--danger)}.agent-status.provisioning::before{color:#b7791f}
|
|
14
14
|
.status-row{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 3px 10px;color:var(--muted);font-size:11px}.workspace-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.save-state{min-width:78px;text-align:right;color:var(--success);font-weight:700}.save-state::before{content:"● ";font-size:9px}.save-state.saving{color:#b7791f}.save-state.error{color:var(--danger)}
|
|
15
|
-
.board-scroll{overflow-x:auto;padding-bottom:12px}.board{display:grid;grid-template-columns:repeat(5,minmax(230px,1fr));gap:11px;min-width:1200px}.column{display:flex;flex-direction:column;min-height:calc(100vh - 230px);background:var(--soft);border:1px solid var(--line);border-radius:15px;overflow:hidden}.column.drag-over{border-color:var(--accent);box-shadow:0 0 0 3px rgba(103,88,232,.13)}.column-head{display:flex;align-items:center;gap:8px;padding:
|
|
15
|
+
.board-scroll{overflow-x:auto;padding-bottom:12px}.board{display:grid;grid-template-columns:repeat(5,minmax(230px,1fr));gap:11px;min-width:1200px}.column{display:flex;flex-direction:column;min-height:calc(100vh - 230px);background:var(--soft);border:1px solid var(--line);border-radius:15px;overflow:hidden}.column.drag-over{border-color:var(--accent);box-shadow:0 0 0 3px rgba(103,88,232,.13)}.column-head{display:flex;align-items:center;gap:8px;padding:11px 10px 7px 13px}.column-dot{width:7px;height:7px;border-radius:50%;background:var(--accent)}.column-title{font-size:13px;font-weight:800}.count{margin-left:auto;display:inline-grid;place-items:center;min-width:23px;height:23px;padding:0 7px;border-radius:999px;background:var(--panel);border:1px solid var(--line);color:var(--muted);font-size:10px}.column-add{width:27px;height:27px;display:grid;place-items:center;padding:0;border-color:transparent;border-radius:8px;background:transparent;color:var(--muted);font-size:19px;font-weight:500;line-height:1}.column-add:hover{border-color:var(--line);background:var(--panel);color:var(--accent)}.task-list{flex:1;min-height:100px;padding:4px 10px 10px}.task{position:relative;background:var(--panel2);border:1px solid var(--line);border-radius:11px;padding:12px;margin:8px 0;box-shadow:0 4px 15px rgba(34,42,65,.045);cursor:pointer}.task:hover{border-color:#d1d5e1;box-shadow:0 8px 22px rgba(34,42,65,.08)}.task.dragging{opacity:.35;cursor:grabbing}.task h3{font-size:13px;line-height:1.45;margin:0 42px 6px 0}.task p{font-size:11px;line-height:1.55;color:var(--muted);margin:0;white-space:pre-wrap;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}.task-actions{display:flex;gap:4px;position:absolute;right:7px;top:7px;opacity:0;transition:opacity .15s}.task:hover .task-actions,.task:focus-within .task-actions{opacity:1}.icon-btn{padding:3px 6px;border-radius:6px;background:var(--soft);color:var(--muted);font-size:10px}.empty{padding:20px 8px;text-align:center;color:#9aa3b4;font-size:11px}
|
|
16
|
+
.task.running{cursor:default;border-color:#cfc8ff}.task-run{margin-top:10px;padding-top:9px;border-top:1px solid var(--line)}.task-run-head{display:flex;align-items:center;justify-content:space-between;gap:7px}.run-status{font-size:10px;font-weight:800;color:var(--muted)}.run-status.running,.run-status.queued{color:var(--accent)}.run-status.succeeded{color:var(--success)}.run-status.failed,.run-status.interrupted{color:var(--danger)}.run-result{margin-top:6px!important;padding:7px 8px;border-radius:7px;background:var(--soft);display:-webkit-box!important;-webkit-line-clamp:3!important}.run-button{padding:5px 8px;font-size:10px;border-radius:7px}.run-button.primary{box-shadow:none}
|
|
16
17
|
.modal-backdrop{position:fixed;inset:0;z-index:20;background:rgba(24,32,51,.36);display:grid;place-items:center;padding:20px;backdrop-filter:blur(3px)}.modal{width:min(560px,100%);background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:20px;box-shadow:0 28px 90px rgba(35,43,65,.24)}.modal h2{margin:0 0 16px}.field{display:grid;gap:7px;margin-bottom:14px}.field label{font-size:13px;color:var(--text);font-weight:650}.field textarea{min-height:150px;resize:vertical}.modal-actions{display:flex;justify-content:flex-end;gap:9px}.directory-modal{width:min(820px,100%)}.directory-layout{display:grid;grid-template-columns:180px minmax(0,1fr);min-height:430px;border:1px solid var(--line);border-radius:12px;overflow:hidden;margin-bottom:16px}.directory-sidebar{padding:12px;background:var(--soft);border-right:1px solid var(--line)}.directory-label{margin:2px 6px 9px;color:var(--muted);font-size:11px;font-weight:800;letter-spacing:.05em}.directory-locations{display:grid;gap:4px}.directory-location{width:100%;padding:8px 9px;border:0;background:transparent;text-align:left;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.directory-location:hover{border:0;background:var(--accent-soft);color:var(--accent)}.directory-main{min-width:0;display:flex;flex-direction:column;padding:12px}.directory-toolbar{display:grid;grid-template-columns:auto auto minmax(0,1fr);gap:7px}.directory-toolbar button{padding:8px 11px}.directory-toolbar input{min-width:0;padding:8px 10px}.directory-list{flex:1;height:310px;overflow:auto;margin-top:10px;border:1px solid var(--line);border-radius:10px;background:var(--soft);padding:6px}.directory-row{width:100%;display:flex;align-items:center;gap:9px;padding:9px 10px;border:0;background:transparent;text-align:left;font-size:12px}.directory-row:hover{border:0;background:#fff}.directory-row.selected{border:0;background:var(--accent-soft);color:var(--accent)}.directory-folder-icon{color:#b7791f;font-size:15px}.directory-empty{padding:46px 16px;text-align:center;color:var(--muted);font-size:12px}.directory-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:10px;color:var(--muted);font-size:11px}.directory-footer label{display:flex;align-items:center;gap:6px;white-space:nowrap}.directory-footer input{width:15px;height:15px;margin:0}.directory-selected{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}.toast{position:fixed;right:22px;bottom:22px;z-index:30;max-width:min(420px,calc(100vw - 44px));padding:12px 15px;border-radius:10px;background:#253047;color:#fff;border:1px solid #35415a;box-shadow:0 16px 50px rgba(35,43,65,.25)}.toast.error{border-color:#ef9bad;background:#9f2942}
|
|
17
|
-
@media(max-width:1100px){.app-shell{grid-template-columns:78px minmax(0,1fr)}.sidebar{padding:22px 12px;align-items:center}.brand span:last-child,.nav-item span:last-child,.side-note{display:none}.nav-item{width:45px;height:45px;justify-content:center;padding:0}.workspace-bar{grid-template-columns:1fr 1fr}.workspace-bar input{grid-column:1/-1}.main-agent-bar{grid-template-columns:1fr
|
|
18
|
-
@media(max-width:700px){.app-shell{display:block}.sidebar{display:none}.shell{padding:14px}.top{align-items:flex-start}.workspace-bar{grid-template-columns:1fr}.workspace-bar input{grid-column:auto}.main-agent-
|
|
18
|
+
@media(max-width:1100px){.app-shell{grid-template-columns:78px minmax(0,1fr)}.sidebar{padding:22px 12px;align-items:center}.brand span:last-child,.nav-item span:last-child,.side-note{display:none}.nav-item{width:45px;height:45px;justify-content:center;padding:0}.workspace-bar{grid-template-columns:1fr 1fr}.workspace-bar input{grid-column:1/-1}.main-agent-bar{grid-template-columns:1fr}.main-agent-controls{justify-content:flex-end}.board{min-width:1200px}}
|
|
19
|
+
@media(max-width:700px){.app-shell{display:block}.sidebar{display:none}.shell{padding:14px}.top{align-items:flex-start}.workspace-bar{grid-template-columns:1fr}.workspace-bar input{grid-column:auto}.main-agent-controls{display:grid;grid-template-columns:minmax(0,1fr) auto;justify-content:stretch}.main-agent-controls select{min-width:0}.board{min-width:1180px}.directory-layout{grid-template-columns:1fr}.directory-sidebar{border-right:0;border-bottom:1px solid var(--line)}.directory-locations{display:flex;overflow-x:auto}.directory-location{width:auto;min-width:max-content}.directory-toolbar{grid-template-columns:auto auto 1fr}.directory-list{height:270px}}
|
|
19
20
|
</style>
|
|
20
21
|
</head>
|
|
21
22
|
<body>
|
|
@@ -39,15 +40,15 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
39
40
|
</section>
|
|
40
41
|
<section class="main-agent-bar" aria-label="项目主 Agent">
|
|
41
42
|
<div class="main-agent-copy"><strong>项目主 Agent</strong><span id="main-agent-status" class="agent-status">请先打开一个项目</span></div>
|
|
42
|
-
<div class="main-agent-controls"><select id="main-agent" disabled aria-label="选择主 Agent"><option value="">选择主 Agent</option></select><button id="save-main-agent" class="primary" disabled
|
|
43
|
+
<div class="main-agent-controls"><select id="main-agent" disabled aria-label="选择主 Agent"><option value="">选择主 Agent</option></select><button id="save-main-agent" class="primary" disabled>设置为主 Agent</button></div>
|
|
43
44
|
</section>
|
|
44
45
|
<div class="status-row"><span id="workspace-name" class="workspace-name">尚未打开工作目录</span><span id="save-state" class="save-state">本地 JSON</span></div>
|
|
45
46
|
<div class="board-scroll"><section class="board" id="board">
|
|
46
|
-
<article class="column" data-column="brainstorm"><header class="column-head"><span class="column-dot"></span><span class="column-title">头脑风暴</span><span class="count">0</span
|
|
47
|
-
<article class="column" data-column="todo"><header class="column-head"><span class="column-dot"></span><span class="column-title">Todo</span><span class="count">0</span
|
|
48
|
-
<article class="column" data-column="doing"><header class="column-head"><span class="column-dot"></span><span class="column-title">Doing</span><span class="count">0</span
|
|
49
|
-
<article class="column" data-column="done"><header class="column-head"><span class="column-dot"></span><span class="column-title">Done</span><span class="count">0</span
|
|
50
|
-
<article class="column" data-column="on_hold"><header class="column-head"><span class="column-dot"></span><span class="column-title">搁置</span><span class="count">0</span
|
|
47
|
+
<article class="column" data-column="brainstorm"><header class="column-head"><span class="column-dot"></span><span class="column-title">头脑风暴</span><span class="count">0</span><button type="button" class="column-add" data-add="brainstorm" aria-label="在头脑风暴中添加任务" title="添加任务">+</button></header><div class="task-list"></div></article>
|
|
48
|
+
<article class="column" data-column="todo"><header class="column-head"><span class="column-dot"></span><span class="column-title">Todo</span><span class="count">0</span><button type="button" class="column-add" data-add="todo" aria-label="在 Todo 中添加任务" title="添加任务">+</button></header><div class="task-list"></div></article>
|
|
49
|
+
<article class="column" data-column="doing"><header class="column-head"><span class="column-dot"></span><span class="column-title">Doing</span><span class="count">0</span><button type="button" class="column-add" data-add="doing" aria-label="在 Doing 中添加任务" title="添加任务">+</button></header><div class="task-list"></div></article>
|
|
50
|
+
<article class="column" data-column="done"><header class="column-head"><span class="column-dot"></span><span class="column-title">Done</span><span class="count">0</span><button type="button" class="column-add" data-add="done" aria-label="在 Done 中添加任务" title="添加任务">+</button></header><div class="task-list"></div></article>
|
|
51
|
+
<article class="column" data-column="on_hold"><header class="column-head"><span class="column-dot"></span><span class="column-title">搁置</span><span class="count">0</span><button type="button" class="column-add" data-add="on_hold" aria-label="在搁置中添加任务" title="添加任务">+</button></header><div class="task-list"></div></article>
|
|
51
52
|
</section></div>
|
|
52
53
|
</main>
|
|
53
54
|
</div>
|
|
@@ -99,7 +100,7 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
99
100
|
<div id="toast" class="toast hidden"></div>
|
|
100
101
|
<script>
|
|
101
102
|
(function(){
|
|
102
|
-
var state={board:null,binding:null,workspaces:[],agentOptions:[],dragTaskId:null,modalTaskId:null,modalColumnId:null,relinkBoardId:null,pendingAgentId:null,pendingWorkspacePath:null,workspaceSelectionVersion:0,directoryLocations:[],directoryPath:null,directoryParentPath:null,directorySelectedPath:null,directoryBrowseVersion:0,contactTimer:null,contactChecking:false,mutationQueue:Promise.resolve()};
|
|
103
|
+
var state={board:null,binding:null,runs:[],runPollTimer:null,runPollBusy:false,workspaces:[],agentOptions:[],dragTaskId:null,modalTaskId:null,modalColumnId:null,relinkBoardId:null,pendingAgentId:null,pendingWorkspacePath:null,workspaceSelectionVersion:0,directoryLocations:[],directoryPath:null,directoryParentPath:null,directorySelectedPath:null,directoryBrowseVersion:0,contactTimer:null,contactChecking:false,mutationQueue:Promise.resolve()};
|
|
103
104
|
var columns=['brainstorm','todo','doing','done','on_hold'];
|
|
104
105
|
var recent=document.getElementById('recent');
|
|
105
106
|
var pathInput=document.getElementById('workspace-path');
|
|
@@ -137,7 +138,7 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
137
138
|
var version=selectionVersion===undefined?++state.workspaceSelectionVersion:selectionVersion;
|
|
138
139
|
await state.mutationQueue;
|
|
139
140
|
setSaving('加载中…','saving');
|
|
140
|
-
try{var result=await request('POST','/api/agent-team/open',{workspacePath:path.trim()});if(version!==state.workspaceSelectionVersion)return;state.board=result.board;state.binding=result.binding||null;state.relinkBoardId=null;relinkButton.classList.add('hidden');pathInput.value=state.board.workspacePath;await refreshWorkspaces(state.board.boardId);if(version!==state.workspaceSelectionVersion)return;renderBoard();setSaving('已保存','');}
|
|
141
|
+
try{var result=await request('POST','/api/agent-team/open',{workspacePath:path.trim()});if(version!==state.workspaceSelectionVersion)return;state.board=result.board;state.binding=result.binding||null;state.runs=[];state.relinkBoardId=null;relinkButton.classList.add('hidden');pathInput.value=state.board.workspacePath;await Promise.all([refreshWorkspaces(state.board.boardId),loadRuns(false)]);if(version!==state.workspaceSelectionVersion)return;renderBoard();scheduleRunPoll();setSaving('已保存','');}
|
|
141
142
|
catch(error){if(version!==state.workspaceSelectionVersion)return;setSaving('打开失败','error');toast(error.message,true);}
|
|
142
143
|
}
|
|
143
144
|
async function selectWorkspacePath(path){
|
|
@@ -158,16 +159,26 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
158
159
|
function closeDirectoryModal(){state.directoryBrowseVersion++;document.getElementById('directory-modal').classList.add('hidden');}
|
|
159
160
|
async function confirmDirectorySelection(){var target=state.directorySelectedPath||state.directoryPath;if(!target)return;var button=document.getElementById('directory-confirm');button.disabled=true;try{var result=await request('POST','/api/agent-team/filesystem/validate-directory',{path:target});closeDirectoryModal();pathInput.value=result.path;if(!state.relinkBoardId)selectWorkspacePath(result.path);}catch(error){toast(error.message,true);}finally{button.disabled=false;}}
|
|
160
161
|
async function reloadBoard(){if(!state.board)return;var result=await api('/api/agent-team/boards/'+encodeURIComponent(state.board.boardId));state.board=result.board;state.binding=result.binding||null;renderBoard();}
|
|
162
|
+
function workspaceShortName(value){var path=String(value||'');var slash=String.fromCharCode(92);while(path.endsWith('/')||path.endsWith(slash))path=path.slice(0,-1);var index=Math.max(path.lastIndexOf('/'),path.lastIndexOf(slash));return path.slice(index+1)||path;}
|
|
163
|
+
async function loadRuns(render){if(!state.board)return;var boardId=state.board.boardId;var result=await api('/api/agent-team/boards/'+encodeURIComponent(boardId)+'/runs');if(!state.board||state.board.boardId!==boardId)return;state.runs=result.runs||[];if(render!==false)renderBoard();}
|
|
164
|
+
function latestRun(taskId){return state.runs.find(function(run){return run.taskId===taskId;})||null;}
|
|
165
|
+
function isActiveRun(run){return !!run&&(run.state==='queued'||run.state==='running');}
|
|
166
|
+
function runLabel(run){var labels={queued:'等待执行',running:run.stopRequestedAt?'正在停止':'主 Agent 执行中',succeeded:'执行成功',failed:'执行失败',canceled:'已停止',interrupted:'重启时中断'};return labels[run.state]||run.state;}
|
|
167
|
+
function scheduleRunPoll(){if(state.runPollTimer){clearTimeout(state.runPollTimer);state.runPollTimer=null;}if(!state.runs.some(isActiveRun))return;state.runPollTimer=setTimeout(pollRuns,1500);}
|
|
168
|
+
async function pollRuns(){if(state.runPollBusy||!state.board)return scheduleRunPoll();state.runPollBusy=true;var boardId=state.board.boardId;try{var results=await Promise.all([api('/api/agent-team/boards/'+encodeURIComponent(boardId)),api('/api/agent-team/boards/'+encodeURIComponent(boardId)+'/runs')]);if(state.board&&state.board.boardId===boardId){state.board=results[0].board;state.binding=results[0].binding||null;state.runs=results[1].runs||[];renderBoard();if(!state.runs.some(isActiveRun))setSaving('已保存','');}}catch(error){toast(error.message,true);}finally{state.runPollBusy=false;scheduleRunPoll();}}
|
|
169
|
+
async function startTaskRun(task){if(!state.board)return;if(!state.binding||state.binding.status!=='ready')return toast('请先设置可用的主 Agent',true);try{setSaving('正在交给主 Agent…','saving');var result=await request('POST','/api/agent-team/boards/'+encodeURIComponent(state.board.boardId)+'/tasks/'+encodeURIComponent(task.id)+'/run',{expectedRevision:state.board.revision});state.board=result.board;state.runs=[result.run].concat(state.runs.filter(function(run){return run.runId!==result.run.runId;}));renderBoard();scheduleRunPoll();setSaving('任务执行中','saving');}catch(error){if(error.code==='revision_conflict')await reloadBoard();toast(error.message,true);setSaving('执行失败','error');}}
|
|
170
|
+
async function stopTaskRun(run){if(!state.board)return;try{var result=await request('POST','/api/agent-team/boards/'+encodeURIComponent(state.board.boardId)+'/runs/'+encodeURIComponent(run.runId)+'/stop',{});state.runs=state.runs.map(function(item){return item.runId===result.run.runId?result.run:item;});renderBoard();scheduleRunPoll();toast('已请求主 Agent 停止任务');}catch(error){toast(error.message,true);}}
|
|
161
171
|
function activeTasks(columnId){return state.board?state.board.tasks.filter(function(task){return !task.deletedAt&&task.columnId===columnId;}).sort(function(a,b){return a.order-b.order;}):[];}
|
|
162
172
|
function renderBoard(){
|
|
163
173
|
document.getElementById('workspace-name').textContent=state.board?state.board.workspacePath:'尚未打开工作目录';
|
|
164
174
|
renderMainAgent();
|
|
165
175
|
columns.forEach(function(columnId){var column=document.querySelector('[data-column="'+columnId+'"]');var list=column.querySelector('.task-list');var tasks=activeTasks(columnId);column.querySelector('.count').textContent=String(tasks.length);list.innerHTML='';
|
|
166
176
|
if(!tasks.length){var empty=document.createElement('div');empty.className='empty';empty.textContent='暂无任务';list.appendChild(empty);}
|
|
167
|
-
tasks.forEach(function(task){var card=document.createElement('article');card.className='task';card.draggable
|
|
177
|
+
tasks.forEach(function(task){var run=latestRun(task.id);var active=isActiveRun(run);var dragged=false;var card=document.createElement('article');card.className='task'+(active?' running':'');card.draggable=!active;card.dataset.taskId=task.id;
|
|
168
178
|
var title=document.createElement('h3');title.textContent=task.title;card.appendChild(title);if(task.description){var desc=document.createElement('p');desc.textContent=task.description;card.appendChild(desc);}
|
|
169
|
-
var actions=document.createElement('div');actions.className='task-actions';var edit=document.createElement('button');edit.type='button';edit.className='icon-btn';edit.textContent='编辑';edit.addEventListener('click',function(event){event.stopPropagation();showEdit(task);});var del=document.createElement('button');del.type='button';del.className='icon-btn';del.textContent='
|
|
170
|
-
|
|
179
|
+
var actions=document.createElement('div');actions.className='task-actions';var edit=document.createElement('button');edit.type='button';edit.className='icon-btn';edit.textContent='编辑';edit.disabled=active;edit.addEventListener('click',function(event){event.stopPropagation();showEdit(task);});var del=document.createElement('button');del.type='button';del.className='icon-btn';del.textContent='删除';del.disabled=active;del.addEventListener('click',function(event){event.stopPropagation();removeTask(task);});actions.append(edit,del);card.appendChild(actions);
|
|
180
|
+
var runBox=document.createElement('div');runBox.className='task-run';var runHead=document.createElement('div');runHead.className='task-run-head';var status=document.createElement('span');status.className='run-status '+(run?run.state:'');status.textContent=run?runLabel(run)+(run.attempt>1?' · 第 '+run.attempt+' 次':''):'尚未执行';var runButton=document.createElement('button');runButton.type='button';runButton.className='run-button '+(!run||!active?'primary':'danger');runButton.textContent=active?'停止':(run?'重试':'交给主 Agent');runButton.addEventListener('click',function(event){event.stopPropagation();if(active)stopTaskRun(run);else startTaskRun(task);});runHead.append(status,runButton);runBox.appendChild(runHead);if(run&&(run.result||run.error)){var resultText=document.createElement('p');resultText.className='run-result';resultText.textContent=run.error||run.result;runBox.appendChild(resultText);}card.appendChild(runBox);
|
|
181
|
+
card.addEventListener('click',function(){if(!active&&!dragged)showEdit(task);});card.addEventListener('dragstart',function(event){if(active){event.preventDefault();return;}dragged=true;state.dragTaskId=task.id;card.classList.add('dragging');event.dataTransfer.effectAllowed='move';});card.addEventListener('dragend',function(){state.dragTaskId=null;card.classList.remove('dragging');document.querySelectorAll('.column').forEach(function(el){el.classList.remove('drag-over');});setTimeout(function(){dragged=false;},0);});list.appendChild(card);
|
|
171
182
|
});
|
|
172
183
|
});
|
|
173
184
|
}
|
|
@@ -176,12 +187,12 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
176
187
|
state.agentOptions.forEach(function(agent){var option=document.createElement('option');option.value=agent.id;option.textContent=agent.label;mainAgentSelect.appendChild(option);});
|
|
177
188
|
mainAgentSelect.value=selected;mainAgentSelect.disabled=!state.board;mainAgentButton.disabled=!state.board||!mainAgentSelect.value;
|
|
178
189
|
var status=document.getElementById('main-agent-status');var binding=state.binding;
|
|
179
|
-
if(!state.board){status.textContent='请先打开一个项目';status.className='agent-status';mainAgentButton.textContent='
|
|
180
|
-
if(!selected){status.textContent='尚未设置;设置后将创建“主Agent-'+state.board.workspacePath
|
|
190
|
+
if(!state.board){status.textContent='请先打开一个项目';status.className='agent-status';mainAgentButton.textContent='设置为主 Agent';return;}
|
|
191
|
+
if(!selected){status.textContent='尚未设置;设置后将创建“主Agent-'+workspaceShortName(state.board.workspacePath)+'”群聊';status.className='agent-status';mainAgentButton.textContent='设置为主 Agent';return;}
|
|
181
192
|
if(binding&&binding.status==='ready'){status.textContent='主 Agent 群已就绪';status.className='agent-status ready';}
|
|
182
193
|
else if(binding&&binding.status==='error'){status.textContent='创建失败,可重新设置后重试';status.className='agent-status error';}
|
|
183
194
|
else{status.textContent='正在准备主 Agent 群';status.className='agent-status provisioning';}
|
|
184
|
-
mainAgentButton.textContent='
|
|
195
|
+
mainAgentButton.textContent='设置为主 Agent';
|
|
185
196
|
}
|
|
186
197
|
function closeDmModal(){document.getElementById('dm-modal').classList.add('hidden');if(state.contactTimer){clearInterval(state.contactTimer);state.contactTimer=null;}state.contactChecking=false;}
|
|
187
198
|
function showDmModal(){document.getElementById('dm-modal').classList.remove('hidden');document.getElementById('dm-detect-status').textContent='正在等待私聊消息…';if(state.contactTimer)clearInterval(state.contactTimer);state.contactTimer=setInterval(checkFeishuContact,1500);}
|
|
@@ -210,7 +221,7 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
|
|
|
210
221
|
document.getElementById('dm-cancel').addEventListener('click',function(){state.pendingAgentId=null;closeDmModal();});document.getElementById('dm-check').addEventListener('click',checkFeishuContact);
|
|
211
222
|
pathInput.addEventListener('change',function(){if(!state.relinkBoardId)selectWorkspacePath(pathInput.value);});pathInput.addEventListener('keydown',function(event){if(event.key==='Enter'){event.preventDefault();if(!state.relinkBoardId)selectWorkspacePath(pathInput.value);}});
|
|
212
223
|
document.getElementById('pick').addEventListener('click',openDirectoryModal);
|
|
213
|
-
recent.addEventListener('change',function(){var item=state.workspaces.find(function(candidate){return candidate.boardId===recent.value;});if(!item)return;pathInput.value=item.workspacePath;if(item.exists){state.relinkBoardId=null;relinkButton.classList.add('hidden');openWorkspace(item.workspacePath);}else{state.relinkBoardId=item.boardId;relinkButton.classList.remove('hidden');api('/api/agent-team/boards/'+encodeURIComponent(item.boardId)).then(function(result){state.board=result.board;state.binding=result.binding||null;renderBoard();toast('原目录不存在,请选择新目录后重新关联',true);}).catch(function(error){toast(error.message,true);});}});
|
|
224
|
+
recent.addEventListener('change',function(){var item=state.workspaces.find(function(candidate){return candidate.boardId===recent.value;});if(!item)return;pathInput.value=item.workspacePath;if(item.exists){state.relinkBoardId=null;relinkButton.classList.add('hidden');openWorkspace(item.workspacePath);}else{state.relinkBoardId=item.boardId;relinkButton.classList.remove('hidden');api('/api/agent-team/boards/'+encodeURIComponent(item.boardId)).then(async function(result){state.board=result.board;state.binding=result.binding||null;state.runs=[];await loadRuns(false);renderBoard();scheduleRunPoll();toast('原目录不存在,请选择新目录后重新关联',true);}).catch(function(error){toast(error.message,true);});}});
|
|
214
225
|
relinkButton.addEventListener('click',function(){if(!state.relinkBoardId||!state.board)return toast('请先打开或选择需要关联的看板',true);mutate(function(){return request('POST','/api/agent-team/boards/'+encodeURIComponent(state.relinkBoardId)+'/relink',{expectedRevision:state.board.revision,workspacePath:pathInput.value});}).then(function(saved){if(!saved)return;refreshWorkspaces(state.board.boardId);state.relinkBoardId=null;relinkButton.classList.add('hidden');});});
|
|
215
226
|
refreshWorkspaces().then(function(defaultWorkspace){pathInput.value=defaultWorkspace;return selectWorkspacePath(defaultWorkspace);}).catch(function(error){toast(error.message,true);});
|
|
216
227
|
})();
|
package/dist/src/config.js
CHANGED
|
@@ -318,6 +318,7 @@ function loadConfig() {
|
|
|
318
318
|
alternativeModel: "",
|
|
319
319
|
effort: "",
|
|
320
320
|
provider: "",
|
|
321
|
+
gitCoAuthor: null,
|
|
321
322
|
compactionTimeoutMs: DEFAULT_CCC_COMPACTION_TIMEOUT_MS,
|
|
322
323
|
contextWindow: DEFAULT_CCC_CONTEXT_WINDOW_TOKENS,
|
|
323
324
|
},
|
|
@@ -527,6 +528,7 @@ function loadConfig() {
|
|
|
527
528
|
alternativeModel: normalizeOptionalConfigField(cccRaw.alternativeModel, { label: "ccc.alternativeModel" }),
|
|
528
529
|
effort: normalizeOptionalConfigField(cccRaw.effort, { label: "ccc.effort" }),
|
|
529
530
|
provider: normalizeCccProviderOverride(cccRaw.provider),
|
|
531
|
+
gitCoAuthor: typeof cccRaw.gitCoAuthor === "boolean" ? cccRaw.gitCoAuthor : null,
|
|
530
532
|
compactionTimeoutMs: normalizePositiveInteger(cccRaw.compactionTimeoutMs, DEFAULT_CCC_COMPACTION_TIMEOUT_MS),
|
|
531
533
|
contextWindow: normalizePositiveInteger(cccRaw.contextWindow, DEFAULT_CCC_CONTEXT_WINDOW_TOKENS),
|
|
532
534
|
},
|
package/dist/src/orchestrator.js
CHANGED
|
@@ -9,6 +9,8 @@ import { readdir, stat } from "node:fs/promises";
|
|
|
9
9
|
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync } from "node:fs";
|
|
10
10
|
import { join, resolve, dirname } from "node:path";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
+
import { config as deepCccConfig } from "../deepccc-agent/src/config.js";
|
|
13
|
+
import { withGitCoAuthor } from "../deepccc-agent/src/file-tools.js";
|
|
12
14
|
import { makeTraceId, logTrace } from "./trace.js";
|
|
13
15
|
import { appendStartupTrace } from "./shared.js";
|
|
14
16
|
import { CLAUDE_MODEL, GIT_TIMEOUT_MS, PROJECT_ROOT, anthropicConfigDisplay, config, fileLog, getAllEffortsForTool, getAllModelsForTool, getDefaultEffortForTool, getDefaultCwd, LOG_DIR, setDefaultCwd, getRecentDirs, addRecentDir, resolveDefaultAgentTool, sessionPrefixForTool, toolDisplayName, ts, } from "./config.js";
|
|
@@ -25,7 +27,7 @@ import { reloadRuntimeConfig } from "./runtime-reload.js";
|
|
|
25
27
|
import { acquireUpdateCommandGuard } from "./update-command-guard.js";
|
|
26
28
|
import { createInternalRestartEnv } from "./startup-lifecycle.js";
|
|
27
29
|
import { resolveChatCccRuntimeSpawnSpec } from "./runtime-entry.js";
|
|
28
|
-
import { feishuP2pContactStore } from "./agent-team/repositories/feishu-p2p-contact-store.js";
|
|
30
|
+
import { feishuP2pContactStore, isValidFeishuOpenId, } from "./agent-team/repositories/feishu-p2p-contact-store.js";
|
|
29
31
|
// ---------------------------------------------------------------------------
|
|
30
32
|
// 辅助函数
|
|
31
33
|
// ---------------------------------------------------------------------------
|
|
@@ -780,7 +782,7 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
780
782
|
const textLower = text.toLowerCase();
|
|
781
783
|
const isCommandText = !sharedPrefix.matched && textLower.startsWith("/");
|
|
782
784
|
recordChatPlatform(chatId, platform);
|
|
783
|
-
if (platform.kind === "feishu" && chatType === "p2p" && openId) {
|
|
785
|
+
if (platform.kind === "feishu" && chatType === "p2p" && isValidFeishuOpenId(openId)) {
|
|
784
786
|
const receivedAt = Number.isFinite(msgTimestamp) ? new Date(msgTimestamp).toISOString() : new Date().toISOString();
|
|
785
787
|
await feishuP2pContactStore.record({ openId, chatId, receivedAt }).catch((err) => {
|
|
786
788
|
console.error(`[${ts()}] [AGENT-TEAM] Failed to remember Feishu private contact: ${err.message}`);
|
|
@@ -1831,7 +1833,13 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
1831
1833
|
return;
|
|
1832
1834
|
}
|
|
1833
1835
|
console.log(`[${ts()}] [GIT] chat=${chatId} cwd=${cwd} cmd="git ${args}" timeoutMs=${GIT_TIMEOUT_MS}`);
|
|
1834
|
-
const
|
|
1836
|
+
const effectiveGitArgs = descriptionTool === "ccc"
|
|
1837
|
+
? withGitCoAuthor(`git ${args}`, {
|
|
1838
|
+
...deepCccConfig.git.coAuthor,
|
|
1839
|
+
enabled: config.ccc.gitCoAuthor ?? deepCccConfig.git.coAuthor.enabled,
|
|
1840
|
+
}).slice(4)
|
|
1841
|
+
: args;
|
|
1842
|
+
const result = await runGitCommand(effectiveGitArgs, cwd, {
|
|
1835
1843
|
timeoutMs: GIT_TIMEOUT_MS,
|
|
1836
1844
|
});
|
|
1837
1845
|
console.log(`[${ts()}] [GIT] exitCode=${result.exitCode}, durationMs=${result.durationMs}, truncated=${result.truncated}, timedOut=${result.timedOut}`);
|
package/dist/src/session.js
CHANGED
|
@@ -537,6 +537,7 @@ export function getAdapterForTool(tool, sessionId) {
|
|
|
537
537
|
// 留空("")不传 → ChatSession 跟随 DeepCCC 内核配置(~/.deepccc/config.json 或 DEEPCCC_PROVIDER)
|
|
538
538
|
...(config.ccc.provider ? { provider: config.ccc.provider } : {}),
|
|
539
539
|
...(config.ccc.subModel ? { subModel: config.ccc.subModel } : {}),
|
|
540
|
+
...(config.ccc.gitCoAuthor !== null ? { gitCoAuthor: config.ccc.gitCoAuthor } : {}),
|
|
540
541
|
});
|
|
541
542
|
}
|
|
542
543
|
else if (tool === "dsh") {
|
package/dist/src/web-ui.js
CHANGED
|
@@ -499,6 +499,10 @@ export function unflattenConfig(flat) {
|
|
|
499
499
|
result.ccc = result.ccc || {};
|
|
500
500
|
result.ccc.provider = val;
|
|
501
501
|
}
|
|
502
|
+
else if (key === "CHATCCC_CCC_GIT_COAUTHOR") {
|
|
503
|
+
result.ccc = result.ccc || {};
|
|
504
|
+
result.ccc.gitCoAuthor = val === "inherit" ? null : val === "enabled";
|
|
505
|
+
}
|
|
502
506
|
else if (key === "CHATCCC_CCC_CONTEXT_WINDOW") {
|
|
503
507
|
result.ccc = result.ccc || {};
|
|
504
508
|
result.ccc.contextWindow = parseInt(String(val), 10) || 1048576;
|
|
@@ -913,6 +917,15 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
913
917
|
</select>
|
|
914
918
|
<div class="hint">与 Base URL 强相关:OpenAI 兼容端点选 openai;Anthropic Messages 端点选 anthropic。留空时使用 ~/.deepccc/config.json 或 DEEPCCC_PROVIDER 的值。</div>
|
|
915
919
|
</div>
|
|
920
|
+
<div class="form-group">
|
|
921
|
+
<label>Git 提交共同作者</label>
|
|
922
|
+
<select id="field-CHATCCC_CCC_GIT_COAUTHOR">
|
|
923
|
+
<option value="inherit">跟随 DeepCCC 全局设置(默认开启)</option>
|
|
924
|
+
<option value="enabled">强制开启</option>
|
|
925
|
+
<option value="disabled">强制关闭</option>
|
|
926
|
+
</select>
|
|
927
|
+
<div class="hint">开启时追加 DeepCCC 共同作者,不替换你的 Git Author。</div>
|
|
928
|
+
</div>
|
|
916
929
|
<div class="form-group">
|
|
917
930
|
<label>Effort(推理强度,选填)</label>
|
|
918
931
|
<select id="field-CHATCCC_CCC_EFFORT">
|
|
@@ -1261,6 +1274,7 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
1261
1274
|
<div class="config-row"><span class="key">模型</span><span class="val" id="cfg-CCC_MODEL">-</span></div>
|
|
1262
1275
|
<div class="config-row"><span class="key">子模型</span><span class="val" id="cfg-CCC_SUB_MODEL">-</span></div>
|
|
1263
1276
|
<div class="config-row"><span class="key">备选模型</span><span class="val" id="cfg-CCC_ALTERNATIVE_MODEL">-</span></div>
|
|
1277
|
+
<div class="config-row"><span class="key">Git 共同作者</span><span class="val" id="cfg-CCC_GIT_COAUTHOR">-</span></div>
|
|
1264
1278
|
<label class="agent-default-row" style="margin-top:10px"><input type="checkbox" id="dash-default-ccc" onchange="setDashboardDefaultAgent('ccc', this.checked)"> 设为默认 Agent</label>
|
|
1265
1279
|
<div class="hint" style="margin-top:6px;line-height:1.6">备选模型仅加入 /model 人工切换列表;保存后下一条消息或下个新会话生效。</div>
|
|
1266
1280
|
<button class="btn btn-outline" style="margin-top:8px" onclick="editSection('ccc')">编辑</button>
|
|
@@ -1311,7 +1325,7 @@ const AGENT_FIELDS = {
|
|
|
1311
1325
|
claude: ['CHATCCC_ANTHROPIC_MODEL','CHATCCC_ANTHROPIC_SUBAGENT_MODEL','CHATCCC_ANTHROPIC_EFFORT','CHATCCC_ANTHROPIC_API_KEY','CHATCCC_ANTHROPIC_BASE_URL','CHATCCC_ANTHROPIC_MAX_TURN'],
|
|
1312
1326
|
cursor: ['CHATCCC_CURSOR_PATH','CHATCCC_CURSOR_MODEL','CHATCCC_CURSOR_ALTERNATIVE_MODEL','CHATCCC_CURSOR_AVATAR_BATTERY_MODE','CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET'],
|
|
1313
1327
|
codex: ['CHATCCC_CODEX_PATH','CHATCCC_CODEX_MODEL','CHATCCC_CODEX_ALTERNATIVE_MODEL','CHATCCC_CODEX_EFFORT','CHATCCC_CODEX_FAST_MODE'],
|
|
1314
|
-
ccc: ['CHATCCC_CCC_API_KEY','CHATCCC_CCC_BASE_URL','CHATCCC_CCC_MODEL','CHATCCC_CCC_SUB_MODEL','CHATCCC_CCC_ALTERNATIVE_MODEL','CHATCCC_CCC_EFFORT','CHATCCC_CCC_PROVIDER','CHATCCC_CCC_CONTEXT_WINDOW'],
|
|
1328
|
+
ccc: ['CHATCCC_CCC_API_KEY','CHATCCC_CCC_BASE_URL','CHATCCC_CCC_MODEL','CHATCCC_CCC_SUB_MODEL','CHATCCC_CCC_ALTERNATIVE_MODEL','CHATCCC_CCC_EFFORT','CHATCCC_CCC_PROVIDER','CHATCCC_CCC_GIT_COAUTHOR','CHATCCC_CCC_CONTEXT_WINDOW'],
|
|
1315
1329
|
dsh: ['CHATCCC_DSH_API_KEY','CHATCCC_DSH_BASE_URL','CHATCCC_DSH_MODEL','CHATCCC_DSH_SUB_MODEL','CHATCCC_DSH_ALTERNATIVE_MODEL','CHATCCC_DSH_PROVIDER','CHATCCC_DSH_MAX_TOKENS']
|
|
1316
1330
|
};
|
|
1317
1331
|
const FEISHU_FIELDS = ['CHATCCC_APP_ID','CHATCCC_APP_SECRET'];
|
|
@@ -1703,6 +1717,8 @@ function renderStep2() {
|
|
|
1703
1717
|
prefillNested('field-CHATCCC_CCC_API_KEY', c.ccc.DEEPSEEK_API_KEY);
|
|
1704
1718
|
prefillNested('field-CHATCCC_CCC_BASE_URL', c.ccc.DEEPSEEK_BASE_URL);
|
|
1705
1719
|
prefillNested('field-CHATCCC_CCC_PROVIDER', c.ccc.provider);
|
|
1720
|
+
var coAuthorField = document.getElementById('field-CHATCCC_CCC_GIT_COAUTHOR');
|
|
1721
|
+
if (coAuthorField) coAuthorField.value = c.ccc.gitCoAuthor == null ? 'inherit' : (c.ccc.gitCoAuthor ? 'enabled' : 'disabled');
|
|
1706
1722
|
prefillNested('field-CHATCCC_CCC_MODEL', c.ccc.model);
|
|
1707
1723
|
prefillNested('field-CHATCCC_CCC_SUB_MODEL', c.ccc.subModel);
|
|
1708
1724
|
prefillNested('field-CHATCCC_CCC_ALTERNATIVE_MODEL', c.ccc.alternativeModel);
|
|
@@ -2146,6 +2162,9 @@ function updateDashboardUI() {
|
|
|
2146
2162
|
document.getElementById('cfg-CCC_MODEL').textContent = (c.ccc && c.ccc.model) || '(留空)';
|
|
2147
2163
|
document.getElementById('cfg-CCC_SUB_MODEL').textContent = (c.ccc && c.ccc.subModel) || '(留空,跟随主模型)';
|
|
2148
2164
|
document.getElementById('cfg-CCC_ALTERNATIVE_MODEL').textContent = (c.ccc && c.ccc.alternativeModel) || '(留空)';
|
|
2165
|
+
document.getElementById('cfg-CCC_GIT_COAUTHOR').textContent = !c.ccc || c.ccc.gitCoAuthor == null
|
|
2166
|
+
? '跟随 DeepCCC 全局设置(缺省开启)'
|
|
2167
|
+
: (c.ccc.gitCoAuthor ? '强制开启' : '强制关闭');
|
|
2149
2168
|
document.getElementById('cfg-DSH_API_KEY').textContent = (c.dsh && c.dsh.apiKey) ? '***已设置***' : '(留空)';
|
|
2150
2169
|
document.getElementById('cfg-DSH_BASE_URL').textContent = (c.dsh && c.dsh.baseUrl) || 'https://api.deepseek.com/v1';
|
|
2151
2170
|
document.getElementById('cfg-DSH_MODEL').textContent = (c.dsh && c.dsh.model) || 'deepseek-v4-flash';
|
|
@@ -2230,6 +2249,7 @@ function editSection(section) {
|
|
|
2230
2249
|
'CHATCCC_CODEX_FAST_MODE': 'Fast 模式',
|
|
2231
2250
|
'CHATCCC_CCC_API_KEY': 'API Key', 'CHATCCC_CCC_BASE_URL': 'Base URL',
|
|
2232
2251
|
'CHATCCC_CCC_PROVIDER': 'API 协议(选填)',
|
|
2252
|
+
'CHATCCC_CCC_GIT_COAUTHOR': 'Git 提交共同作者',
|
|
2233
2253
|
'CHATCCC_CCC_MODEL': '模型', 'CHATCCC_CCC_SUB_MODEL': '子模型', 'CHATCCC_CCC_ALTERNATIVE_MODEL': '备选模型', 'CHATCCC_CCC_EFFORT': 'Effort', 'CHATCCC_CCC_CONTEXT_WINDOW': '上下文窗口',
|
|
2234
2254
|
'CHATCCC_DSH_API_KEY': 'API Key', 'CHATCCC_DSH_BASE_URL': 'Base URL', 'CHATCCC_DSH_MODEL': '模型', 'CHATCCC_DSH_SUB_MODEL': '子模型', 'CHATCCC_DSH_ALTERNATIVE_MODEL': '备选模型', 'CHATCCC_DSH_PROVIDER': 'Provider 路由', 'CHATCCC_DSH_MAX_TOKENS': '单次最大输出 Tokens'
|
|
2235
2255
|
};
|
|
@@ -2239,6 +2259,7 @@ function editSection(section) {
|
|
|
2239
2259
|
'CHATCCC_CHROME_DEVTOOLS_PORT': '默认 15166,健康检查端点为 http://127.0.0.1:15166/json/version。',
|
|
2240
2260
|
'CHATCCC_CHROME_DEVTOOLS_PATH': '选填。留空时自动探测 Google Chrome。',
|
|
2241
2261
|
'CHATCCC_CCC_PROVIDER': '与 Base URL 强相关:OpenAI 兼容端点选 openai;Anthropic Messages 端点选 anthropic。留空 = 跟随 DeepCCC 内核配置(~/.deepccc/config.json 或 DEEPCCC_PROVIDER),改动需重启 ChatCCC 生效。',
|
|
2262
|
+
'CHATCCC_CCC_GIT_COAUTHOR': '跟随全局时读取 ~/.deepccc/config.json 的 git.coAuthor.enabled(缺省为开启)。强制选项只影响 ChatCCC 内置 CCC Agent。',
|
|
2242
2263
|
'CHATCCC_CCC_CONTEXT_WINDOW': '压缩阈值自动 = 窗口 × 80%(超出即把较早消息压缩为摘要)。⚠️ 超过模型/服务端实际上限时请求会被 API 直接拒绝(context length exceeded),实际窗口以模型与所用服务端为准(如 litellm 代理的 max_input_tokens);单位 k = 1024 tokens,1M = 1,048,576 tokens。',
|
|
2243
2264
|
'CHATCCC_CCC_SUB_MODEL': '用于 DeepCCC 内部轻量环节(上下文压缩摘要生成、task 子代理任务)。留空 = 跟随主模型;改动需重启 ChatCCC 生效。'
|
|
2244
2265
|
};
|
|
@@ -2283,6 +2304,7 @@ function editSection(section) {
|
|
|
2283
2304
|
if (key === 'CHATCCC_CCC_API_KEY') val = state.config.ccc.DEEPSEEK_API_KEY || '';
|
|
2284
2305
|
else if (key === 'CHATCCC_CCC_BASE_URL') val = state.config.ccc.DEEPSEEK_BASE_URL || '';
|
|
2285
2306
|
else if (key === 'CHATCCC_CCC_PROVIDER') val = state.config.ccc.provider || '';
|
|
2307
|
+
else if (key === 'CHATCCC_CCC_GIT_COAUTHOR') val = state.config.ccc.gitCoAuthor == null ? 'inherit' : (state.config.ccc.gitCoAuthor ? 'enabled' : 'disabled');
|
|
2286
2308
|
else if (key === 'CHATCCC_CCC_MODEL') val = state.config.ccc.model || '';
|
|
2287
2309
|
else if (key === 'CHATCCC_CCC_SUB_MODEL') val = state.config.ccc.subModel || '';
|
|
2288
2310
|
else if (key === 'CHATCCC_CCC_ALTERNATIVE_MODEL') val = state.config.ccc.alternativeModel || '';
|
|
@@ -2312,6 +2334,15 @@ function editSection(section) {
|
|
|
2312
2334
|
html += '<option value="apiPercent"' + (modeVal === 'apiPercent' ? ' selected' : '') + '>API 使用比例</option>';
|
|
2313
2335
|
html += '<option value="onDemandUse"' + (modeVal === 'onDemandUse' ? ' selected' : '') + '>On demand use 金额</option>';
|
|
2314
2336
|
html += '</select></div>';
|
|
2337
|
+
} else if (key === 'CHATCCC_CCC_GIT_COAUTHOR') {
|
|
2338
|
+
var coAuthorVal = val || 'inherit';
|
|
2339
|
+
html += '<div class="form-group"><label>' + (labelMap[key] || key) + '</label>';
|
|
2340
|
+
html += '<select id="edit-' + key + '" style="width:100%;padding:8px 12px;border:1px solid #cbd5e1;border-radius:8px;font-size:14px;outline:none">';
|
|
2341
|
+
html += '<option value="inherit"' + (coAuthorVal === 'inherit' ? ' selected' : '') + '>跟随 DeepCCC 全局设置(默认开启)</option>';
|
|
2342
|
+
html += '<option value="enabled"' + (coAuthorVal === 'enabled' ? ' selected' : '') + '>强制开启</option>';
|
|
2343
|
+
html += '<option value="disabled"' + (coAuthorVal === 'disabled' ? ' selected' : '') + '>强制关闭</option></select>';
|
|
2344
|
+
if (hintMap[key]) html += '<div class="hint" style="margin-top:6px;line-height:1.5">' + hintMap[key] + '</div>';
|
|
2345
|
+
html += '</div>';
|
|
2315
2346
|
} else if (key === 'CHATCCC_CCC_PROVIDER') {
|
|
2316
2347
|
var providerVal = val || '';
|
|
2317
2348
|
html += '<div class="form-group"><label>' + (labelMap[key] || key) + '</label>';
|