chatccc 0.2.176 → 0.2.177
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 +3 -3
- package/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +2 -2
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/package.json +1 -1
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/claude-adapter.test.ts +592 -592
- package/src/__tests__/config-reload.test.ts +18 -18
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +129 -129
- package/src/__tests__/feishu-platform.test.ts +16 -16
- package/src/__tests__/format-message.test.ts +272 -272
- package/src/__tests__/orchestrator.test.ts +93 -93
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/web-ui.test.ts +92 -92
- package/src/adapters/claude-adapter.ts +566 -566
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/agent-stop-stuck.ts +129 -129
- package/src/cards.ts +4 -4
- package/src/feishu-api.ts +2 -2
- package/src/feishu-platform.ts +15 -15
- package/src/format-message.ts +213 -213
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +112 -112
- package/src/privacy.ts +118 -118
- package/src/session-chat-binding.ts +6 -6
- package/src/sim-platform.ts +14 -14
- package/src/web-ui.ts +6 -6
package/src/orchestrator.ts
CHANGED
|
@@ -78,10 +78,10 @@ import {
|
|
|
78
78
|
enqueueMessage,
|
|
79
79
|
cancelQueuedMessage,
|
|
80
80
|
} from "./session-chat-binding.ts";
|
|
81
|
-
import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
|
|
82
|
-
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
83
|
-
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
84
|
-
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
81
|
+
import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
|
|
82
|
+
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
83
|
+
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
84
|
+
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
85
85
|
|
|
86
86
|
// ---------------------------------------------------------------------------
|
|
87
87
|
// 辅助函数
|
|
@@ -97,7 +97,7 @@ export function sessionChatName(left: string, cwd: string): string {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/** 模型模糊匹配:精确匹配优先,否则找子串匹配(模型名越短越优先) */
|
|
100
|
-
function findModelMatch(input: string, models: string[]): string | null {
|
|
100
|
+
function findModelMatch(input: string, models: string[]): string | null {
|
|
101
101
|
if (models.length === 0) return null;
|
|
102
102
|
const inputLower = input.toLowerCase();
|
|
103
103
|
// 1) 精确匹配(忽略大小写)
|
|
@@ -109,72 +109,72 @@ function findModelMatch(input: string, models: string[]): string | null {
|
|
|
109
109
|
.filter(m => m.toLowerCase().includes(inputLower))
|
|
110
110
|
.sort((a, b) => a.length - b.length);
|
|
111
111
|
return candidates[0] ?? null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function formatCodexUsageSummary(usage: CodexUsageSummary): string {
|
|
115
|
-
const progressBar = (usedPercent: number) => {
|
|
116
|
-
const width = 20;
|
|
117
|
-
const usedBlocks = Math.max(0, Math.min(width, Math.round((usedPercent / 100) * width)));
|
|
118
|
-
return `[${"█".repeat(usedBlocks)}${"░".repeat(width - usedBlocks)}]`;
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const formatDuration = (seconds: number | null) => {
|
|
122
|
-
if (seconds === null) return "";
|
|
123
|
-
if (seconds <= 0) return "(已到重置时间)";
|
|
124
|
-
const totalMinutes = Math.max(1, Math.floor(seconds / 60));
|
|
125
|
-
const days = Math.floor(totalMinutes / 1440);
|
|
126
|
-
const hours = Math.floor((totalMinutes % 1440) / 60);
|
|
127
|
-
const minutes = totalMinutes % 60;
|
|
128
|
-
const parts: string[] = [];
|
|
129
|
-
if (days > 0) parts.push(`${days}天`);
|
|
130
|
-
if (hours > 0) parts.push(`${hours}小时`);
|
|
131
|
-
if (minutes > 0 || parts.length === 0) parts.push(`${minutes}分钟`);
|
|
132
|
-
return `(约 ${parts.join("")}后)`;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const formatResetTime = (balance: CodexUsageSummary["fiveHour"]) => {
|
|
136
|
-
if (balance.resetAtEpochSeconds === null) return "暂无数据";
|
|
137
|
-
const date = new Date(balance.resetAtEpochSeconds * 1000);
|
|
138
|
-
const pad = (value: number) => String(value).padStart(2, "0");
|
|
139
|
-
const absolute = [
|
|
140
|
-
date.getFullYear(),
|
|
141
|
-
"-",
|
|
142
|
-
pad(date.getMonth() + 1),
|
|
143
|
-
"-",
|
|
144
|
-
pad(date.getDate()),
|
|
145
|
-
" ",
|
|
146
|
-
pad(date.getHours()),
|
|
147
|
-
":",
|
|
148
|
-
pad(date.getMinutes()),
|
|
149
|
-
].join("");
|
|
150
|
-
return `${absolute}${formatDuration(balance.resetAfterSeconds)}`;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const formatWindow = (label: string, balance: CodexUsageSummary["fiveHour"] | null) => {
|
|
154
|
-
if (!balance) return `**${label}:** 暂无数据`;
|
|
155
|
-
return [
|
|
156
|
-
`**${label}:** 已用 ${balance.usedPercent}%,剩余 ${balance.remainingPercent}%,重置: ${formatResetTime(balance)}`,
|
|
157
|
-
progressBar(balance.usedPercent),
|
|
158
|
-
].join("\n");
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
return [
|
|
162
|
-
"Codex 用量:",
|
|
163
|
-
"",
|
|
164
|
-
formatWindow("5h", usage.fiveHour),
|
|
165
|
-
formatWindow("周", usage.weekly),
|
|
166
|
-
].join("\n");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function codexUsageHelpLine(tool: string): string {
|
|
170
|
-
return tool === "codex"
|
|
171
|
-
? "\n发送 **/usage** 查看 Codex 5h 和周用量。"
|
|
172
|
-
: "";
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function isUntitledSessionChatName(name: string): boolean {
|
|
176
|
-
return name === "新会话" || name.startsWith("新会话-");
|
|
177
|
-
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatCodexUsageSummary(usage: CodexUsageSummary): string {
|
|
115
|
+
const progressBar = (usedPercent: number) => {
|
|
116
|
+
const width = 20;
|
|
117
|
+
const usedBlocks = Math.max(0, Math.min(width, Math.round((usedPercent / 100) * width)));
|
|
118
|
+
return `[${"█".repeat(usedBlocks)}${"░".repeat(width - usedBlocks)}]`;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const formatDuration = (seconds: number | null) => {
|
|
122
|
+
if (seconds === null) return "";
|
|
123
|
+
if (seconds <= 0) return "(已到重置时间)";
|
|
124
|
+
const totalMinutes = Math.max(1, Math.floor(seconds / 60));
|
|
125
|
+
const days = Math.floor(totalMinutes / 1440);
|
|
126
|
+
const hours = Math.floor((totalMinutes % 1440) / 60);
|
|
127
|
+
const minutes = totalMinutes % 60;
|
|
128
|
+
const parts: string[] = [];
|
|
129
|
+
if (days > 0) parts.push(`${days}天`);
|
|
130
|
+
if (hours > 0) parts.push(`${hours}小时`);
|
|
131
|
+
if (minutes > 0 || parts.length === 0) parts.push(`${minutes}分钟`);
|
|
132
|
+
return `(约 ${parts.join("")}后)`;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const formatResetTime = (balance: CodexUsageSummary["fiveHour"]) => {
|
|
136
|
+
if (balance.resetAtEpochSeconds === null) return "暂无数据";
|
|
137
|
+
const date = new Date(balance.resetAtEpochSeconds * 1000);
|
|
138
|
+
const pad = (value: number) => String(value).padStart(2, "0");
|
|
139
|
+
const absolute = [
|
|
140
|
+
date.getFullYear(),
|
|
141
|
+
"-",
|
|
142
|
+
pad(date.getMonth() + 1),
|
|
143
|
+
"-",
|
|
144
|
+
pad(date.getDate()),
|
|
145
|
+
" ",
|
|
146
|
+
pad(date.getHours()),
|
|
147
|
+
":",
|
|
148
|
+
pad(date.getMinutes()),
|
|
149
|
+
].join("");
|
|
150
|
+
return `${absolute}${formatDuration(balance.resetAfterSeconds)}`;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const formatWindow = (label: string, balance: CodexUsageSummary["fiveHour"] | null) => {
|
|
154
|
+
if (!balance) return `**${label}:** 暂无数据`;
|
|
155
|
+
return [
|
|
156
|
+
`**${label}:** 已用 ${balance.usedPercent}%,剩余 ${balance.remainingPercent}%,重置: ${formatResetTime(balance)}`,
|
|
157
|
+
progressBar(balance.usedPercent),
|
|
158
|
+
].join("\n");
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return [
|
|
162
|
+
"Codex 用量:",
|
|
163
|
+
"",
|
|
164
|
+
formatWindow("5h", usage.fiveHour),
|
|
165
|
+
formatWindow("周", usage.weekly),
|
|
166
|
+
].join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function codexUsageHelpLine(tool: string): string {
|
|
170
|
+
return tool === "codex"
|
|
171
|
+
? "\n发送 **/usage** 查看 Codex 5h 和周用量。"
|
|
172
|
+
: "";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isUntitledSessionChatName(name: string): boolean {
|
|
176
|
+
return name === "新会话" || name.startsWith("新会话-");
|
|
177
|
+
}
|
|
178
178
|
|
|
179
179
|
function shouldSendWechatProcessingAck(
|
|
180
180
|
platform: PlatformAdapter,
|
|
@@ -354,7 +354,7 @@ export async function handleCommand(
|
|
|
354
354
|
return;
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
if (textLower === "/updateg") {
|
|
357
|
+
if (textLower === "/updateg") {
|
|
358
358
|
logTrace(tid, "BRANCH", { cmd: "/updateg" });
|
|
359
359
|
const isGlobal = isRunningFromGlobalNpm();
|
|
360
360
|
appendStartupTrace("updateg: command received", { isGlobal, chatId });
|
|
@@ -368,32 +368,32 @@ export async function handleCommand(
|
|
|
368
368
|
appendStartupTrace("updateg: sync update begin", { fromPid: process.pid });
|
|
369
369
|
syncUpdateAndRestart();
|
|
370
370
|
setTimeout(() => process.exit(0), 2000);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (textLower === "/usage") {
|
|
375
|
-
logTrace(tid, "BRANCH", { cmd: "/usage", tool: "codex" });
|
|
376
|
-
try {
|
|
377
|
-
const content = formatCodexUsageSummary(await getCodexUsageSummary());
|
|
378
|
-
if (platform.kind === "wechat") {
|
|
379
|
-
await platform.sendText(chatId, content).catch(() => {});
|
|
380
|
-
} else {
|
|
381
|
-
await platform.sendCard(chatId, "Codex Usage", content, "blue");
|
|
382
|
-
}
|
|
383
|
-
logTrace(tid, "DONE", { outcome: "usage" });
|
|
384
|
-
} catch (err) {
|
|
385
|
-
const message = `无法获取 Codex 用量:${(err as Error).message}`;
|
|
386
|
-
if (platform.kind === "wechat") {
|
|
387
|
-
await platform.sendText(chatId, message).catch(() => {});
|
|
388
|
-
} else {
|
|
389
|
-
await platform.sendCard(chatId, "Codex Usage", message, "red");
|
|
390
|
-
}
|
|
391
|
-
logTrace(tid, "DONE", { outcome: "usage_fail", error: (err as Error).message });
|
|
392
|
-
}
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (textLower === "/cd" || textLower.startsWith("/cd ")) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (textLower === "/usage") {
|
|
375
|
+
logTrace(tid, "BRANCH", { cmd: "/usage", tool: "codex" });
|
|
376
|
+
try {
|
|
377
|
+
const content = formatCodexUsageSummary(await getCodexUsageSummary());
|
|
378
|
+
if (platform.kind === "wechat") {
|
|
379
|
+
await platform.sendText(chatId, content).catch(() => {});
|
|
380
|
+
} else {
|
|
381
|
+
await platform.sendCard(chatId, "Codex Usage", content, "blue");
|
|
382
|
+
}
|
|
383
|
+
logTrace(tid, "DONE", { outcome: "usage" });
|
|
384
|
+
} catch (err) {
|
|
385
|
+
const message = `无法获取 Codex 用量:${(err as Error).message}`;
|
|
386
|
+
if (platform.kind === "wechat") {
|
|
387
|
+
await platform.sendText(chatId, message).catch(() => {});
|
|
388
|
+
} else {
|
|
389
|
+
await platform.sendCard(chatId, "Codex Usage", message, "red");
|
|
390
|
+
}
|
|
391
|
+
logTrace(tid, "DONE", { outcome: "usage_fail", error: (err as Error).message });
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (textLower === "/cd" || textLower.startsWith("/cd ")) {
|
|
397
397
|
logTrace(tid, "BRANCH", {
|
|
398
398
|
cmd: "/cd",
|
|
399
399
|
arg: text.slice(3).trim() || "(none)",
|
|
@@ -612,13 +612,13 @@ export async function handleCommand(
|
|
|
612
612
|
`**工作目录:** \`${cwd}\`\n\n` +
|
|
613
613
|
`直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
|
|
614
614
|
`发送 **/cd** 切换新建会话的默认目录。\n` +
|
|
615
|
-
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
616
|
-
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
617
|
-
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
618
|
-
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
619
|
-
codexUsageHelpLine(tool),
|
|
620
|
-
"green",
|
|
621
|
-
);
|
|
615
|
+
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
616
|
+
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
617
|
+
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
618
|
+
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
619
|
+
codexUsageHelpLine(tool),
|
|
620
|
+
"green",
|
|
621
|
+
);
|
|
622
622
|
console.log(
|
|
623
623
|
`[${ts()}] [NEW] P2P session created: ${sessionId} (${toolLabel})`,
|
|
624
624
|
);
|
|
@@ -699,13 +699,13 @@ export async function handleCommand(
|
|
|
699
699
|
`**工作目录:** \`${cwd}\`\n\n` +
|
|
700
700
|
`直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
|
|
701
701
|
`发送 **/cd** 切换新建会话的默认目录。\n` +
|
|
702
|
-
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
703
|
-
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
704
|
-
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
705
|
-
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
706
|
-
codexUsageHelpLine(tool),
|
|
707
|
-
"green",
|
|
708
|
-
);
|
|
702
|
+
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
703
|
+
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
704
|
+
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
705
|
+
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
706
|
+
codexUsageHelpLine(tool),
|
|
707
|
+
"green",
|
|
708
|
+
);
|
|
709
709
|
|
|
710
710
|
console.log(`[${ts()}] [STEP 4/4] Replied to new group → OK`);
|
|
711
711
|
logTrace(tid, "DONE", {
|
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
|
+
}
|
|
@@ -71,11 +71,11 @@ export interface ActivePrompt {
|
|
|
71
71
|
/** Set when the watchdog detects that the CLI process disappeared before stream finalization. */
|
|
72
72
|
abnormalExit?: boolean;
|
|
73
73
|
abnormalExitNotified?: boolean;
|
|
74
|
-
/** Set when the resource monitor detects CPU + memory unchanged for 3 minutes. */
|
|
75
|
-
resourceStuck?: boolean;
|
|
76
|
-
/** Adapter-provided callback to close the underlying SDK session / subprocess.
|
|
77
|
-
* Called by stop-stuck-loop before controller.abort() to terminate the CLI
|
|
78
|
-
* process immediately, rather than waiting for the async generator to unblock. */
|
|
74
|
+
/** Set when the resource monitor detects CPU + memory unchanged for 3 minutes. */
|
|
75
|
+
resourceStuck?: boolean;
|
|
76
|
+
/** Adapter-provided callback to close the underlying SDK session / subprocess.
|
|
77
|
+
* Called by stop-stuck-loop before controller.abort() to terminate the CLI
|
|
78
|
+
* process immediately, rather than waiting for the async generator to unblock. */
|
|
79
79
|
closeSession?: () => void;
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -228,4 +228,4 @@ export function resetBindingState(): void {
|
|
|
228
228
|
clearInterval(unifiedDisplayLoopHandle);
|
|
229
229
|
unifiedDisplayLoopHandle = null;
|
|
230
230
|
}
|
|
231
|
-
}
|
|
231
|
+
}
|
package/src/sim-platform.ts
CHANGED
|
@@ -123,19 +123,19 @@ export const SimulatedPlatform: FeishuPlatform = {
|
|
|
123
123
|
},
|
|
124
124
|
|
|
125
125
|
// ---- 头像 ----
|
|
126
|
-
async setChatAvatar(_token, _chatId, _tool, _status) {
|
|
127
|
-
// 模拟模式不需要头像
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
async getCodexUsageSummary() {
|
|
131
|
-
return {
|
|
132
|
-
fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
133
|
-
weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
134
|
-
};
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
// ---- 图片下载 ----
|
|
138
|
-
async getOrDownloadImage(_token, _messageId, fileKey) {
|
|
126
|
+
async setChatAvatar(_token, _chatId, _tool, _status) {
|
|
127
|
+
// 模拟模式不需要头像
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
async getCodexUsageSummary() {
|
|
131
|
+
return {
|
|
132
|
+
fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
133
|
+
weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// ---- 图片下载 ----
|
|
138
|
+
async getOrDownloadImage(_token, _messageId, fileKey) {
|
|
139
139
|
return join(SIM_DIR, "images", fileKey);
|
|
140
140
|
},
|
|
141
141
|
|
|
@@ -168,4 +168,4 @@ export const SimulatedPlatform: FeishuPlatform = {
|
|
|
168
168
|
};
|
|
169
169
|
|
|
170
170
|
/** 模拟模式下的默认 chat_id(重新导出以保持向后兼容) */
|
|
171
|
-
export { SIM_DEFAULT_CHAT_ID } from "./sim-store.ts";
|
|
171
|
+
export { SIM_DEFAULT_CHAT_ID } from "./sim-store.ts";
|
package/src/web-ui.ts
CHANGED
|
@@ -480,7 +480,7 @@ async function handleForgetIlink(_req: IncomingMessage, res: ServerResponse): Pr
|
|
|
480
480
|
// HTML page (embedded template)
|
|
481
481
|
// ---------------------------------------------------------------------------
|
|
482
482
|
|
|
483
|
-
export const PAGE_HTML = `<!DOCTYPE html>
|
|
483
|
+
export const PAGE_HTML = `<!DOCTYPE html>
|
|
484
484
|
<html lang="zh-CN">
|
|
485
485
|
<head>
|
|
486
486
|
<meta charset="UTF-8">
|
|
@@ -1539,11 +1539,11 @@ function editSection(section) {
|
|
|
1539
1539
|
html += '<option value="feishu"' + (ptVal === 'feishu' ? ' selected' : '') + '>飞书 (open.feishu.cn)</option>';
|
|
1540
1540
|
html += '<option value="lark"' + (ptVal === 'lark' ? ' selected' : '') + '>Lark (open.larksuite.com)</option>';
|
|
1541
1541
|
html += '</select></div>';
|
|
1542
|
-
}
|
|
1543
|
-
document.getElementById('edit-modal-fields').innerHTML = html;
|
|
1544
|
-
document.getElementById('edit-modal').classList.remove('hidden');
|
|
1545
|
-
document.getElementById('edit-overlay').classList.remove('hidden');
|
|
1546
|
-
}
|
|
1542
|
+
}
|
|
1543
|
+
document.getElementById('edit-modal-fields').innerHTML = html;
|
|
1544
|
+
document.getElementById('edit-modal').classList.remove('hidden');
|
|
1545
|
+
document.getElementById('edit-overlay').classList.remove('hidden');
|
|
1546
|
+
}
|
|
1547
1547
|
|
|
1548
1548
|
function closeEditModal() {
|
|
1549
1549
|
document.getElementById('edit-modal').classList.add('hidden');
|