chatccc 0.2.175 → 0.2.176

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.
@@ -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,68 +1,118 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { USER_DATA_DIR } from "./config.ts";
4
4
 
5
- // ---------------------------------------------------------------------------
6
- // 隐私替换规则
7
- // ---------------------------------------------------------------------------
8
-
9
5
  interface PrivacyRules {
10
6
  [key: string]: string;
11
7
  }
12
8
 
13
- let rules: PrivacyRules | null = null;
14
- let loaded = false;
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
+ }
15
70
 
16
- function loadRules(): PrivacyRules {
17
- const filePath = join(USER_DATA_DIR, "privacy.json");
71
+ function loadConfig(): PrivacyConfig {
72
+ const filePath = privacyFilePath();
18
73
  if (!existsSync(filePath)) {
19
- return {};
74
+ return { enabled: true, rules: {} };
20
75
  }
21
76
  try {
22
- const raw = readFileSync(filePath, "utf-8");
77
+ const raw = readFileSync(filePath, "utf-8").replace(/^\uFEFF/, "");
23
78
  const parsed = JSON.parse(raw);
24
79
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
25
- console.error(`[PRIVACY] privacy.json 格式错误:应为对象`);
26
- return {};
80
+ console.error("[PRIVACY] privacy.json must be an object");
81
+ return { enabled: true, rules: {} };
27
82
  }
28
- for (const [k, v] of Object.entries(parsed)) {
29
- if (typeof v !== "string") {
30
- console.error(`[PRIVACY] privacy.json 值必须为字符串,跳过 "${k}"`);
31
- delete (parsed as Record<string, unknown>)[k];
32
- }
33
- }
34
- return parsed as PrivacyRules;
83
+ return normalizeConfig(parsed as Record<string, unknown>);
35
84
  } catch (err) {
36
- console.error(`[PRIVACY] 读取 privacy.json 失败: ${(err as Error).message}`);
37
- return {};
85
+ console.error(`[PRIVACY] failed to read privacy.json: ${(err as Error).message}`);
86
+ return { enabled: true, rules: {} };
38
87
  }
39
88
  }
40
89
 
41
- export function getPrivacyRules(): PrivacyRules {
42
- if (!loaded) {
43
- rules = loadRules();
44
- loaded = true;
90
+ export function getPrivacyConfig(): PrivacyConfig {
91
+ const stamp = privacyFileStamp();
92
+ if (!config || stamp !== loadedStamp) {
93
+ config = loadConfig();
94
+ loadedStamp = stamp;
45
95
  }
46
- return rules!;
96
+ return config;
97
+ }
98
+
99
+ export function getPrivacyRules(): PrivacyRules {
100
+ return getPrivacyConfig().rules;
47
101
  }
48
102
 
49
- /** 重新加载规则(热更新用) */
50
103
  export function reloadPrivacyRules(): void {
51
- loaded = false;
52
- rules = null;
104
+ config = null;
105
+ loadedStamp = undefined;
53
106
  }
54
107
 
55
- /**
56
- * 对文本应用隐私替换规则。
57
- * 若无规则或文本为空,直接返回原文。
58
- */
59
108
  export function applyPrivacy(text: string): string {
60
- const r = getPrivacyRules();
61
- if (Object.keys(r).length === 0 || !text) return text;
109
+ const { enabled, rules } = getPrivacyConfig();
110
+ if (!enabled || Object.keys(rules).length === 0 || !text) return text;
111
+
62
112
  let result = text;
63
- for (const [from, to] of Object.entries(r)) {
64
- // split+join 替代 replaceAll,避免正则特殊字符问题
113
+ for (const [from, to] of Object.entries(rules)) {
114
+ // Use split+join instead of regex replacement so rule keys stay literal.
65
115
  result = result.split(from).join(to);
66
116
  }
67
117
  return result;
68
- }
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
+ }
@@ -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');