chatccc 0.2.72 → 0.2.74

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.72",
3
+ "version": "0.2.74",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -3,7 +3,12 @@ import { join } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  import { afterEach, describe, expect, it } from "vitest";
5
5
 
6
- import { buildImSkillsPrompt, exportSkillSubDocs } from "../im-skills.ts";
6
+ import {
7
+ buildImSkillsPrompt,
8
+ buildImSkillsPromptCached,
9
+ clearImSkillsPromptCache,
10
+ exportSkillSubDocs,
11
+ } from "../im-skills.ts";
7
12
 
8
13
  let tempRoot: string | null = null;
9
14
 
@@ -97,4 +102,24 @@ describe("IM skills prompt rendering", () => {
97
102
  expect(prompt).toContain("[ChatCCC IM skill: wechat-skill]");
98
103
  expect(exported.length).toBeGreaterThanOrEqual(2);
99
104
  });
105
+
106
+ it("exports cached prompt helpers used by session startup", async () => {
107
+ tempRoot = await mkdtemp(join(tmpdir(), "chatccc-im-skills-cache-"));
108
+ const skillDir = join(tempRoot, "feishu-skill");
109
+ await mkdir(skillDir);
110
+ await writeFile(join(skillDir, "skill.md"), "cwd={{cwd}}", "utf-8");
111
+
112
+ const input = {
113
+ skillsDir: tempRoot,
114
+ variables: { cwd: "C:/first", session_id: "sid_cache" },
115
+ };
116
+ const first = await buildImSkillsPromptCached(input);
117
+ await writeFile(join(skillDir, "skill.md"), "cwd={{cwd}} changed", "utf-8");
118
+ const cached = await buildImSkillsPromptCached(input);
119
+ clearImSkillsPromptCache("sid_cache");
120
+ const refreshed = await buildImSkillsPromptCached(input);
121
+
122
+ expect(cached).toBe(first);
123
+ expect(refreshed).toContain("changed");
124
+ });
100
125
  });
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { buildPlatformStartupPlan } from "../platform-startup.ts";
4
+
5
+ describe("buildPlatformStartupPlan", () => {
6
+ it("starts WeChat iLink without requiring Feishu when Feishu is disabled", () => {
7
+ expect(buildPlatformStartupPlan({ feishuEnabled: false, ilinkEnabled: true })).toEqual({
8
+ startFeishu: false,
9
+ startIlink: true,
10
+ });
11
+ });
12
+
13
+ it("can start both platforms when both are enabled", () => {
14
+ expect(buildPlatformStartupPlan({ feishuEnabled: true, ilinkEnabled: true })).toEqual({
15
+ startFeishu: true,
16
+ startIlink: true,
17
+ });
18
+ });
19
+ });
package/src/im-skills.ts CHANGED
@@ -70,6 +70,37 @@ export async function buildImSkillsPrompt(input: BuildImSkillsPromptInput): Prom
70
70
  .join("\n\n");
71
71
  }
72
72
 
