chatccc 0.2.27 → 0.2.29

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.
@@ -4,17 +4,22 @@
4
4
 
5
5
  Videos are sent as regular files (not media), which looks cleaner in Feishu.
6
6
 
7
- ### Script (recommended)
7
+ First, query the current send token:
8
+ ```bash
9
+ curl "{{session_grants_url}}?sid={{session_id}}"
10
+ ```
8
11
 
12
+ Then send with the returned url and token:
13
+
14
+ ### Script (recommended)
9
15
  ```bash
10
- node "{{send_file_script}}" --url "{{send_file_url}}" --token "{{send_file_token}}" --path "<absolute file path>" --caption "<optional caption>"
16
+ node "{{send_file_script}}" --url <url_from_query> --token <token_from_query> --path "<absolute file path>" --caption "<optional caption>"
11
17
  ```
12
18
 
13
19
  ### Direct HTTP
14
-
15
20
  ```http
16
- POST {{send_file_url}}
17
- Authorization: Bearer {{send_file_token}}
21
+ POST <url_from_query>
22
+ Authorization: Bearer <token_from_query>
18
23
  Content-Type: application/json; charset=utf-8
19
24
 
20
25
  {"path":"<absolute file path>","caption":"<optional caption>"}
@@ -2,17 +2,22 @@
2
2
 
3
3
  ## Send Images
4
4
 
5
- ### Script (recommended)
5
+ First, query the current send token:
6
+ ```bash
7
+ curl "{{session_grants_url}}?sid={{session_id}}"
8
+ ```
6
9
 
10
+ Then send with the returned url and token:
11
+
12
+ ### Script (recommended)
7
13
  ```bash
8
- node "{{send_image_script}}" --url "{{send_image_url}}" --token "{{send_image_token}}" --path "<absolute image path>" --caption "<optional caption>"
14
+ node "{{send_image_script}}" --url <url_from_query> --token <token_from_query> --path "<absolute image path>" --caption "<optional caption>"
9
15
  ```
10
16
 
11
17
  ### Direct HTTP
12
-
13
18
  ```http
14
- POST {{send_image_url}}
15
- Authorization: Bearer {{send_image_token}}
19
+ POST <url_from_query>
20
+ Authorization: Bearer <token_from_query>
16
21
  Content-Type: application/json; charset=utf-8
17
22
 
18
23
  {"path":"<absolute image path>","caption":"<optional caption>"}
@@ -7,5 +7,6 @@ Current working directory: {{cwd}}
7
7
 
8
8
  Use local endpoints instead of calling Feishu Open Platform directly.
9
9
 
10
- - **Images** (send & receive): read `{{im_skills_cache_dir}}/feishu-skill/send-image.md`
11
- - **Files & Videos** (send & download): read `{{im_skills_cache_dir}}/feishu-skill/send-file.md`
10
+ - **Get send tokens**: `GET {{session_grants_url}}?sid={{session_id}}` — returns `{ image: {url,token}, file: {url,token} }`
11
+ - **Images** (send & receive): read `{{im_skills_cache_dir}}/feishu-skill/receive-send-image.md`
12
+ - **Files & Videos** (send & download): read `{{im_skills_cache_dir}}/feishu-skill/receive-send-file.md`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.27",
3
+ "version": "0.2.29",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -23,9 +23,7 @@ describe("IM skills prompt rendering", () => {
23
23
  "---",
24
24
  "name: feishu-skill",
25
25
  "---",
26
- "POST {{send_image_url}}",
27
- "Authorization: Bearer {{send_image_token}}",
28
- "Content-Type: application/json; charset=utf-8",
26
+ "GET {{session_grants_url}}?sid={{session_id}}",
29
27
  "cwd={{cwd}}",
30
28
  ].join("\n"),
31
29
  "utf-8",
@@ -35,15 +33,13 @@ describe("IM skills prompt rendering", () => {
35
33
  skillsDir: tempRoot,
36
34
  variables: {
37
35
  cwd: "C:/work",
38
- send_image_url: "http://127.0.0.1:18080/api/agent/send-image",
39
- send_image_token: "tok_test",
36
+ session_grants_url: "http://127.0.0.1:18080/api/agent/session-grants",
37
+ session_id: "sid_test",
40
38
  },
41
39
  });
42
40
 
43
41
  expect(prompt).toContain("[ChatCCC IM skill: feishu-skill]");
44
- expect(prompt).toContain("POST http://127.0.0.1:18080/api/agent/send-image");
45
- expect(prompt).toContain("Authorization: Bearer tok_test");
46
- expect(prompt).toContain("Content-Type: application/json; charset=utf-8");
42
+ expect(prompt).toContain("GET http://127.0.0.1:18080/api/agent/session-grants?sid=sid_test");
47
43
  expect(prompt).toContain("cwd=C:/work");
48
44
  expect(prompt).not.toContain("{{");
49
45
  });
