chatccc 0.2.268 → 0.2.270

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.
@@ -1,4 +1,5 @@
1
1
  import { resolve } from "node:path";
2
+ import { homedir } from "node:os";
2
3
  import { resolveDefaultAgentTool } from "./config.js";
3
4
  import { readUtf8JsonBody } from "./agent-rpc-body.js";
4
5
  import { delegateAgentTask } from "./agent-delegate-task.js";
@@ -6,7 +7,7 @@ import { applySharedPrefix } from "./shared-prefix.js";
6
7
  import { beginSafeMaintenanceTrackedWork, isSafeMaintenanceAdmissionClosed, } from "./safe-maintenance.js";
7
8
  export const AGENT_DELEGATE_TASK_PATH = "/api/agent/delegate-task";
8
9
  const MAX_REQUEST_BYTES = 128 * 1024;
9
- const VALID_TOOLS = new Set(["claude", "cursor", "codex"]);
10
+ const VALID_TOOLS = new Set(["claude", "cursor", "codex", "ccc", "dsh"]);
10
11
  function jsonReply(res, status, data) {
11
12
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
12
13
  res.end(JSON.stringify(data));
@@ -38,7 +39,7 @@ function validateTool(rawTool) {
38
39
  function validateCwd(rawCwd) {
39
40
  const cwd = stringValue(rawCwd);
40
41
  if (!cwd)
41
- throw new Error("cwd must be a non-empty string");
42
+ return homedir();
42
43
  return resolve(cwd);
43
44
  }
44
45
  export async function handleAgentDelegateTaskRequest(req, res, platform) {
@@ -70,8 +71,6 @@ export async function handleAgentDelegateTaskRequest(req, res, platform) {
70
71
  tool = validateTool(payload.tool);
71
72
  cwd = validateCwd(payload.cwd);
72
73
  const rawPrompt = promptFromPayload(payload);
73
- if (!rawPrompt)
74
- throw new Error("prompt must be a non-empty string");
75
74
  const sharedPrefix = applySharedPrefix(rawPrompt);
76
75
  promptText = sharedPrefix.text;
77
76
  promptNamePrefix = sharedPrefix.body || rawPrompt;
@@ -117,24 +116,27 @@ export async function handleAgentDelegateTaskRequest(req, res, platform) {
117
116
  }
118
117
  export function buildAgentDelegateTaskCapabilityPrompt(input) {
119
118
  const lines = [
120
- "[ChatCCC local capability: delegate task]",
121
- "You can create a separate Feishu ChatCCC agent session and assign its first task by calling this local endpoint.",
119
+ "[ChatCCC local capability: new session]",
120
+ "You can create a separate Feishu ChatCCC agent session (a new group) and optionally assign its first task by calling this local endpoint.",
122
121
  "",
123
122
  `POST ${input.url}`,
124
123
  "Content-Type: application/json; charset=utf-8",
125
124
  "",
126
- 'Body: {"tool":"codex|claude|cursor","cwd":"absolute working directory","open_id":"Feishu open_id to invite","prompt":"first task text"}',
125
+ 'Body: {"tool":"claude|cursor|codex|ccc|dsh","cwd":"absolute working directory","open_id":"initiator open_id","prompt":"optional first task"}',
127
126
  "",
128
127
  "Rules:",
129
- "- Use this only when the user asks you to start a separate delegated conversation/session.",
130
- "- Pass cwd explicitly as an absolute local path.",
131
- "- Pass tool explicitly when the user specified a target agent.",
132
- "- Use open_id for one user or open_ids/openIds for multiple users.",
128
+ "- Use this when the user asks to start a new conversation/session, optionally in a specific directory.",
129
+ "- Pass open_id exactly as provided to you; the new group will only include that requester.",
130
+ "- Pass cwd as an absolute local path. When the user does not name a directory, use your current working directory.",
131
+ "- tool is optional; omit it to use the default agent, or pass one of claude/cursor/codex/ccc/dsh when the user names a tool.",
132
+ "- prompt is optional; omit it to just create the session without sending a first task.",
133
133
  "- The prompt is sent through the normal ChatCCC prompt path, so project prompt injection and IM skills still apply.",
134
134
  "- Request body must be UTF-8 encoded JSON bytes. Do not call Feishu Open Platform directly.",
135
- "[/ChatCCC local capability: delegate task]",
135
+ "[/ChatCCC local capability: new session]",
136
136
  ];
137
137
  if (input.cwd)
138
138
  lines.splice(2, 0, `Current working directory: ${input.cwd}`);
139
+ if (input.openId)
140
+ lines.splice(2, 0, `Initiator open_id: ${input.openId}`);
139
141
  return lines.join("\n");
140
142
  }
@@ -7,9 +7,10 @@ import { sessionChatName } from "./session-name.js";
7
7
  export async function delegateAgentTask(input) {
8
8
  const cwd = resolve(input.cwd);
9
9
  const toolLabel = toolDisplayName(input.tool);
10
+ const hasPrompt = input.promptText.trim().length > 0;
10
11
  const init = await initClaudeSession(input.tool, cwd);
11
12
  const sessionId = init.sessionId;
12
- const chatNamePrefix = input.chatNamePrefix?.trim() || input.promptText.slice(0, 10) || "新会话";
13
+ const chatNamePrefix = input.chatNamePrefix?.trim() || (hasPrompt ? input.promptText.slice(0, 10) : "新会话");
13
14
  const chatName = sessionChatName(chatNamePrefix, cwd);
14
15
  let chatId;
15
16
  try {
@@ -36,13 +37,17 @@ export async function delegateAgentTask(input) {
36
37
  await input.platform.sendCard(chatId, `${toolLabel} Session Ready`, `已创建 **${toolLabel}** 会话群。\n\n` +
37
38
  `**Session ID:** ${sessionId}\n` +
38
39
  `**工作目录:** \`${cwd}\`\n\n` +
39
- `下面会自动把任务作为第一句话发送给 ${toolLabel}。`, "green").catch(() => { });
40
+ (hasPrompt
41
+ ? `下面会自动把任务作为第一句话发送给 ${toolLabel}。`
42
+ : `直接在这里发消息即可与 ${toolLabel} 对话。`), "green").catch(() => { });
40
43
  const fastMode = getEffectiveFastModeForTool(input.tool, sessionId);
41
44
  const avatarUpdate = fastMode
42
45
  ? input.platform.setChatAvatar(chatId, input.tool, "new", { fastMode: true })
43
46
  : input.platform.setChatAvatar(chatId, input.tool, "new");
44
47
  avatarUpdate.catch(() => { });
45
- await resumeAndPrompt(sessionId, input.promptText, input.platform, chatId, input.msgTimestamp ?? Date.now(), input.tool, input.traceId);
48
+ if (hasPrompt) {
49
+ await resumeAndPrompt(sessionId, input.promptText, input.platform, chatId, input.msgTimestamp ?? Date.now(), input.tool, input.traceId, input.openIds?.[0]);
50
+ }
46
51
  console.log(`[${ts()}] [AGENT-DELEGATE-TASK] created ${toolLabel} session=${sessionId} chat=${chatId} cwd=${cwd}`);
47
52
  return { chatId, sessionId, tool: input.tool, cwd };
48
53
  }
@@ -0,0 +1,73 @@
1
+ import { resolve } from "node:path";
2
+ import { stat } from "node:fs/promises";
3
+ import { addRecentDir, setDefaultCwd } from "./config.js";
4
+ import { readUtf8JsonBody } from "./agent-rpc-body.js";
5
+ import { getChatsForSession } from "./session-chat-binding.js";
6
+ export const AGENT_SET_CWD_PATH = "/api/agent/set-cwd";
7
+ const MAX_REQUEST_BYTES = 64 * 1024;
8
+ function jsonReply(res, status, data) {
9
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
10
+ res.end(JSON.stringify(data));
11
+ }
12
+ function stringValue(value) {
13
+ return typeof value === "string" ? value.trim() : "";
14
+ }
15
+ /**
16
+ * Agent 本地能力:设置后续新建会话的默认工作目录(等价于 /cd,不改变当前会话)。
17
+ * 请求携带发起者当前 session_id,主进程据此反查 chatId,并持久化到该 chat 的
18
+ * working_dir 文件,同时写入最近目录记录。
19
+ */
20
+ export async function handleAgentSetCwdRequest(req, res) {
21
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
22
+ if (url.pathname !== AGENT_SET_CWD_PATH)
23
+ return false;
24
+ if (req.method !== "POST") {
25
+ jsonReply(res, 405, { ok: false, error: "Method not allowed" });
26
+ return true;
27
+ }
28
+ let payload;
29
+ try {
30
+ payload = await readUtf8JsonBody(req, MAX_REQUEST_BYTES);
31
+ }
32
+ catch (err) {
33
+ jsonReply(res, 400, { ok: false, error: err.message || "Invalid JSON" });
34
+ return true;
35
+ }
36
+ const sessionId = stringValue(payload.session_id);
37
+ if (!sessionId) {
38
+ jsonReply(res, 400, { ok: false, error: "Missing session_id" });
39
+ return true;
40
+ }
41
+ const rawDir = stringValue(payload.dir) || stringValue(payload.path);
42
+ if (!rawDir) {
43
+ jsonReply(res, 400, { ok: false, error: "dir must be a non-empty string" });
44
+ return true;
45
+ }
46
+ const dir = resolve(rawDir);
47
+ try {
48
+ const st = await stat(dir);
49
+ if (!st.isDirectory()) {
50
+ jsonReply(res, 400, { ok: false, error: `path is not a directory: ${dir}` });
51
+ return true;
52
+ }
53
+ }
54
+ catch {
55
+ jsonReply(res, 400, { ok: false, error: `directory does not exist: ${dir}` });
56
+ return true;
57
+ }
58
+ const chatIds = getChatsForSession(sessionId);
59
+ const chatId = chatIds[0];
60
+ if (!chatId) {
61
+ jsonReply(res, 404, { ok: false, error: "No chat bound to this session; cannot set working directory." });
62
+ return true;
63
+ }
64
+ try {
65
+ await setDefaultCwd(dir, chatId);
66
+ await addRecentDir(dir);
67
+ jsonReply(res, 200, { ok: true, dir, chat_id: chatId });
68
+ }
69
+ catch (err) {
70
+ jsonReply(res, 500, { ok: false, error: err.message });
71
+ }
72
+ return true;
73
+ }
package/dist/src/cards.js CHANGED
@@ -143,10 +143,7 @@ export function buildHelpCard(userText, opts = {}) {
143
143
  "发送 **/ask** 以问答模式提问(只读,不执行写操作)",
144
144
  "发送 **/usage** 查看当前 Agent 的用量或余额",
145
145
  "发送 **/restart** 重启 ChatCCC 进程",
146
- "发送 **/restart safe** 等待现有任务完成后安全重启",
147
146
  "发送 **/update** 更新并重启(仅 npm 全局安装可用)",
148
- "发送 **/update safe** 等待现有任务完成后安全更新",
149
- "发送 **/safestatus** 查看安全维护状态,**/cancelsf** 取消等待中的预约",
150
147
  ABD_HELP_LINE,
151
148
  ].join("\n");
152
149
  return JSON.stringify({
@@ -56,8 +56,8 @@ function promptCacheKey(input) {
56
56
  const names = input.enabledSkillNames
57
57
  ? [...input.enabledSkillNames].sort().join(",")
58
58
  : "*";
59
- const { session_id, cwd } = input.variables;
60
- return `${input.skillsDir ?? DEFAULT_IM_SKILLS_DIR}|${names}|${session_id}|${cwd}`;
59
+ const { session_id, cwd, open_id } = input.variables;
60
+ return `${input.skillsDir ?? DEFAULT_IM_SKILLS_DIR}|${names}|${session_id}|${cwd}|${open_id}`;
61
61
  }
62
62
  /** 带会话级缓存的 buildImSkillsPrompt。同 session + 同 cwd 时直接返回缓存的渲染结果。 */
63
63
  export async function buildImSkillsPromptCached(input) {
package/dist/src/index.js CHANGED
@@ -40,6 +40,7 @@ import { handleAgentFileRequest } from "./agent-file-rpc.js";
40
40
  import { handleAgentDelegateTaskRequest } from "./agent-delegate-task-rpc.js";
41
41
  import { handleAgentStopStuckRequest } from "./agent-stop-stuck.js";
42
42
  import { handleAgentReloadConfigRequest } from "./agent-reload-config-rpc.js";
43
+ import { handleAgentSetCwdRequest } from "./agent-set-cwd-rpc.js";
43
44
  import { handleChatGptSubscriptionRequest } from "./chatgpt-subscription-rpc.js";
44
45
  import { applyPrivacy } from "./privacy.js";
45
46
  import { createCardKitCard, sendCardKitMessage, updateCardKitCard, } from "./cardkit.js";
@@ -561,6 +562,7 @@ async function main() {
561
562
  if (injected)
562
563
  return true;
563
564
  return (await handleAgentReloadConfigRequest(req, res))
565
+ || (await handleAgentSetCwdRequest(req, res))
564
566
  || (await handleAgentImageRequest(req, res))
565
567
  || (await handleAgentFileRequest(req, res))
566
568
  || (await handleAgentDelegateTaskRequest(req, res, feishuPlatform))
@@ -622,6 +624,7 @@ async function main() {
622
624
  });
623
625
  setExtraApiHandler(async (req, res) => {
624
626
  return (await handleAgentReloadConfigRequest(req, res))
627
+ || (await handleAgentSetCwdRequest(req, res))
625
628
  || (await handleAgentImageRequest(req, res))
626
629
  || (await handleAgentFileRequest(req, res))
627
630
  || (await handleAgentDelegateTaskRequest(req, res, feishuPlatform))
@@ -2090,7 +2090,7 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
2090
2090
  }
2091
2091
  try {
2092
2092
  logTrace(tid, "RESUME", { sessionId, tool: descriptionTool });
2093
- const resumeOutcome = await resumeAndPrompt(sessionId, promptText, platform, chatId, msgTimestamp, descriptionTool, tid);
2093
+ const resumeOutcome = await resumeAndPrompt(sessionId, promptText, platform, chatId, msgTimestamp, descriptionTool, tid, openId);
2094
2094
  if (resumeOutcome === "error") {
2095
2095
  logTrace(tid, "DONE", { outcome: "resume_error", sessionId });
2096
2096
  console.error(`[${ts()}] [RESUME] Session ${sessionId} ended with an Agent error`);
@@ -2269,7 +2269,7 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
2269
2269
  if (!switchResult.ok) {
2270
2270
  throw switchResult.error ?? new Error("Failed to bind Feishu private session");
2271
2271
  }
2272
- await resumeAndPrompt(sessionId, promptText, platform, chatId, msgTimestamp, tool, tid);
2272
+ await resumeAndPrompt(sessionId, promptText, platform, chatId, msgTimestamp, tool, tid, openId);
2273
2273
  logTrace(tid, "DONE", {
2274
2274
  outcome: "auto_new_feishu_p2p_prompt_done",
2275
2275
  chatId,
@@ -913,8 +913,10 @@ export async function initClaudeSession(tool, overrideCwd, chatId) {
913
913
  await addRecentDir(cwd);
914
914
  return { sessionId, cwd };
915
915
  }
916
- export async function resumeAndPrompt(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId) {
917
- return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
916
+ export async function resumeAndPrompt(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId, initiatorOpenId) {
917
+ return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId, {
918
+ initiatorOpenId,
919
+ });
918
920
  }
919
921
  export async function runAgentSession(sessionId, userText, platform, _chatId, msgTimestamp, tool, traceId, options = {}) {
920
922
  const tid = traceId ?? "";
@@ -990,8 +992,10 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
990
992
  const skillVariables = {
991
993
  cwd,
992
994
  session_id: sessionId,
995
+ open_id: options.initiatorOpenId,
993
996
  im_skills_cache_dir: imSkillsCacheDir,
994
997
  delegate_task_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/delegate-task`,
998
+ set_cwd_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/set-cwd`,
995
999
  send_image_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-image`,
996
1000
  send_file_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-file`,
997
1001
  send_image_script: join(feishuSkillDir, "send-image.mjs"),
@@ -1,12 +1,26 @@
1
- ---
2
- name: feishu-skill
3
- description: Feishu IM local skills for sending and receiving images, files, and videos.
4
- ---
5
-
6
- Current working directory: {{cwd}}
7
-
8
- Use local endpoints instead of calling Feishu Open Platform directly.
9
-
1
+ ---
2
+ name: feishu-skill
3
+ description: Feishu IM local skills for sending images, files, videos, and for creating new sessions or switching working directories.
4
+ ---
5
+
6
+ Current working directory: {{cwd}}
7
+ Your session id: {{session_id}}
8
+ Your Feishu open_id: {{open_id}}
9
+
10
+ Use local endpoints instead of calling Feishu Open Platform directly.
11
+
10
12
  - **Send images**: POST `{{send_image_url}}` with `{"session_id":"{{session_id}}","path":"<absolute path>","caption":"<optional>"}` — read `{{im_skills_cache_dir}}/feishu-skill/receive-send-image.md`
11
13
  - **Send files/videos**: POST `{{send_file_url}}` with `{"session_id":"{{session_id}}","path":"<absolute path>","caption":"<optional>"}` — read `{{im_skills_cache_dir}}/feishu-skill/receive-send-file.md`
12
- - **Delegate a task to a new agent conversation**: POST `{{delegate_task_url}}` with `{"tool":"codex|claude|cursor","cwd":"<absolute working directory>","open_id":"<Feishu user open_id>","prompt":"<first task>"}`. Use `open_ids` for multiple users. This uses the normal ChatCCC prompt flow, so project prompt injection and IM skills still apply.
14
+ - **Create a new session (新建会话)**: POST `{{delegate_task_url}}` with `{"tool":"claude|cursor|codex|ccc|dsh","cwd":"<absolute path>","open_id":"{{open_id}}","prompt":"<optional first task>"}`. This creates a new Feishu group and session, and only adds you (the requester). `tool` and `prompt` are optional; omit `prompt` to just create the session without a first task.
15
+ - **Set default working directory (cd / 切换目录)**: POST `{{set_cwd_url}}` with `{"session_id":"{{session_id}}","dir":"<absolute path>"}`. This sets the default directory for future new sessions only; it does not change the current session.
16
+
17
+ How to map user requests to these endpoints:
18
+
19
+ - "新建会话 / 开个新会话 / 换个新会话" → create a new session (no prompt). Use `cwd` = your current working directory ({{cwd}}) unless the user names a directory.
20
+ - "在 <目录> 新建会话(做 <任务>)" → create a new session with `cwd` = that directory, and set `prompt` to the task if one was given.
21
+ - "cd 到 <目录> / 切换到 <目录> / 去 <目录> 干活" → judge intent:
22
+ - if the user wants to start working there now (a fresh conversation in that directory) → create a new session with `cwd` = that directory.
23
+ - if the user only wants to change the default directory for future sessions → call set-cwd.
24
+ - when ambiguous, prefer creating a new session (the more common intent for "切换到").
25
+ - Directory names may be fuzzy or relative; resolve them to an absolute local path (using your file tools) before calling either endpoint.
26
+ - `open_id` must always be passed as exactly {{open_id}}; do not invent it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.268",
3
+ "version": "0.2.270",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",