73
+ // ---------------------------------------------------------------------------
74
+ // 会话级缓存:相同 session_id + cwd 的渲染结果完全相同,避免每轮重复读文件+渲染
75
+ // ---------------------------------------------------------------------------
76
+
77
+ const promptCache = new Map<string, string>();
78
+
79
+ function promptCacheKey(input: BuildImSkillsPromptInput): string {
80
+ const names = input.enabledSkillNames
81
+ ? [...input.enabledSkillNames].sort().join(",")
82
+ : "*";
83
+ const { session_id, cwd } = input.variables;
84
+ return `${input.skillsDir ?? DEFAULT_IM_SKILLS_DIR}|${names}|${session_id}|${cwd}`;
85
+ }
86
+
87
+ /** 带会话级缓存的 buildImSkillsPrompt。同 session + 同 cwd 时直接返回缓存的渲染结果。 */
88
+ export async function buildImSkillsPromptCached(input: BuildImSkillsPromptInput): Promise<string> {
89
+ const key = promptCacheKey(input);
90
+ const cached = promptCache.get(key);
91
+ if (cached !== undefined) return cached;
92
+ const result = await buildImSkillsPrompt(input);
93
+ promptCache.set(key, result);
94
+ return result;
95
+ }
96
+
97
+ /** 清除指定会话的缓存(如 /cd 后 cwd 变了,旧 key 不再需要) */
98
+ export function clearImSkillsPromptCache(sessionId: string): void {
99
+ for (const key of promptCache.keys()) {
100
+ if (key.includes(`|${sessionId}|`)) promptCache.delete(key);
101
+ }
102
+ }
103
+
73
104
  /**
74
105
  * 渲染技能目录下的子文档(skill.md 除外)并写入 outputDir。
75
106
  * 返回写入的文件路径列表。通常在会话初始化时调用,写入的文档供 Agent 按需读取。
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ import WebSocket from "ws";
29
29
 
30
30
  import { appendStartupTrace, attachRelayWebSocket, ensureSingleInstance, freeRelayListenPort, installCrashLogging, waitForPortFree } from "./shared.ts";
31
31
  import { createUiRouter, setExtraApiHandler, setReloadConfigHook, startSetupMode } from "./web-ui.ts";
32
+ import { buildPlatformStartupPlan } from "./platform-startup.ts";
32
33
  import { makeTraceId, logTrace } from "./trace.ts";
33
34
  import {
34
35
  CHATCCC_PORT,
@@ -668,6 +669,36 @@ async function startWechatSupervisor(): Promise<void> {
668
669
  console.log("[WX] 微信 iLink 平台已停止。");
669
670
  }
670
671
 
672
+ async function startConfiguredPlatforms(
673
+ httpServer: Server,
674
+ options: { failOnFeishuError: boolean },
675
+ ): Promise<void> {
676
+ const plan = buildPlatformStartupPlan({
677
+ feishuEnabled: FEISHU_ENABLED,
678
+ ilinkEnabled: ILINK_ENABLED,
679
+ });
680
+
681
+ if (plan.startFeishu) {
682
+ try {
683
+ await startBotService({ httpServer, port: CHATCCC_PORT });
684
+ } catch (err) {
685
+ if (options.failOnFeishuError) throw err;
686
+ console.error(`\n[飞书] 启动失败: ${(err as Error).message}`);
687
+ console.error("[飞书] 微信等其他平台不受影响,将继续启动。\n");
688
+ }
689
+ } else {
690
+ console.log("[飞书] 平台未启用,跳过飞书启动。");
691
+ }
692
+
693
+ if (plan.startIlink) {
694
+ startWechatSupervisor().catch((err) =>
695
+ console.error(`[WX] 微信 supervisor 异常退出: ${(err as Error).message}`),
696
+ );
697
+ } else {
698
+ console.log("[WX] 微信 iLink 未启用,跳过微信启动。");
699
+ }
700
+ }
701
+
671
702
  // ---------------------------------------------------------------------------
672
703
  // Main
673
704
  // ---------------------------------------------------------------------------
@@ -784,11 +815,11 @@ async function main(): Promise<void> {
784
815
  appIdMaskAfterReload: maskAppId(APP_ID),
785
816
  });
786
817
  try {
787
- await startBotService({ httpServer, port: CHATCCC_PORT });
818
+ await startConfiguredPlatforms(httpServer, { failOnFeishuError: true });
788
819
  installShutdownHandlers(httpServer);
789
820
  return { ok: true };
790
821
  } catch (err) {
791
- appendStartupTrace("setup-activate: startBotService failed", {
822
+ appendStartupTrace("setup-activate: startConfiguredPlatforms failed", {
792
823
  message: (err as Error).message,
793
824
  });
794
825
  return { ok: false, error: (err as Error).message };
@@ -822,19 +853,7 @@ async function main(): Promise<void> {
822
853
  process.exit(1);
823
854
  });
824
855
 
825
- if (FEISHU_ENABLED) {
826
- try {
827
- await startBotService({ httpServer, port: CHATCCC_PORT });
828
- } catch (err) {
829
- console.error(`\n[飞书] 启动失败: ${(err as Error).message}`);
830
- console.error("[飞书] 微信等其他平台不受影响,将继续启动。\n");
831
- }
832
- }
833
-
834
- // 启动微信 iLink 平台(后台运行,不阻塞飞书)
835
- startWechatSupervisor().catch((err) =>
836
- console.error(`[WX] 微信 supervisor 异常退出: ${(err as Error).message}`),
837
- );
856
+ await startConfiguredPlatforms(httpServer, { failOnFeishuError: false });
838
857
 
839
858
  installShutdownHandlers(httpServer);
840
859
  }
@@ -0,0 +1,16 @@
1
+ export interface PlatformStartupPlanInput {
2
+ feishuEnabled: boolean;
3
+ ilinkEnabled: boolean;
4
+ }
5
+
6
+ export interface PlatformStartupPlan {
7
+ startFeishu: boolean;
8
+ startIlink: boolean;
9
+ }
10
+
11
+ export function buildPlatformStartupPlan(input: PlatformStartupPlanInput): PlatformStartupPlan {
12
+ return {
13
+ startFeishu: input.feishuEnabled,
14
+ startIlink: input.ilinkEnabled,
15
+ };
16
+ }