chatccc 0.2.231 → 0.2.233
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/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/README.md +90 -90
- package/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +13 -13
- package/bin/cccagent.mjs +17 -17
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/im-skills/feishu-skill/skill.md +3 -3
- package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
- package/im-skills/wechat-file-skill/send-file.mjs +83 -83
- package/im-skills/wechat-file-skill/skill.md +10 -10
- package/im-skills/wechat-image-skill/skill.md +10 -10
- package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
- package/im-skills/wechat-video-skill/send-video.mjs +79 -79
- package/im-skills/wechat-video-skill/skill.md +10 -10
- package/package.json +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/__tests__/agent-activity.test.ts +86 -86
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
- package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
- package/src/__tests__/builtin-chat-session.test.ts +449 -393
- package/src/__tests__/builtin-cli-json.test.ts +39 -39
- package/src/__tests__/builtin-config.test.ts +26 -26
- package/src/__tests__/builtin-context.test.ts +319 -202
- package/src/__tests__/builtin-file-tools.test.ts +240 -240
- package/src/__tests__/builtin-permissions.test.ts +211 -211
- package/src/__tests__/builtin-session-search.test.ts +228 -0
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/builtin-skills.test.ts +284 -284
- package/src/__tests__/builtin-web-tools.test.ts +220 -220
- package/src/__tests__/card-action-routing.test.ts +18 -18
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/ccc-adapter.test.ts +170 -170
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
- package/src/__tests__/chatgpt-subscription.test.ts +135 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
- package/src/__tests__/claude-adapter.test.ts +614 -614
- package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
- package/src/__tests__/codex-adapter.test.ts +58 -58
- package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/crash-logging.test.ts +95 -1
- package/src/__tests__/cursor-adapter.test.ts +268 -268
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +164 -164
- package/src/__tests__/feishu-message-ingress.test.ts +138 -138
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/jsonl-stream.test.ts +79 -79
- package/src/__tests__/package-files.test.ts +24 -24
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/progress-reducer.test.ts +121 -121
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/response-stall.test.ts +49 -49
- package/src/__tests__/restart.test.ts +55 -10
- package/src/__tests__/session.test.ts +199 -199
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/sim-platform.test.ts +16 -16
- package/src/__tests__/startup-lifecycle.test.ts +231 -231
- package/src/__tests__/stop-session.test.ts +34 -34
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/terminal-error.test.ts +54 -54
- package/src/__tests__/terminal-renderer.test.ts +247 -247
- package/src/__tests__/update-command-guard.test.ts +144 -144
- package/src/__tests__/web-ui.test.ts +326 -326
- package/src/adapters/adapter-interface.ts +33 -33
- package/src/adapters/ccc-adapter.ts +141 -141
- package/src/adapters/claude-adapter.ts +620 -620
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +426 -426
- package/src/adapters/cursor-adapter.ts +681 -681
- package/src/adapters/jsonl-stream.ts +157 -157
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/adapters/resource-monitor.ts +140 -140
- package/src/agent-activity.ts +175 -175
- package/src/agent-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +91 -91
- package/src/agent-reload-config-rpc.ts +34 -34
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +6 -0
- package/src/builtin/config.ts +84 -84
- package/src/builtin/context.ts +465 -391
- package/src/builtin/file-log.ts +38 -38
- package/src/builtin/file-tools.ts +34 -0
- package/src/builtin/index.ts +24 -23
- package/src/builtin/proc-tree-kill.ts +61 -61
- package/src/builtin/progress/cards-helpers.ts +76 -76
- package/src/builtin/progress/reducer.ts +113 -113
- package/src/builtin/progress/terminal-renderer.ts +294 -294
- package/src/builtin/progress/view.ts +77 -77
- package/src/builtin/raw-stream-log.ts +124 -124
- package/src/builtin/session-search.ts +363 -0
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/sigint.ts +50 -50
- package/src/builtin/skills.ts +205 -205
- package/src/builtin/web-tools.ts +313 -313
- package/src/card-action-routing.ts +14 -14
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -318
- package/src/codex-reset-actions.ts +184 -184
- package/src/feishu-api.ts +193 -193
- package/src/feishu-message-ingress.ts +195 -195
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/index.ts +6 -1
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +36 -13
- package/src/platform-adapter.ts +6 -6
- package/src/privacy.ts +118 -118
- package/src/progress/reducer.ts +113 -113
- package/src/progress/terminal-renderer.ts +294 -294
- package/src/progress/view.ts +77 -77
- package/src/response-stall.ts +28 -28
- package/src/runtime-reload.ts +34 -34
- package/src/session-chat-binding.ts +82 -82
- package/src/session-name.ts +8 -8
- package/src/session.ts +201 -201
- package/src/shared-prefix.ts +29 -29
- package/src/shared.ts +44 -0
- package/src/sim-platform.ts +20 -20
- package/src/startup-lifecycle.ts +250 -250
- package/src/stream-state.ts +21 -21
- package/src/terminal-error.ts +129 -129
- package/src/turn-cards.ts +117 -117
- package/src/update-command-guard.ts +165 -165
package/src/orchestrator.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* 所有 IM 平台操作通过 PlatformAdapter 接口注入,不直接依赖 feishu-platform.ts。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { execSync, spawn, type ChildProcess } from "node:child_process";
|
|
8
|
+
import { execSync, spawn, type ChildProcess, type StdioOptions } from "node:child_process";
|
|
9
9
|
import { readdir, stat } from "node:fs/promises";
|
|
10
|
-
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { join, resolve, dirname } from "node:path";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
getAllModelsForTool,
|
|
28
28
|
getDefaultEffortForTool,
|
|
29
29
|
getDefaultCwd,
|
|
30
|
+
LOG_DIR,
|
|
30
31
|
setDefaultCwd,
|
|
31
32
|
getRecentDirs,
|
|
32
33
|
addRecentDir,
|
|
@@ -815,39 +816,61 @@ export interface RestartSpawnDeps {
|
|
|
815
816
|
projectRoot?: string;
|
|
816
817
|
spawnImpl?: typeof spawn;
|
|
817
818
|
trace?: typeof appendStartupTrace;
|
|
819
|
+
/** restart 子进程 stderr 的落盘目录;默认 ~/.chatccc/logs */
|
|
820
|
+
restartLogDir?: string;
|
|
818
821
|
}
|
|
819
822
|
|
|
820
823
|
/**
|
|
821
|
-
* spawn 自重启子进程。
|
|
822
|
-
*
|
|
824
|
+
* spawn 自重启子进程。
|
|
825
|
+
*
|
|
826
|
+
* stderr 不用 pipe 收集:pipe 读端随父进程退出关闭后,子进程(尤其经 tsx
|
|
827
|
+
* 包装)再写 stderr(如飞书 SDK 内部 console.warn)会 EPIPE → uncaughtException
|
|
828
|
+
* → 整个服务崩溃。改为把 stderr 重定向到磁盘日志文件:子进程继承文件句柄,
|
|
829
|
+
* 父进程退出不影响写入,启动诊断同样留痕。若日志文件打开失败,退回 pipe
|
|
830
|
+
* 收集(旧行为),并记录 trace。
|
|
823
831
|
*/
|
|
824
832
|
export function spawnRestartChild(deps: RestartSpawnDeps = {}): ChildProcess {
|
|
825
833
|
const projectRoot = deps.projectRoot ?? PROJECT_ROOT;
|
|
826
834
|
const spawnImpl = deps.spawnImpl ?? spawn;
|
|
827
835
|
const trace = deps.trace ?? appendStartupTrace;
|
|
836
|
+
const restartLogDir = deps.restartLogDir ?? LOG_DIR;
|
|
828
837
|
const { command, args } = buildRestartSpawnSpec(projectRoot);
|
|
838
|
+
|
|
839
|
+
let stderrFd: number | undefined;
|
|
840
|
+
const stdio: StdioOptions = ["ignore", "ignore", "pipe"];
|
|
841
|
+
try {
|
|
842
|
+
mkdirSync(restartLogDir, { recursive: true });
|
|
843
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
844
|
+
const restartLogPath = join(restartLogDir, `restart-${timestamp}.log`);
|
|
845
|
+
stderrFd = openSync(restartLogPath, "a");
|
|
846
|
+
stdio[2] = stderrFd;
|
|
847
|
+
} catch (err) {
|
|
848
|
+
trace("restart: stderr log open failed, falling back to pipe", {
|
|
849
|
+
error: err instanceof Error ? err.message : String(err),
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
829
853
|
const child = spawnImpl(command, args, {
|
|
830
854
|
cwd: projectRoot,
|
|
831
855
|
detached: true,
|
|
832
|
-
stdio
|
|
856
|
+
stdio,
|
|
833
857
|
shell: false,
|
|
834
858
|
env: createInternalRestartEnv(),
|
|
835
859
|
});
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
860
|
+
// 子进程已继承 stderr 文件句柄;父进程关闭自己的副本,避免"子进程早退、
|
|
861
|
+
// 父进程留下继续服务"时 fd 泄漏。
|
|
862
|
+
if (stderrFd !== undefined) {
|
|
863
|
+
try { closeSync(stderrFd); } catch { /* ignore */ }
|
|
864
|
+
}
|
|
865
|
+
|
|
842
866
|
child.on("error", (err) => {
|
|
843
|
-
trace("restart: spawn error", { error: err.message
|
|
867
|
+
trace("restart: spawn error", { error: err.message });
|
|
844
868
|
});
|
|
845
869
|
child.on("exit", (code, signal) => {
|
|
846
870
|
trace("restart: child exit", {
|
|
847
871
|
childPid: child.pid,
|
|
848
872
|
code,
|
|
849
873
|
signal,
|
|
850
|
-
stderr: stderrBuf.slice(0, 2000),
|
|
851
874
|
});
|
|
852
875
|
});
|
|
853
876
|
return child;
|
package/src/platform-adapter.ts
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
import type { CursorUsageSummary } from "./cursor-usage.ts";
|
|
9
9
|
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
10
10
|
|
|
11
|
-
export interface ChatAvatarUsageHints {
|
|
12
|
-
codexUsage?: CodexUsageSummary | null;
|
|
13
|
-
cursorUsage?: CursorUsageSummary | null;
|
|
14
|
-
fastMode?: boolean;
|
|
15
|
-
}
|
|
11
|
+
export interface ChatAvatarUsageHints {
|
|
12
|
+
codexUsage?: CodexUsageSummary | null;
|
|
13
|
+
cursorUsage?: CursorUsageSummary | null;
|
|
14
|
+
fastMode?: boolean;
|
|
15
|
+
}
|
|
16
16
|
|
|
17
17
|
export interface PlatformAdapter {
|
|
18
18
|
/** 平台标识,用于区分不同平台的行为(如 wechat、feishu 等) */
|
|
@@ -67,4 +67,4 @@ export interface PlatformAdapter {
|
|
|
67
67
|
|
|
68
68
|
/** 更新已发送的进度展示,sequence 保证有序 */
|
|
69
69
|
cardUpdate(cardId: string, cardJson: string, sequence: number): Promise<void>;
|
|
70
|
-
}
|
|
70
|
+
}
|
package/src/privacy.ts
CHANGED
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { USER_DATA_DIR } from "./config.ts";
|
|
4
|
-
|
|
5
|
-
interface PrivacyRules {
|
|
6
|
-
[key: string]: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface PrivacyConfig {
|
|
10
|
-
enabled: boolean;
|
|
11
|
-
rules: PrivacyRules;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let config: PrivacyConfig | null = null;
|
|
15
|
-
let loadedStamp: string | null | undefined;
|
|
16
|
-
|
|
17
|
-
function privacyFilePath(): string {
|
|
18
|
-
return join(USER_DATA_DIR, "privacy.json");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function privacyFileStamp(): string | null {
|
|
22
|
-
const filePath = privacyFilePath();
|
|
23
|
-
if (!existsSync(filePath)) return null;
|
|
24
|
-
try {
|
|
25
|
-
const s = statSync(filePath);
|
|
26
|
-
return `${s.mtimeMs}:${s.size}`;
|
|
27
|
-
} catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function sanitizeRules(raw: Record<string, unknown>): PrivacyRules {
|
|
33
|
-
const result: PrivacyRules = {};
|
|
34
|
-
for (const [from, to] of Object.entries(raw)) {
|
|
35
|
-
if (!from) {
|
|
36
|
-
console.error("[PRIVACY] privacy.json rule key cannot be empty, skipped");
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (typeof to !== "string") {
|
|
40
|
-
console.error(`[PRIVACY] privacy.json value must be a string, skipped "${from}"`);
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
result[from] = to;
|
|
44
|
-
}
|
|
45
|
-
return result;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function normalizeConfig(parsed: Record<string, unknown>): PrivacyConfig {
|
|
49
|
-
const hasNewSchema = Object.prototype.hasOwnProperty.call(parsed, "enabled") ||
|
|
50
|
-
Object.prototype.hasOwnProperty.call(parsed, "rules");
|
|
51
|
-
|
|
52
|
-
if (!hasNewSchema) {
|
|
53
|
-
return { enabled: true, rules: sanitizeRules(parsed) };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const enabledRaw = parsed.enabled;
|
|
57
|
-
const enabled = enabledRaw === undefined ? true : enabledRaw !== false;
|
|
58
|
-
if (enabledRaw !== undefined && typeof enabledRaw !== "boolean") {
|
|
59
|
-
console.error("[PRIVACY] privacy.json enabled must be a boolean, using true");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const rulesRaw = parsed.rules;
|
|
63
|
-
if (rulesRaw === undefined) return { enabled, rules: {} };
|
|
64
|
-
if (typeof rulesRaw !== "object" || rulesRaw === null || Array.isArray(rulesRaw)) {
|
|
65
|
-
console.error("[PRIVACY] privacy.json rules must be an object");
|
|
66
|
-
return { enabled, rules: {} };
|
|
67
|
-
}
|
|
68
|
-
return { enabled, rules: sanitizeRules(rulesRaw as Record<string, unknown>) };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function loadConfig(): PrivacyConfig {
|
|
72
|
-
const filePath = privacyFilePath();
|
|
73
|
-
if (!existsSync(filePath)) {
|
|
74
|
-
return { enabled: true, rules: {} };
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
const raw = readFileSync(filePath, "utf-8").replace(/^\uFEFF/, "");
|
|
78
|
-
const parsed = JSON.parse(raw);
|
|
79
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
80
|
-
console.error("[PRIVACY] privacy.json must be an object");
|
|
81
|
-
return { enabled: true, rules: {} };
|
|
82
|
-
}
|
|
83
|
-
return normalizeConfig(parsed as Record<string, unknown>);
|
|
84
|
-
} catch (err) {
|
|
85
|
-
console.error(`[PRIVACY] failed to read privacy.json: ${(err as Error).message}`);
|
|
86
|
-
return { enabled: true, rules: {} };
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function getPrivacyConfig(): PrivacyConfig {
|
|
91
|
-
const stamp = privacyFileStamp();
|
|
92
|
-
if (!config || stamp !== loadedStamp) {
|
|
93
|
-
config = loadConfig();
|
|
94
|
-
loadedStamp = stamp;
|
|
95
|
-
}
|
|
96
|
-
return config;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function getPrivacyRules(): PrivacyRules {
|
|
100
|
-
return getPrivacyConfig().rules;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function reloadPrivacyRules(): void {
|
|
104
|
-
config = null;
|
|
105
|
-
loadedStamp = undefined;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function applyPrivacy(text: string): string {
|
|
109
|
-
const { enabled, rules } = getPrivacyConfig();
|
|
110
|
-
if (!enabled || Object.keys(rules).length === 0 || !text) return text;
|
|
111
|
-
|
|
112
|
-
let result = text;
|
|
113
|
-
for (const [from, to] of Object.entries(rules)) {
|
|
114
|
-
// Use split+join instead of regex replacement so rule keys stay literal.
|
|
115
|
-
result = result.split(from).join(to);
|
|
116
|
-
}
|
|
117
|
-
return result;
|
|
118
|
-
}
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { USER_DATA_DIR } from "./config.ts";
|
|
4
|
+
|
|
5
|
+
interface PrivacyRules {
|
|
6
|
+
[key: string]: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface PrivacyConfig {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
rules: PrivacyRules;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let config: PrivacyConfig | null = null;
|
|
15
|
+
let loadedStamp: string | null | undefined;
|
|
16
|
+
|
|
17
|
+
function privacyFilePath(): string {
|
|
18
|
+
return join(USER_DATA_DIR, "privacy.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function privacyFileStamp(): string | null {
|
|
22
|
+
const filePath = privacyFilePath();
|
|
23
|
+
if (!existsSync(filePath)) return null;
|
|
24
|
+
try {
|
|
25
|
+
const s = statSync(filePath);
|
|
26
|
+
return `${s.mtimeMs}:${s.size}`;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sanitizeRules(raw: Record<string, unknown>): PrivacyRules {
|
|
33
|
+
const result: PrivacyRules = {};
|
|
34
|
+
for (const [from, to] of Object.entries(raw)) {
|
|
35
|
+
if (!from) {
|
|
36
|
+
console.error("[PRIVACY] privacy.json rule key cannot be empty, skipped");
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (typeof to !== "string") {
|
|
40
|
+
console.error(`[PRIVACY] privacy.json value must be a string, skipped "${from}"`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
result[from] = to;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeConfig(parsed: Record<string, unknown>): PrivacyConfig {
|
|
49
|
+
const hasNewSchema = Object.prototype.hasOwnProperty.call(parsed, "enabled") ||
|
|
50
|
+
Object.prototype.hasOwnProperty.call(parsed, "rules");
|
|
51
|
+
|
|
52
|
+
if (!hasNewSchema) {
|
|
53
|
+
return { enabled: true, rules: sanitizeRules(parsed) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const enabledRaw = parsed.enabled;
|
|
57
|
+
const enabled = enabledRaw === undefined ? true : enabledRaw !== false;
|
|
58
|
+
if (enabledRaw !== undefined && typeof enabledRaw !== "boolean") {
|
|
59
|
+
console.error("[PRIVACY] privacy.json enabled must be a boolean, using true");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rulesRaw = parsed.rules;
|
|
63
|
+
if (rulesRaw === undefined) return { enabled, rules: {} };
|
|
64
|
+
if (typeof rulesRaw !== "object" || rulesRaw === null || Array.isArray(rulesRaw)) {
|
|
65
|
+
console.error("[PRIVACY] privacy.json rules must be an object");
|
|
66
|
+
return { enabled, rules: {} };
|
|
67
|
+
}
|
|
68
|
+
return { enabled, rules: sanitizeRules(rulesRaw as Record<string, unknown>) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function loadConfig(): PrivacyConfig {
|
|
72
|
+
const filePath = privacyFilePath();
|
|
73
|
+
if (!existsSync(filePath)) {
|
|
74
|
+
return { enabled: true, rules: {} };
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const raw = readFileSync(filePath, "utf-8").replace(/^\uFEFF/, "");
|
|
78
|
+
const parsed = JSON.parse(raw);
|
|
79
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
80
|
+
console.error("[PRIVACY] privacy.json must be an object");
|
|
81
|
+
return { enabled: true, rules: {} };
|
|
82
|
+
}
|
|
83
|
+
return normalizeConfig(parsed as Record<string, unknown>);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error(`[PRIVACY] failed to read privacy.json: ${(err as Error).message}`);
|
|
86
|
+
return { enabled: true, rules: {} };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getPrivacyConfig(): PrivacyConfig {
|
|
91
|
+
const stamp = privacyFileStamp();
|
|
92
|
+
if (!config || stamp !== loadedStamp) {
|
|
93
|
+
config = loadConfig();
|
|
94
|
+
loadedStamp = stamp;
|
|
95
|
+
}
|
|
96
|
+
return config;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getPrivacyRules(): PrivacyRules {
|
|
100
|
+
return getPrivacyConfig().rules;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function reloadPrivacyRules(): void {
|
|
104
|
+
config = null;
|
|
105
|
+
loadedStamp = undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function applyPrivacy(text: string): string {
|
|
109
|
+
const { enabled, rules } = getPrivacyConfig();
|
|
110
|
+
if (!enabled || Object.keys(rules).length === 0 || !text) return text;
|
|
111
|
+
|
|
112
|
+
let result = text;
|
|
113
|
+
for (const [from, to] of Object.entries(rules)) {
|
|
114
|
+
// Use split+join instead of regex replacement so rule keys stay literal.
|
|
115
|
+
result = result.split(from).join(to);
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
package/src/progress/reducer.ts
CHANGED
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* progress/reducer.ts — ChatEvent 事件流 → ProgressView 的增量合并
|
|
3
|
-
*
|
|
4
|
-
* 这是飞书与终端共享的唯一一份"事件 → 过程视图"公共逻辑:
|
|
5
|
-
* 终端 REPL 直接消费 ChatSession.chat() 的事件流,逐条 reduce;
|
|
6
|
-
* 飞书侧 display loop 若未来接入事件流,同样使用本 reducer,保证两端
|
|
7
|
-
* 看到的过程状态(正文累积、工具调用、终态判定)完全一致。
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { ChatEvent } from "../builtin/index.ts";
|
|
11
|
-
import {
|
|
12
|
-
withProgressView,
|
|
13
|
-
type ProgressToolCall,
|
|
14
|
-
type ProgressView,
|
|
15
|
-
} from "./view.ts";
|
|
16
|
-
|
|
17
|
-
/** 工具调用 input 单行摘要(终端折叠行展示用) */
|
|
18
|
-
export function summarizeToolInput(input: unknown, maxChars = 120): string {
|
|
19
|
-
let raw: string;
|
|
20
|
-
if (typeof input === "string") {
|
|
21
|
-
raw = input;
|
|
22
|
-
} else {
|
|
23
|
-
try {
|
|
24
|
-
raw = JSON.stringify(input);
|
|
25
|
-
} catch {
|
|
26
|
-
raw = String(input);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
const oneLine = raw.replace(/\s+/g, " ").trim();
|
|
30
|
-
return oneLine.length > maxChars ? oneLine.slice(0, maxChars) + "…" : oneLine;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** 工具结果单行摘要(成功/失败各取首行) */
|
|
34
|
-
export function summarizeToolResult(content: unknown, maxChars = 120): string {
|
|
35
|
-
let raw: string;
|
|
36
|
-
if (typeof content === "string") {
|
|
37
|
-
raw = content;
|
|
38
|
-
} else if (content instanceof Error) {
|
|
39
|
-
raw = content.message;
|
|
40
|
-
} else {
|
|
41
|
-
try {
|
|
42
|
-
raw = JSON.stringify(content);
|
|
43
|
-
} catch {
|
|
44
|
-
raw = String(content);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
const firstLine = raw.split("\n")[0] ?? "";
|
|
48
|
-
return firstLine.length > maxChars ? firstLine.slice(0, maxChars) + "…" : firstLine;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* 把一条 ChatEvent 合并进 ProgressView,返回新视图(不可变更新)。
|
|
53
|
-
* 未识别的/不影响展示的事件(如 compact)返回原视图。
|
|
54
|
-
*/
|
|
55
|
-
export function reduceProgress(prev: ProgressView, event: ChatEvent): ProgressView {
|
|
56
|
-
switch (event.type) {
|
|
57
|
-
case "status":
|
|
58
|
-
return withProgressView(prev, {
|
|
59
|
-
headerTitle: event.phase === "compacting" ? "压缩上下文中..." : "生成回复中...",
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
case "text":
|
|
63
|
-
// accumulated 是全文累积,直接全量替换,天然幂等
|
|
64
|
-
return withProgressView(prev, { text: event.accumulated });
|
|
65
|
-
|
|
66
|
-
case "tool_use": {
|
|
67
|
-
const tool: ProgressToolCall = {
|
|
68
|
-
id: event.id ?? `tool-${prev.tools.length + 1}`,
|
|
69
|
-
name: event.name,
|
|
70
|
-
status: "running",
|
|
71
|
-
detail: summarizeToolInput(event.input),
|
|
72
|
-
};
|
|
73
|
-
return withProgressView(prev, { tools: [...prev.tools, tool] });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
case "tool_result": {
|
|
77
|
-
const nextStatus = event.is_error ? ("error" as const) : ("ok" as const);
|
|
78
|
-
const summary = summarizeToolResult(event.content);
|
|
79
|
-
let matched = false;
|
|
80
|
-
const tools = prev.tools.map((t) => {
|
|
81
|
-
if (matched || t.id !== event.tool_use_id) return t;
|
|
82
|
-
matched = true;
|
|
83
|
-
return { ...t, status: nextStatus, summary };
|
|
84
|
-
});
|
|
85
|
-
if (!matched) {
|
|
86
|
-
// id 缺失或失配:兜底更新最后一个 running 工具,避免状态悬挂
|
|
87
|
-
const lastRunning = [...prev.tools].reverse().findIndex((t) => t.status === "running");
|
|
88
|
-
if (lastRunning >= 0) {
|
|
89
|
-
const idx = prev.tools.length - 1 - lastRunning;
|
|
90
|
-
const updated = prev.tools.map((t, i) =>
|
|
91
|
-
i === idx ? { ...t, status: nextStatus, summary } : t,
|
|
92
|
-
);
|
|
93
|
-
return withProgressView(prev, { tools: updated });
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return withProgressView(prev, { tools });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
case "done":
|
|
100
|
-
return withProgressView(prev, {
|
|
101
|
-
status: "done",
|
|
102
|
-
showStop: false,
|
|
103
|
-
text: event.text,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
case "error":
|
|
107
|
-
return withProgressView(prev, { status: "error", showStop: false });
|
|
108
|
-
|
|
109
|
-
case "compact":
|
|
110
|
-
// 旧上下文压缩不影响当前过程展示
|
|
111
|
-
return prev;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* progress/reducer.ts — ChatEvent 事件流 → ProgressView 的增量合并
|
|
3
|
+
*
|
|
4
|
+
* 这是飞书与终端共享的唯一一份"事件 → 过程视图"公共逻辑:
|
|
5
|
+
* 终端 REPL 直接消费 ChatSession.chat() 的事件流,逐条 reduce;
|
|
6
|
+
* 飞书侧 display loop 若未来接入事件流,同样使用本 reducer,保证两端
|
|
7
|
+
* 看到的过程状态(正文累积、工具调用、终态判定)完全一致。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ChatEvent } from "../builtin/index.ts";
|
|
11
|
+
import {
|
|
12
|
+
withProgressView,
|
|
13
|
+
type ProgressToolCall,
|
|
14
|
+
type ProgressView,
|
|
15
|
+
} from "./view.ts";
|
|
16
|
+
|
|
17
|
+
/** 工具调用 input 单行摘要(终端折叠行展示用) */
|
|
18
|
+
export function summarizeToolInput(input: unknown, maxChars = 120): string {
|
|
19
|
+
let raw: string;
|
|
20
|
+
if (typeof input === "string") {
|
|
21
|
+
raw = input;
|
|
22
|
+
} else {
|
|
23
|
+
try {
|
|
24
|
+
raw = JSON.stringify(input);
|
|
25
|
+
} catch {
|
|
26
|
+
raw = String(input);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const oneLine = raw.replace(/\s+/g, " ").trim();
|
|
30
|
+
return oneLine.length > maxChars ? oneLine.slice(0, maxChars) + "…" : oneLine;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 工具结果单行摘要(成功/失败各取首行) */
|
|
34
|
+
export function summarizeToolResult(content: unknown, maxChars = 120): string {
|
|
35
|
+
let raw: string;
|
|
36
|
+
if (typeof content === "string") {
|
|
37
|
+
raw = content;
|
|
38
|
+
} else if (content instanceof Error) {
|
|
39
|
+
raw = content.message;
|
|
40
|
+
} else {
|
|
41
|
+
try {
|
|
42
|
+
raw = JSON.stringify(content);
|
|
43
|
+
} catch {
|
|
44
|
+
raw = String(content);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const firstLine = raw.split("\n")[0] ?? "";
|
|
48
|
+
return firstLine.length > maxChars ? firstLine.slice(0, maxChars) + "…" : firstLine;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 把一条 ChatEvent 合并进 ProgressView,返回新视图(不可变更新)。
|
|
53
|
+
* 未识别的/不影响展示的事件(如 compact)返回原视图。
|
|
54
|
+
*/
|
|
55
|
+
export function reduceProgress(prev: ProgressView, event: ChatEvent): ProgressView {
|
|
56
|
+
switch (event.type) {
|
|
57
|
+
case "status":
|
|
58
|
+
return withProgressView(prev, {
|
|
59
|
+
headerTitle: event.phase === "compacting" ? "压缩上下文中..." : "生成回复中...",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
case "text":
|
|
63
|
+
// accumulated 是全文累积,直接全量替换,天然幂等
|
|
64
|
+
return withProgressView(prev, { text: event.accumulated });
|
|
65
|
+
|
|
66
|
+
case "tool_use": {
|
|
67
|
+
const tool: ProgressToolCall = {
|
|
68
|
+
id: event.id ?? `tool-${prev.tools.length + 1}`,
|
|
69
|
+
name: event.name,
|
|
70
|
+
status: "running",
|
|
71
|
+
detail: summarizeToolInput(event.input),
|
|
72
|
+
};
|
|
73
|
+
return withProgressView(prev, { tools: [...prev.tools, tool] });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case "tool_result": {
|
|
77
|
+
const nextStatus = event.is_error ? ("error" as const) : ("ok" as const);
|
|
78
|
+
const summary = summarizeToolResult(event.content);
|
|
79
|
+
let matched = false;
|
|
80
|
+
const tools = prev.tools.map((t) => {
|
|
81
|
+
if (matched || t.id !== event.tool_use_id) return t;
|
|
82
|
+
matched = true;
|
|
83
|
+
return { ...t, status: nextStatus, summary };
|
|
84
|
+
});
|
|
85
|
+
if (!matched) {
|
|
86
|
+
// id 缺失或失配:兜底更新最后一个 running 工具,避免状态悬挂
|
|
87
|
+
const lastRunning = [...prev.tools].reverse().findIndex((t) => t.status === "running");
|
|
88
|
+
if (lastRunning >= 0) {
|
|
89
|
+
const idx = prev.tools.length - 1 - lastRunning;
|
|
90
|
+
const updated = prev.tools.map((t, i) =>
|
|
91
|
+
i === idx ? { ...t, status: nextStatus, summary } : t,
|
|
92
|
+
);
|
|
93
|
+
return withProgressView(prev, { tools: updated });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return withProgressView(prev, { tools });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case "done":
|
|
100
|
+
return withProgressView(prev, {
|
|
101
|
+
status: "done",
|
|
102
|
+
showStop: false,
|
|
103
|
+
text: event.text,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
case "error":
|
|
107
|
+
return withProgressView(prev, { status: "error", showStop: false });
|
|
108
|
+
|
|
109
|
+
case "compact":
|
|
110
|
+
// 旧上下文压缩不影响当前过程展示
|
|
111
|
+
return prev;
|
|
112
|
+
}
|
|
113
|
+
}
|