@@ -0,0 +1,71 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ import type { AgentImageGrant } from "./agent-image-rpc.ts";
4
+ import type { AgentFileGrant } from "./agent-file-rpc.ts";
5
+ import { ts } from "./config.ts";
6
+
7
+ export const AGENT_SESSION_GRANTS_PATH = "/api/agent/session-grants";
8
+
9
+ interface SessionGrantsEntry {
10
+ image: AgentImageGrant;
11
+ file: AgentFileGrant;
12
+ }
13
+
14
+ const sessionGrants = new Map<string, SessionGrantsEntry>();
15
+
16
+ export function setSessionGrants(
17
+ sessionId: string,
18
+ imageGrant: AgentImageGrant,
19
+ fileGrant: AgentFileGrant,
20
+ ): void {
21
+ sessionGrants.set(sessionId, { image: imageGrant, file: fileGrant });
22
+ }
23
+
24
+ export function clearSessionGrants(sessionId: string): void {
25
+ sessionGrants.delete(sessionId);
26
+ }
27
+
28
+ function jsonReply(res: ServerResponse, status: number, data: unknown): void {
29
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
30
+ res.end(JSON.stringify(data));
31
+ }
32
+
33
+ export async function handleAgentGrantsRequest(
34
+ req: IncomingMessage,
35
+ res: ServerResponse,
36
+ ): Promise<boolean> {
37
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
38
+ if (url.pathname !== AGENT_SESSION_GRANTS_PATH) return false;
39
+
40
+ if (req.method !== "GET") {
41
+ jsonReply(res, 405, { ok: false, error: "Method not allowed" });
42
+ return true;
43
+ }
44
+
45
+ const sid = url.searchParams.get("sid");
46
+ if (!sid) {
47
+ jsonReply(res, 400, { ok: false, error: "Missing sid parameter" });
48
+ return true;
49
+ }
50
+
51
+ const entry = sessionGrants.get(sid);
52
+ if (!entry) {
53
+ jsonReply(res, 404, { ok: false, error: "No active grants for this session" });
54
+ return true;
55
+ }
56
+
57
+ const now = Date.now();
58
+ if (entry.image.expiresAt <= now || entry.file.expiresAt <= now) {
59
+ sessionGrants.delete(sid);
60
+ jsonReply(res, 410, { ok: false, error: "Session grants expired" });
61
+ return true;
62
+ }
63
+
64
+ console.log(`[${ts()}] [AGENT-GRANTS] query session=${sid}`);
65
+ jsonReply(res, 200, {
66
+ ok: true,
67
+ image: { url: entry.image.url, token: entry.image.token },
68
+ file: { url: entry.file.url, token: entry.file.token },
69
+ });
70
+ return true;
71
+ }
package/src/index.ts CHANGED
@@ -84,6 +84,7 @@ import { buildHelpCard, buildStatusCard, buildProgressCard, buildCdContent, buil
84
84
  import { updateCardKitCard } from "./cardkit.ts";
85
85
  import { handleAgentImageRequest } from "./agent-image-rpc.ts";
86
86
  import { handleAgentFileRequest } from "./agent-file-rpc.ts";
87
+ import { handleAgentGrantsRequest } from "./agent-grants-rpc.ts";
87
88
  import { formatGitResult, gitResultHeaderTemplate, runGitCommand } from "./git-command.ts";
88
89
  import {
89
90
  MAX_PROCESSED,
@@ -1058,7 +1059,7 @@ async function main(): Promise<void> {
1058
1059
  });
1059
1060
  });
1060
1061
  setExtraApiHandler(async (req, res) => {
1061
- return (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
1062
+ return (await handleAgentGrantsRequest(req, res)) || (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
1062
1063
  });
1063
1064
 
1064
1065
  console.log(`[启动 2/7] 环境与凭证检查`);
package/src/session.ts CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  createAgentFileGrant,
41
41
  revokeAgentFileGrant,
42
42
  } from "./agent-file-rpc.ts";
43
+ import { setSessionGrants, clearSessionGrants, AGENT_SESSION_GRANTS_PATH } from "./agent-grants-rpc.ts";
43
44
  import { buildImSkillsPrompt, exportSkillSubDocs } from "./im-skills.ts";
44
45
 
45
46
  // ---------------------------------------------------------------------------
@@ -341,15 +342,15 @@ export async function resumeAndPrompt(
341
342
  const imSkillsCacheDir = join(USER_DATA_DIR, "im-skills");
342
343
  const skillVariables = {
343
344
  cwd,
345
+ session_id: sessionId,
344
346
  im_skills_cache_dir: imSkillsCacheDir,
347
+ session_grants_url: `http://127.0.0.1:${CHATCCC_PORT}${AGENT_SESSION_GRANTS_PATH}`,
345
348
  send_image_url: imageGrant.url,
346
- send_image_token: imageGrant.token,
347
- send_file_url: fileGrant.url,
348
- send_file_token: fileGrant.token,
349
349
  send_image_script: join(feishuSkillDir, "send-image.mjs"),
350
350
  send_file_script: join(feishuSkillDir, "send-file.mjs"),
351
351
  download_video_script: join(feishuSkillDir, "download-video.mjs"),
352
352
  };
353
+ setSessionGrants(sessionId, imageGrant, fileGrant);
353
354
  const imSkillsPrompt = await buildImSkillsPrompt({ variables: skillVariables });
354
355
  // 渲染子文档到缓存目录,供 Agent 按需读取
355
356
  await exportSkillSubDocs({ variables: skillVariables }, imSkillsCacheDir);
@@ -516,6 +517,7 @@ export async function resumeAndPrompt(
516
517
  if (sendInterval) clearInterval(sendInterval);
517
518
  revokeAgentImageGrant(imageGrant.token);
518
519
  revokeAgentFileGrant(fileGrant.token);
520
+ clearSessionGrants(sessionId);
519
521
  }
520
522
 
521
523
  const cEntry = chatSessionMap.get(chatId);