chatccc 0.2.259 → 0.2.260
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 +20 -11
- package/config.sample.json +23 -8
- package/dist/src/adapters/claude-adapter.js +3 -2
- package/dist/src/adapters/dsh-adapter.js +230 -0
- package/dist/src/agent-tool.js +2 -1
- package/dist/src/card-action-parser.js +1 -0
- package/dist/src/cards.js +11 -3
- package/dist/src/config.js +34 -2
- package/dist/src/engines/engine-manager.js +408 -0
- package/dist/src/engines/engine-specs.js +157 -0
- package/dist/src/feishu-api.js +2 -1
- package/dist/src/orchestrator.js +21 -10
- package/dist/src/session.js +23 -0
- package/dist/src/web-ui.js +286 -200
- package/package.json +1 -1
- package/dist/src/claude-sdk-installer.js +0 -249
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# ChatCCC
|
|
2
2
|
|
|
3
|
-
**用飞书或微信聊天控制 Claude Code / Cursor / Codex / CCC Agent。**
|
|
3
|
+
**用飞书或微信聊天控制 Claude Code / Cursor / Codex / CCC Agent / DeepSeek Harness。**
|
|
4
4
|
|
|
5
|
-
ChatCCC 把本地 AI 编程工具接入即时通讯软件。你可以在手机上发消息,让 Claude Code、Cursor Agent、Codex
|
|
5
|
+
ChatCCC 把本地 AI 编程工具接入即时通讯软件。你可以在手机上发消息,让 Claude Code、Cursor Agent、Codex、内置 CCC Agent 或 DeepSeek Harness 继续写代码、查问题、跑命令;不用一直守在电脑前。
|
|
6
6
|
|
|
7
7
|
飞书是推荐入口:直接私聊机器人即可持续使用同一个专属会话,需要并行任务时再用 `/new` 创建独立会话群;卡片能流式更新,体验完整。微信 iLink 更适合快速试用或临时使用:扫码即可接入,但只能走私聊文本模式。
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ ChatCCC 把本地 AI 编程工具接入即时通讯软件。你可以在手机
|
|
|
21
21
|
- **手机上也能用 AI 编程工具**:在飞书或微信发消息,就像在终端给 Agent 下指令。
|
|
22
22
|
- **飞书体验更完整**:私聊可持续对话,`/new` 创建的一群一会话支持多任务并行,CardKit 卡片可流式更新。
|
|
23
23
|
- **微信接入更轻**:不用创建飞书应用,启动后扫码即可在微信私聊里使用。
|
|
24
|
-
- **多 Agent 切换**:`/new` 使用默认 Agent,也可以用 `/new claude`、`/new cursor`、`/new codex`、`/new ccc` 指定工具。
|
|
24
|
+
- **多 Agent 切换**:`/new` 使用默认 Agent,也可以用 `/new claude`、`/new cursor`、`/new codex`、`/new ccc`、`/new dsh` 指定工具。
|
|
25
25
|
- **群里能跑 git**:`/git status`、`/git pull`、`/git log` 会在当前会话工作目录执行,并把输出发回聊天窗口。
|
|
26
26
|
|
|
27
27
|
## 飞书和微信的差异
|
|
@@ -216,7 +216,7 @@ chatccc
|
|
|
216
216
|
|
|
217
217
|
### 3. AI 工具配置
|
|
218
218
|
|
|
219
|
-
Claude Code、Cursor 和 Codex 需要对应的本地工具;CCC Agent 内置于 ChatCCC,开箱即用,**模型接入不限于 DeepSeek**——它支持 OpenAI-compatible 和 Anthropic Messages 两种协议(详见下文 CCC Agent)。
|
|
219
|
+
Claude Code、Cursor 和 Codex 需要对应的本地工具;CCC Agent 内置于 ChatCCC,开箱即用,**模型接入不限于 DeepSeek**——它支持 OpenAI-compatible 和 Anthropic Messages 两种协议(详见下文 CCC Agent)。DeepSeek Harness(DSH)采用按需安装,不计入 ChatCCC 主包生产依赖。
|
|
220
220
|
|
|
221
221
|
#### CCC Agent
|
|
222
222
|
|
|
@@ -242,9 +242,15 @@ ChatCCC 会把 `ccc.DEEPSEEK_API_KEY`、`ccc.DEEPSEEK_BASE_URL`、模型和 effo
|
|
|
242
242
|
|
|
243
243
|
#### Claude Code
|
|
244
244
|
|
|
245
|
-
ChatCCC 通过 Anthropic Claude Agent SDK 调用 Claude Code 能力。SDK 引擎(含 Claude Code CLI 原生二进制,约 220MB)**按需下载**:在首次配置向导或 Web
|
|
245
|
+
ChatCCC 通过 Anthropic Claude Agent SDK 调用 Claude Code 能力。SDK 引擎(含 Claude Code CLI 原生二进制,约 220MB)**按需下载**:在首次配置向导或 Web 管理页打开 Claude Code 开关,或点击一次安装按钮,页面会连续显示环境检查、下载、校验、Runtime 验证、原子切换和清理进度,不需要逐步确认。**只有启用 Claude Code 的用户才需要下载**,不使用 Claude Code 的安装不包含该引擎,chatccc 主包体积保持精简。
|
|
246
246
|
|
|
247
|
-
SDK 始终使用其内置的 Claude Code CLI(档位 b),不依赖用户自行安装的 `claude` 命令。本机完成 Claude Code 登录后即可使用(SDK 复用 `~/.claude` 登录态);同时支持官方和第三方 Anthropic 兼容 API:使用官方服务无需额外配置,使用第三方 API 则填写 `claude.apiKey` 和 `claude.baseUrl`。`claude.model`、`claude.subagentModel`、`claude.effort`、`claude.apiKey`、`claude.baseUrl` 均为选填;`claude.maxTurn` 控制每次对话的最大轮数(默认 0,即无限制)。填写后会把对应配置传给 Claude Agent SDK;留空则以 `~/.claude/settings.json` 为准。
|
|
247
|
+
SDK 始终使用其内置的 Claude Code CLI(档位 b),不依赖用户自行安装的 `claude` 命令。本机完成 Claude Code 登录后即可使用(SDK 复用 `~/.claude` 登录态);同时支持官方和第三方 Anthropic 兼容 API:使用官方服务无需额外配置,使用第三方 API 则填写 `claude.apiKey` 和 `claude.baseUrl`。`claude.model`、`claude.subagentModel`、`claude.effort`、`claude.apiKey`、`claude.baseUrl` 均为选填;`claude.maxTurn` 控制每次对话的最大轮数(默认 0,即无限制)。填写后会把对应配置传给 Claude Agent SDK;留空则以 `~/.claude/settings.json` 为准。
|
|
248
|
+
|
|
249
|
+
#### DeepSeek Harness
|
|
250
|
+
|
|
251
|
+
DeepSeek Harness 是可选 Agent。在首次配置向导或 Web 管理页打开 DSH 开关,ChatCCC 会一次点击完成环境检查、临时目录准备、精确版本下载、包校验、Runtime 握手、原子切换和旧版本清理,并在页面逐步显示进度。安装失败时不会替换当前可用版本;刷新页面后仍可恢复安装进度。当前固定安装 `0.1.0-rc.6`,要求 Node.js >= 22.19.0。
|
|
252
|
+
|
|
253
|
+
DSH 引擎安装在 `~/.chatccc/engines/dsh/`,不加入 ChatCCC 的 `dependencies`。配置 `dsh.apiKey`、`dsh.baseUrl`、`dsh.model`、`dsh.provider` 和 `dsh.maxTokens` 后,可设为默认 Agent 或使用 `/new dsh` 新建会话。安装验证只做本地 JSON-RPC Runtime 握手,不会调用付费模型。
|
|
248
254
|
|
|
249
255
|
#### Cursor Agent CLI
|
|
250
256
|
|
|
@@ -374,13 +380,15 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
374
380
|
| `ccc.model` | CCC Agent 默认模型 |
|
|
375
381
|
| `ccc.subModel` | CCC Agent 子模型(选填):用于内部轻量环节(压缩摘要生成、task 子代理任务);留空跟随主模型 |
|
|
376
382
|
| `ccc.compactionTimeoutMs` | CCC Agent 上下文压缩单轮超时(毫秒),默认 300000(5 分钟);压缩超时会让整轮对话失败,建议保持默认或调大 |
|
|
377
|
-
| `ccc.contextWindow` | CCC Agent 模型上下文窗口(token),默认 1048576(1M,DeepSeek V4 Pro/Flash 原生规格);压缩阈值自动 = 窗口 × 80%;超过模型/服务端实际上限会被 API 拒绝,可在 Web UI 下拉选择或自定义(单位 k) |
|
|
383
|
+
| `ccc.contextWindow` | CCC Agent 模型上下文窗口(token),默认 1048576(1M,DeepSeek V4 Pro/Flash 原生规格);压缩阈值自动 = 窗口 × 80%;超过模型/服务端实际上限会被 API 拒绝,可在 Web UI 下拉选择或自定义(单位 k) |
|
|
384
|
+
| `dsh.apiKey` / `dsh.baseUrl` | DeepSeek Harness 的 API Key 和官方 DeepSeek 服务地址 |
|
|
385
|
+
| `dsh.model` / `dsh.provider` / `dsh.maxTokens` | DSH 默认模型、Runtime provider 和最大 token 数 |
|
|
378
386
|
|
|
379
387
|
> **权限控制**:普通消息以 `bypassPermissions` 模式运行,跳过 Agent 操作确认。使用 `/plan` 或 `/ask` 前缀时,ChatCCC 自动切换为只读模式:Claude SDK 仅放行 Read + stop-stuck-loop 网络请求,Codex 使用 `--sandbox read-only`,Cursor 使用 `--mode plan/ask`。请只在可信环境中使用。
|
|
380
388
|
|
|
381
389
|
### 5. 开始使用
|
|
382
390
|
|
|
383
|
-
**飞书:** 找到机器人后直接发送普通消息,即可在当前私聊中创建并持续使用专属 AI 会话;私聊工作目录固定为运行 ChatCCC 的系统账号用户目录。默认 Agent 发生变化后,下一条私聊普通消息会触发切换并创建新的空会话;若旧 Agent 正在生成,该消息会先排队,待当前回复完成后再切换。命令不会触发自动切换。需要独立任务时,发送 `/new`、`/new claude`、`/new cursor`、`/new codex` 或 `/new
|
|
391
|
+
**飞书:** 找到机器人后直接发送普通消息,即可在当前私聊中创建并持续使用专属 AI 会话;私聊工作目录固定为运行 ChatCCC 的系统账号用户目录。默认 Agent 发生变化后,下一条私聊普通消息会触发切换并创建新的空会话;若旧 Agent 正在生成,该消息会先排队,待当前回复完成后再切换。命令不会触发自动切换。需要独立任务时,发送 `/new`、`/new claude`、`/new cursor`、`/new codex`、`/new ccc` 或 `/new dsh`,机器人会另外创建会话群。
|
|
384
392
|
|
|
385
393
|
**微信:** 扫码登录后,在机器人私聊里发送 `/new` 或指定 Agent 的 `/new ...` 命令即可开始。功能与飞书基本一致,但展示为纯文本。
|
|
386
394
|
|
|
@@ -396,7 +404,8 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
396
404
|
| `/new claude` | 创建 Claude Code 会话;飞书中会创建新群 |
|
|
397
405
|
| `/new cursor` | 创建 Cursor 会话;飞书中会创建新群 |
|
|
398
406
|
| `/new codex` | 创建 Codex 会话;飞书中会创建新群 |
|
|
399
|
-
| `/new ccc` | 创建内置 CCC Agent 会话;飞书中会创建新群 |
|
|
407
|
+
| `/new ccc` | 创建内置 CCC Agent 会话;飞书中会创建新群 |
|
|
408
|
+
| `/new dsh` | 创建 DeepSeek Harness 会话;飞书中会创建新群 |
|
|
400
409
|
| `/forget` | 在当前聊天原地重置会话(忘掉上下文);群聊保留工作目录,飞书私聊固定使用系统用户目录 |
|
|
401
410
|
| `/model` | 查看或切换当前会话的模型 |
|
|
402
411
|
| `/fast` | 查看当前 Codex 会话的 Fast 模式;使用 `/fast on` 或 `/fast off` 切换 |
|
|
@@ -406,7 +415,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
406
415
|
| `/cd` | 查看或设置后续新建会话的默认工作目录,不改变当前会话;飞书私聊自身始终使用系统用户目录 |
|
|
407
416
|
| `/sessions` | 查看所有会话状态 |
|
|
408
417
|
| `/session <数字>` | 将当前群聊切换到 `/sessions` 列表中的指定会话;飞书私聊不支持切换 |
|
|
409
|
-
| `/usage` | 查看当前会话对应 Agent 的用量;Codex 显示 5h/7天窗口,Cursor 显示当前周期用量,CCC Agent 仅在官方 DeepSeek 端点时显示账户余额(其他兼容端点自动跳过) |
|
|
418
|
+
| `/usage` | 查看当前会话对应 Agent 的用量;Codex 显示 5h/7天窗口,Cursor 显示当前周期用量,CCC Agent 和 DSH 仅在官方 DeepSeek 端点时显示账户余额(其他兼容端点自动跳过) |
|
|
410
419
|
| `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
|
|
411
420
|
| `/abd<内容>` | 去掉 `/abd` 前缀后把内容发给 Agent,并在消息末尾追加第一性原理需求澄清提示 |
|
|
412
421
|
| `/plan <内容>` | 只读计划模式:仅允许读文件和 stop-stuck-loop 请求,不执行任何写操作 |
|
|
@@ -425,4 +434,4 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
425
434
|
|
|
426
435
|
## 技术栈
|
|
427
436
|
|
|
428
|
-
TypeScript(发布前编译为 JavaScript,tsx 仅用于开发)/ Node.js >= 20 / AI SDK / Anthropic Claude Agent SDK
|
|
437
|
+
TypeScript(发布前编译为 JavaScript,tsx 仅用于开发)/ Node.js >= 20 / AI SDK / Anthropic Claude Agent SDK(按需下载)/ DeepSeek Harness SDK + JSON-RPC Runtime(按需下载,要求 Node.js >= 22.19.0)/ Cursor Agent CLI / Codex CLI / 飞书 WebSocket API / CardKit / 微信 iLink
|
package/config.sample.json
CHANGED
|
@@ -37,14 +37,20 @@
|
|
|
37
37
|
"retentionDays": 7,
|
|
38
38
|
"keepCompleted": false
|
|
39
39
|
},
|
|
40
|
-
"ccc": {
|
|
41
|
-
"enabled": false,
|
|
42
|
-
"maxBytesPerTurn": 52428800,
|
|
43
|
-
"retentionDays": 7,
|
|
44
|
-
"keepCompleted": false
|
|
40
|
+
"ccc": {
|
|
41
|
+
"enabled": false,
|
|
42
|
+
"maxBytesPerTurn": 52428800,
|
|
43
|
+
"retentionDays": 7,
|
|
44
|
+
"keepCompleted": false
|
|
45
|
+
},
|
|
46
|
+
"dsh": {
|
|
47
|
+
"enabled": false,
|
|
48
|
+
"maxBytesPerTurn": 52428800,
|
|
49
|
+
"retentionDays": 7,
|
|
50
|
+
"keepCompleted": false
|
|
45
51
|
}
|
|
46
52
|
},
|
|
47
|
-
"ccc": {
|
|
53
|
+
"ccc": {
|
|
48
54
|
"enabled": false,
|
|
49
55
|
"defaultAgent": false,
|
|
50
56
|
"DEEPSEEK_API_KEY": "",
|
|
@@ -55,8 +61,17 @@
|
|
|
55
61
|
"effort": "",
|
|
56
62
|
"provider": "",
|
|
57
63
|
"compactionTimeoutMs": 300000,
|
|
58
|
-
"contextWindow": 1048576
|
|
59
|
-
},
|
|
64
|
+
"contextWindow": 1048576
|
|
65
|
+
},
|
|
66
|
+
"dsh": {
|
|
67
|
+
"enabled": false,
|
|
68
|
+
"defaultAgent": false,
|
|
69
|
+
"apiKey": "",
|
|
70
|
+
"baseUrl": "https://api.deepseek.com/v1",
|
|
71
|
+
"model": "deepseek-v4-flash",
|
|
72
|
+
"provider": "deepseek-official",
|
|
73
|
+
"maxTokens": 49152
|
|
74
|
+
},
|
|
60
75
|
"claude": {
|
|
61
76
|
"enabled": false,
|
|
62
77
|
"defaultAgent": true,
|
|
@@ -14,7 +14,7 @@ import { parseUserCommand } from "./adapter-interface.js";
|
|
|
14
14
|
import { CHATCCC_PORT, config, PROJECT_ROOT, RAW_STREAM_LOGS_DIR } from "../config.js";
|
|
15
15
|
import { defaultClaudeSessionMetaStore, } from "./claude-session-meta-store.js";
|
|
16
16
|
import { createRawStreamLog, } from "./raw-stream-log.js";
|
|
17
|
-
import {
|
|
17
|
+
import { engineManager } from "../engines/engine-specs.js";
|
|
18
18
|
const CLAUDE_SPECIFIC_PROMPT_PATH = join(PROJECT_ROOT, "agent-prompts", "claude_specific.md");
|
|
19
19
|
export function buildSdkEnv(subagentModel, apiKey, baseUrl) {
|
|
20
20
|
const subagentModelTrim = (subagentModel ?? "").trim();
|
|
@@ -230,7 +230,8 @@ let sdkModulePromise = null;
|
|
|
230
230
|
*/
|
|
231
231
|
export function loadClaudeSdkModule() {
|
|
232
232
|
if (!sdkModulePromise) {
|
|
233
|
-
sdkModulePromise =
|
|
233
|
+
sdkModulePromise = engineManager.getEntryPath("claude")
|
|
234
|
+
.then((entryPath) => import(__rewriteRelativeImportExtension(pathToFileURL(entryPath).href)))
|
|
234
235
|
.then((m) => m)
|
|
235
236
|
.catch((err) => {
|
|
236
237
|
sdkModulePromise = null;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { mkdir } from "node:fs/promises";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
|
+
import { pathToFileURL } from "node:url";
|
|
14
|
+
import { engineManager } from "../engines/engine-specs.js";
|
|
15
|
+
import { config, RAW_STREAM_LOGS_DIR } from "../config.js";
|
|
16
|
+
import { createRawStreamLog } from "./raw-stream-log.js";
|
|
17
|
+
let injectedSdk = null;
|
|
18
|
+
const knownSessions = new Map();
|
|
19
|
+
export function __setDshSdkModuleForTest(module) {
|
|
20
|
+
injectedSdk = module;
|
|
21
|
+
}
|
|
22
|
+
export function createDshAdapter(options = {}) {
|
|
23
|
+
return {
|
|
24
|
+
displayName: "DeepSeek Harness",
|
|
25
|
+
sessionDescPrefix: "DSH Session:",
|
|
26
|
+
responseStallDetectionEnabled: true,
|
|
27
|
+
async createSession(cwd) {
|
|
28
|
+
await engineManager.getEntryPath("dsh");
|
|
29
|
+
const sessionId = `dsh-${randomUUID()}`;
|
|
30
|
+
knownSessions.set(sessionId, { sessionId, cwd, lastModified: Date.now(), model: options.model ?? "deepseek-v4-flash" });
|
|
31
|
+
return { sessionId };
|
|
32
|
+
},
|
|
33
|
+
async *prompt(sessionId, userText, cwd, signal, promptOptions) {
|
|
34
|
+
const entryPath = await engineManager.getEntryPath("dsh");
|
|
35
|
+
const installationDir = resolve(dirname(entryPath), "../../../..");
|
|
36
|
+
const sdk = injectedSdk ?? await import(__rewriteRelativeImportExtension(pathToFileURL(entryPath).href));
|
|
37
|
+
const sessionRoot = join(homedir(), ".chatccc", "dsh-sessions");
|
|
38
|
+
await mkdir(sessionRoot, { recursive: true });
|
|
39
|
+
const runtime = new sdk.DeepSeekHarness({
|
|
40
|
+
launch: {
|
|
41
|
+
command: process.execPath,
|
|
42
|
+
args: [
|
|
43
|
+
join(installationDir, "node_modules", "@deepseek-ai", "dsh-sdk-jsonrpc-demo", "lib", "bin.js"),
|
|
44
|
+
join(installationDir, "dsh-runtime.cordis.yml"),
|
|
45
|
+
],
|
|
46
|
+
cwd,
|
|
47
|
+
env: {
|
|
48
|
+
...process.env,
|
|
49
|
+
DSH_CWD: cwd,
|
|
50
|
+
DSH_SESSION_ROOT: sessionRoot,
|
|
51
|
+
...(options.apiKey ? { DEEPSEEK_API_KEY: options.apiKey } : {}),
|
|
52
|
+
...(options.baseUrl ? { DEEPSEEK_BASE_URL: options.baseUrl } : {}),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
cwd,
|
|
56
|
+
provider: options.provider ?? "deepseek-official",
|
|
57
|
+
model: options.model ?? "deepseek-v4-flash",
|
|
58
|
+
...(options.maxTokens ? { maxTokens: options.maxTokens } : {}),
|
|
59
|
+
});
|
|
60
|
+
const queue = new AsyncMessageQueue();
|
|
61
|
+
const rawLogConfig = config.rawStreamLogs.dsh;
|
|
62
|
+
let rawLog = null;
|
|
63
|
+
try {
|
|
64
|
+
rawLog = await createRawStreamLog({
|
|
65
|
+
enabled: rawLogConfig.enabled,
|
|
66
|
+
rootDir: RAW_STREAM_LOGS_DIR,
|
|
67
|
+
tool: "dsh",
|
|
68
|
+
sessionId,
|
|
69
|
+
label: "prompt",
|
|
70
|
+
maxBytesPerTurn: rawLogConfig.maxBytesPerTurn,
|
|
71
|
+
retentionDays: rawLogConfig.retentionDays,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.error(`[DSH raw stream log] create failed: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
let closed = false;
|
|
78
|
+
let completed = false;
|
|
79
|
+
const closeRuntime = () => {
|
|
80
|
+
if (closed)
|
|
81
|
+
return;
|
|
82
|
+
closed = true;
|
|
83
|
+
void runtime.close();
|
|
84
|
+
};
|
|
85
|
+
promptOptions?.onSessionCreated?.(closeRuntime);
|
|
86
|
+
const onAbort = () => closeRuntime();
|
|
87
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
88
|
+
const task = (async () => {
|
|
89
|
+
try {
|
|
90
|
+
await runtime.start();
|
|
91
|
+
const result = await runtime.run(userText, {
|
|
92
|
+
sessionId,
|
|
93
|
+
onNotification: (notification) => {
|
|
94
|
+
rawLog?.writeLine(JSON.stringify(notification));
|
|
95
|
+
const message = notificationToMessage(notification);
|
|
96
|
+
if (message)
|
|
97
|
+
queue.push(message);
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
if (result.finalResponse) {
|
|
101
|
+
rawLog?.writeLine(JSON.stringify({ type: "run.result", result }));
|
|
102
|
+
queue.push({ type: "assistant", blocks: [{ type: "text_final", text: result.finalResponse }], isFinalResponse: true });
|
|
103
|
+
}
|
|
104
|
+
completed = true;
|
|
105
|
+
knownSessions.set(sessionId, { sessionId, cwd, lastModified: Date.now(), model: options.model ?? "deepseek-v4-flash" });
|
|
106
|
+
queue.end();
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
queue.fail(error instanceof Error ? error : new Error(String(error)));
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
signal?.removeEventListener("abort", onAbort);
|
|
113
|
+
await runtime.close().catch(() => { });
|
|
114
|
+
await rawLog?.close({ keep: rawLogConfig.keepCompleted || signal?.aborted === true || !completed });
|
|
115
|
+
closed = true;
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
118
|
+
try {
|
|
119
|
+
for await (const message of queue)
|
|
120
|
+
yield message;
|
|
121
|
+
await task;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
closeRuntime();
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
async getSessionInfo(sessionId) {
|
|
128
|
+
return knownSessions.get(sessionId) ?? { sessionId };
|
|
129
|
+
},
|
|
130
|
+
async closeSession() {
|
|
131
|
+
// Each prompt owns and closes its runtime. Durable state lives under ~/.chatccc/dsh-sessions.
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function notificationToMessage(notification) {
|
|
136
|
+
if (notification.method === "session.status") {
|
|
137
|
+
return notification.params.status === "running"
|
|
138
|
+
? { type: "system", blocks: [{ type: "agent_status", status: "responding" }] }
|
|
139
|
+
: null;
|
|
140
|
+
}
|
|
141
|
+
if (notification.method === "subagent.started") {
|
|
142
|
+
return { type: "system", blocks: [{ type: "agent_progress", phase: "reasoning" }] };
|
|
143
|
+
}
|
|
144
|
+
if (notification.method !== "session.event")
|
|
145
|
+
return null;
|
|
146
|
+
const event = asRecord(notification.params.event);
|
|
147
|
+
if (!event)
|
|
148
|
+
return null;
|
|
149
|
+
const data = asRecord(event.data);
|
|
150
|
+
if (!data)
|
|
151
|
+
return null;
|
|
152
|
+
const blocks = [];
|
|
153
|
+
if (event.type === "assistant/chunk") {
|
|
154
|
+
const chunk = asRecord(data.chunk);
|
|
155
|
+
if (chunk?.type === "text-delta" && typeof chunk.text === "string" && chunk.text) {
|
|
156
|
+
blocks.push({ type: "text", text: chunk.text });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if (event.type === "tool/call") {
|
|
160
|
+
const raw = typeof data.arguments === "string" ? data.arguments : "{}";
|
|
161
|
+
let input = raw;
|
|
162
|
+
try {
|
|
163
|
+
input = JSON.parse(raw);
|
|
164
|
+
}
|
|
165
|
+
catch { /* retain raw provider arguments */ }
|
|
166
|
+
blocks.push({
|
|
167
|
+
type: "tool_use",
|
|
168
|
+
...(typeof data.callId === "string" ? { id: data.callId } : {}),
|
|
169
|
+
name: typeof data.name === "string" ? data.name : "tool",
|
|
170
|
+
input,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else if (event.type === "tool/result") {
|
|
174
|
+
const message = asRecord(data.message);
|
|
175
|
+
const callId = typeof message?.toolCallId === "string"
|
|
176
|
+
? message.toolCallId
|
|
177
|
+
: typeof message?.callId === "string"
|
|
178
|
+
? message.callId
|
|
179
|
+
: "";
|
|
180
|
+
blocks.push({
|
|
181
|
+
type: "tool_result",
|
|
182
|
+
tool_use_id: callId,
|
|
183
|
+
content: message?.content ?? data.message,
|
|
184
|
+
...(data.error ? { is_error: true } : {}),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
else if (event.type === "turn/start" || event.type === "step/start") {
|
|
188
|
+
blocks.push({ type: "agent_progress", phase: "reasoning" });
|
|
189
|
+
}
|
|
190
|
+
return blocks.length ? { type: "assistant", blocks } : null;
|
|
191
|
+
}
|
|
192
|
+
function asRecord(value) {
|
|
193
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
194
|
+
? value
|
|
195
|
+
: null;
|
|
196
|
+
}
|
|
197
|
+
class AsyncMessageQueue {
|
|
198
|
+
values = [];
|
|
199
|
+
waiters = [];
|
|
200
|
+
done = false;
|
|
201
|
+
error = null;
|
|
202
|
+
push(value) {
|
|
203
|
+
if (this.done)
|
|
204
|
+
return;
|
|
205
|
+
this.values.push(value);
|
|
206
|
+
this.waiters.shift()?.();
|
|
207
|
+
}
|
|
208
|
+
end() {
|
|
209
|
+
this.done = true;
|
|
210
|
+
while (this.waiters.length)
|
|
211
|
+
this.waiters.shift()?.();
|
|
212
|
+
}
|
|
213
|
+
fail(error) {
|
|
214
|
+
this.error = error;
|
|
215
|
+
this.end();
|
|
216
|
+
}
|
|
217
|
+
async *[Symbol.asyncIterator]() {
|
|
218
|
+
while (true) {
|
|
219
|
+
if (this.values.length) {
|
|
220
|
+
yield this.values.shift();
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (this.error)
|
|
224
|
+
throw this.error;
|
|
225
|
+
if (this.done)
|
|
226
|
+
return;
|
|
227
|
+
await new Promise((resolvePromise) => this.waiters.push(resolvePromise));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
package/dist/src/agent-tool.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export const AGENT_TOOLS = ["claude", "cursor", "codex", "ccc"];
|
|
1
|
+
export const AGENT_TOOLS = ["claude", "cursor", "codex", "ccc", "dsh"];
|
|
2
2
|
export const AGENT_TOOL_OPTIONS = [
|
|
3
3
|
{ id: "ccc", label: "CCC" },
|
|
4
4
|
{ id: "claude", label: "Claude" },
|
|
5
5
|
{ id: "cursor", label: "Cursor" },
|
|
6
6
|
{ id: "codex", label: "Codex" },
|
|
7
|
+
{ id: "dsh", label: "DeepSeek Harness" },
|
|
7
8
|
];
|
|
8
9
|
export function isAgentTool(value) {
|
|
9
10
|
return typeof value === "string" && AGENT_TOOLS.includes(value);
|
package/dist/src/cards.js
CHANGED
|
@@ -137,6 +137,7 @@ export function buildHelpCard(userText, opts = {}) {
|
|
|
137
137
|
"发送 **/new cursor** 创建新 Cursor 会话",
|
|
138
138
|
"发送 **/new codex** 创建新 Codex 会话",
|
|
139
139
|
"发送 **/new ccc** 创建新 CCC Agent 会话",
|
|
140
|
+
"发送 **/new dsh** 创建新 DeepSeek Harness 会话",
|
|
140
141
|
"发送 **/forget** 重置当前会话(忘掉上下文,沿用当前工作目录,不切换)",
|
|
141
142
|
"发送 **/plan** 以规划模式提问(只读,不执行写操作)",
|
|
142
143
|
"发送 **/ask** 以问答模式提问(只读,不执行写操作)",
|
|
@@ -157,6 +158,7 @@ export function buildHelpCard(userText, opts = {}) {
|
|
|
157
158
|
{ text: "新建 Cursor 会话(/new cursor)", value: JSON.stringify({ cmd: "new cursor" }), type: "primary" },
|
|
158
159
|
{ text: "新建 Codex 会话(/new codex)", value: JSON.stringify({ cmd: "new codex" }), type: "primary" },
|
|
159
160
|
{ text: "新建 CCC Agent 会话(/new ccc)", value: JSON.stringify({ cmd: "new ccc" }), type: "primary" },
|
|
161
|
+
{ text: "新建 DeepSeek Harness 会话(/new dsh)", value: JSON.stringify({ cmd: "new dsh" }), type: "primary" },
|
|
160
162
|
{ text: "重启 ChatCCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
|
|
161
163
|
{ text: "更新并重启(/update)", value: JSON.stringify({ cmd: "update" }), type: "danger" },
|
|
162
164
|
{ text: "切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
|
|
@@ -253,6 +255,8 @@ function sessionToolLabel(tool) {
|
|
|
253
255
|
return "Codex";
|
|
254
256
|
if (tool === "ccc")
|
|
255
257
|
return "CCC Agent";
|
|
258
|
+
if (tool === "dsh")
|
|
259
|
+
return "DeepSeek Harness";
|
|
256
260
|
return "Claude Code";
|
|
257
261
|
}
|
|
258
262
|
function pushSessionGroup(lines, title, sessions, formatSession, index) {
|
|
@@ -271,22 +275,24 @@ export function buildSessionsCard(sessions, opts = {}) {
|
|
|
271
275
|
const defaultToolLabel = opts.defaultToolLabel ?? "Claude Code";
|
|
272
276
|
const fixedPrivateSession = opts.fixedPrivateSession ?? false;
|
|
273
277
|
// 按 tool 分组排序:Claude Code 在前,Cursor 其次,Codex 最后
|
|
274
|
-
const claudeCodeSessions = sessions.filter(s => s.tool
|
|
278
|
+
const claudeCodeSessions = sessions.filter(s => s.tool === "claude");
|
|
275
279
|
const cursorSessions = sessions.filter(s => s.tool === "cursor");
|
|
276
280
|
const codexSessions = sessions.filter(s => s.tool === "codex");
|
|
277
281
|
const cccSessions = sessions.filter(s => s.tool === "ccc");
|
|
282
|
+
const dshSessions = sessions.filter(s => s.tool === "dsh");
|
|
278
283
|
const hasClaudeCode = claudeCodeSessions.length > 0;
|
|
279
284
|
const hasCursor = cursorSessions.length > 0;
|
|
280
285
|
const hasCodex = codexSessions.length > 0;
|
|
281
286
|
const hasCcc = cccSessions.length > 0;
|
|
287
|
+
const hasDsh = dshSessions.length > 0;
|
|
282
288
|
if (sessions.length === 0) {
|
|
283
289
|
return JSON.stringify({
|
|
284
290
|
config: { wide_screen_mode: true },
|
|
285
291
|
header: { template: "blue", title: { content: "所有会话", tag: "plain_text" } },
|
|
286
292
|
elements: [
|
|
287
293
|
{ tag: "div", text: { tag: "lark_md", content: fixedPrivateSession
|
|
288
|
-
? `当前没有会话记录。\n\n直接发送普通消息即可创建飞书私聊专属 ${defaultToolLabel} 会话;默认 Agent 变化后,下一条普通消息会创建对应 Agent 的新空会话。发送 **/new**、**/new claude**、**/new cursor**、**/new codex** 或 **/new
|
|
289
|
-
: `当前没有会话记录。\n\n使用 **/new**(默认 ${defaultToolLabel})、**/new claude**、**/new cursor**、**/new codex** 或 **/new
|
|
294
|
+
? `当前没有会话记录。\n\n直接发送普通消息即可创建飞书私聊专属 ${defaultToolLabel} 会话;默认 Agent 变化后,下一条普通消息会创建对应 Agent 的新空会话。发送 **/new**、**/new claude**、**/new cursor**、**/new codex**、**/new ccc** 或 **/new dsh** 会另外创建会话群。`
|
|
295
|
+
: `当前没有会话记录。\n\n使用 **/new**(默认 ${defaultToolLabel})、**/new claude**、**/new cursor**、**/new codex**、**/new ccc** 或 **/new dsh** 创建新会话。\n创建后可在任意会话群内发送 **/sessions** 查看列表,用 **/session 数字** 切换会话。` } },
|
|
290
296
|
{ tag: "hr" },
|
|
291
297
|
{ tag: "action", actions: [{ tag: "button", text: { tag: "plain_text", content: "收起" }, type: "default", value: { action: "close" } }] },
|
|
292
298
|
],
|
|
@@ -322,6 +328,8 @@ export function buildSessionsCard(sessions, opts = {}) {
|
|
|
322
328
|
pushSessionGroup(lines, "Codex 会话", codexSessions, formatSession, idx);
|
|
323
329
|
if (hasCcc)
|
|
324
330
|
pushSessionGroup(lines, "CCC Agent 会话", cccSessions, formatSession, idx);
|
|
331
|
+
if (hasDsh)
|
|
332
|
+
pushSessionGroup(lines, "DeepSeek Harness 会话", dshSessions, formatSession, idx);
|
|
325
333
|
return JSON.stringify({
|
|
326
334
|
config: { wide_screen_mode: true },
|
|
327
335
|
header: { template: "blue", title: { content: "所有会话", tag: "plain_text" } },
|
package/dist/src/config.js
CHANGED
|
@@ -58,6 +58,9 @@ export function getAllModelsForTool(tool, cfg = config) {
|
|
|
58
58
|
collect(cfg.ccc.model);
|
|
59
59
|
collect(cfg.ccc.alternativeModel);
|
|
60
60
|
}
|
|
61
|
+
else if (tool === "dsh") {
|
|
62
|
+
collect(cfg.dsh.model);
|
|
63
|
+
}
|
|
61
64
|
return Array.from(seen).slice(0, 100);
|
|
62
65
|
}
|
|
63
66
|
export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
@@ -291,6 +294,7 @@ function loadConfig() {
|
|
|
291
294
|
cursor: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
292
295
|
codex: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
293
296
|
ccc: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
297
|
+
dsh: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
|
|
294
298
|
},
|
|
295
299
|
claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", maxTurn: 0 },
|
|
296
300
|
cursor: {
|
|
@@ -316,6 +320,15 @@ function loadConfig() {
|
|
|
316
320
|
compactionTimeoutMs: DEFAULT_CCC_COMPACTION_TIMEOUT_MS,
|
|
317
321
|
contextWindow: DEFAULT_CCC_CONTEXT_WINDOW_TOKENS,
|
|
318
322
|
},
|
|
323
|
+
dsh: {
|
|
324
|
+
enabled: false,
|
|
325
|
+
defaultAgent: false,
|
|
326
|
+
apiKey: "",
|
|
327
|
+
baseUrl: DEFAULT_CCC_DEEPSEEK_BASE_URL,
|
|
328
|
+
model: "deepseek-v4-flash",
|
|
329
|
+
provider: "deepseek-official",
|
|
330
|
+
maxTokens: 49152,
|
|
331
|
+
},
|
|
319
332
|
};
|
|
320
333
|
if (!IS_TEST_ENV) {
|
|
321
334
|
migrateLegacyData();
|
|
@@ -368,6 +381,7 @@ function loadConfig() {
|
|
|
368
381
|
const cursorRaw = (parsed.cursor ?? {});
|
|
369
382
|
const codexRaw = (parsed.codex ?? {});
|
|
370
383
|
const cccRaw = (parsed.ccc ?? {});
|
|
384
|
+
const dshRaw = (parsed.dsh ?? {});
|
|
371
385
|
const webUiRaw = (parsed.webUi ?? {});
|
|
372
386
|
const chromeDevtoolsRaw = (parsed.chromeDevtools ?? {});
|
|
373
387
|
const rawStreamLogsRaw = typeof parsed.rawStreamLogs === "object" && parsed.rawStreamLogs !== null
|
|
@@ -408,17 +422,20 @@ function loadConfig() {
|
|
|
408
422
|
const cursorEnabled = resolveEnabled(cursorRaw.enabled, cursorNonEmpty);
|
|
409
423
|
const codexEnabled = resolveEnabled(codexRaw.enabled, codexNonEmpty);
|
|
410
424
|
const cccEnabled = resolveCccEnabled(cccRaw.enabled, cccRaw.DEEPSEEK_API_KEY);
|
|
425
|
+
const dshEnabled = resolveEnabled(dshRaw.enabled, () => Boolean(typeof dshRaw.apiKey === "string" && dshRaw.apiKey.trim()));
|
|
411
426
|
const chromeDevtoolsPort = Number(chromeDevtoolsRaw.port);
|
|
412
427
|
const explicitDefaultTool = typeof claude.defaultAgent === "boolean" && claude.defaultAgent && claudeEnabled ? "claude" :
|
|
413
428
|
typeof cursorRaw.defaultAgent === "boolean" && cursorRaw.defaultAgent && cursorEnabled ? "cursor" :
|
|
414
429
|
typeof codexRaw.defaultAgent === "boolean" && codexRaw.defaultAgent && codexEnabled ? "codex" :
|
|
415
430
|
typeof cccRaw.defaultAgent === "boolean" && cccRaw.defaultAgent && cccEnabled ? "ccc" :
|
|
416
|
-
|
|
431
|
+
typeof dshRaw.defaultAgent === "boolean" && dshRaw.defaultAgent && dshEnabled ? "dsh" :
|
|
432
|
+
null;
|
|
417
433
|
const fallbackDefaultTool = claudeEnabled ? "claude" :
|
|
418
434
|
cursorEnabled ? "cursor" :
|
|
419
435
|
codexEnabled ? "codex" :
|
|
420
436
|
cccEnabled ? "ccc" :
|
|
421
|
-
"
|
|
437
|
+
dshEnabled ? "dsh" :
|
|
438
|
+
"claude";
|
|
422
439
|
const defaultTool = explicitDefaultTool ?? fallbackDefaultTool;
|
|
423
440
|
return {
|
|
424
441
|
feishu: {
|
|
@@ -462,6 +479,7 @@ function loadConfig() {
|
|
|
462
479
|
cursor: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.cursor),
|
|
463
480
|
codex: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.codex),
|
|
464
481
|
ccc: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.ccc),
|
|
482
|
+
dsh: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.dsh),
|
|
465
483
|
},
|
|
466
484
|
claude: {
|
|
467
485
|
enabled: claudeEnabled,
|
|
@@ -509,6 +527,15 @@ function loadConfig() {
|
|
|
509
527
|
compactionTimeoutMs: normalizePositiveInteger(cccRaw.compactionTimeoutMs, DEFAULT_CCC_COMPACTION_TIMEOUT_MS),
|
|
510
528
|
contextWindow: normalizePositiveInteger(cccRaw.contextWindow, DEFAULT_CCC_CONTEXT_WINDOW_TOKENS),
|
|
511
529
|
},
|
|
530
|
+
dsh: {
|
|
531
|
+
enabled: dshEnabled,
|
|
532
|
+
defaultAgent: defaultTool === "dsh",
|
|
533
|
+
apiKey: normalizeOptionalConfigField(dshRaw.apiKey, { label: "dsh.apiKey" }),
|
|
534
|
+
baseUrl: normalizeOptionalConfigField(dshRaw.baseUrl, { label: "dsh.baseUrl", fallback: DEFAULT_CCC_DEEPSEEK_BASE_URL }),
|
|
535
|
+
model: normalizeOptionalConfigField(dshRaw.model, { label: "dsh.model", fallback: "deepseek-v4-flash" }),
|
|
536
|
+
provider: normalizeOptionalConfigField(dshRaw.provider, { label: "dsh.provider", fallback: "deepseek-official" }),
|
|
537
|
+
maxTokens: normalizePositiveInteger(dshRaw.maxTokens, 49152),
|
|
538
|
+
},
|
|
512
539
|
};
|
|
513
540
|
}
|
|
514
541
|
/**
|
|
@@ -781,6 +808,7 @@ export const CURSOR_SESSION_PREFIX = "Cursor Session:";
|
|
|
781
808
|
export const CODEX_SESSION_PREFIX = "Codex Session:";
|
|
782
809
|
/** 群描述中用于识别 CCC Agent 会话的前缀 */
|
|
783
810
|
export const CCC_SESSION_PREFIX = "CCC Session:";
|
|
811
|
+
export const DSH_SESSION_PREFIX = "DSH Session:";
|
|
784
812
|
/** 根据 tool 名称返回对应的群描述前缀 */
|
|
785
813
|
export function sessionPrefixForTool(tool) {
|
|
786
814
|
if (tool === "cursor")
|
|
@@ -789,6 +817,8 @@ export function sessionPrefixForTool(tool) {
|
|
|
789
817
|
return CODEX_SESSION_PREFIX;
|
|
790
818
|
if (tool === "ccc")
|
|
791
819
|
return CCC_SESSION_PREFIX;
|
|
820
|
+
if (tool === "dsh")
|
|
821
|
+
return DSH_SESSION_PREFIX;
|
|
792
822
|
return CLAUDE_SESSION_PREFIX;
|
|
793
823
|
}
|
|
794
824
|
/** 根据 tool 名称返回用于状态展示的标签 */
|
|
@@ -799,6 +829,8 @@ export function toolDisplayName(tool) {
|
|
|
799
829
|
return "Codex";
|
|
800
830
|
if (tool === "ccc")
|
|
801
831
|
return "CCC Agent";
|
|
832
|
+
if (tool === "dsh")
|
|
833
|
+
return "DeepSeek Harness";
|
|
802
834
|
return "Claude Code";
|
|
803
835
|
}
|
|
804
836
|
/** 解析 /new 未指定工具时使用的默认 Agent。旧配置缺省 defaultAgent 时保持 Claude 优先。 */
|