chatccc 0.2.178 → 0.2.180
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 +67 -49
- package/config.sample.json +3 -1
- package/package.json +1 -1
- package/src/__tests__/card-plain-text.test.ts +5 -4
- package/src/__tests__/cards.test.ts +22 -13
- package/src/__tests__/config-reload.test.ts +29 -22
- package/src/__tests__/feishu-avatar.test.ts +219 -129
- package/src/__tests__/orchestrator.test.ts +244 -106
- package/src/__tests__/shared-prefix.test.ts +36 -0
- package/src/cards.ts +13 -10
- package/src/config.ts +31 -2
- package/src/cursor-usage.ts +128 -0
- package/src/feishu-api.ts +65 -9
- package/src/index.ts +1 -1
- package/src/orchestrator.ts +260 -170
- package/src/shared-prefix.ts +29 -0
- package/src/web-ui.ts +95 -11
package/src/orchestrator.ts
CHANGED
|
@@ -79,9 +79,11 @@ import {
|
|
|
79
79
|
cancelQueuedMessage,
|
|
80
80
|
} from "./session-chat-binding.ts";
|
|
81
81
|
import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
|
|
82
|
+
import { getCursorUsageSummary, type CursorUsageSummary } from "./cursor-usage.ts";
|
|
83
|
+
import { applySharedPrefix } from "./shared-prefix.ts";
|
|
82
84
|
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
83
|
-
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
84
|
-
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
85
|
+
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
86
|
+
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
85
87
|
|
|
86
88
|
// ---------------------------------------------------------------------------
|
|
87
89
|
// 辅助函数
|
|
@@ -97,7 +99,7 @@ export function sessionChatName(left: string, cwd: string): string {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
/** 模型模糊匹配:精确匹配优先,否则找子串匹配(模型名越短越优先) */
|
|
100
|
-
function findModelMatch(input: string, models: string[]): string | null {
|
|
102
|
+
function findModelMatch(input: string, models: string[]): string | null {
|
|
101
103
|
if (models.length === 0) return null;
|
|
102
104
|
const inputLower = input.toLowerCase();
|
|
103
105
|
// 1) 精确匹配(忽略大小写)
|
|
@@ -109,82 +111,175 @@ function findModelMatch(input: string, models: string[]): string | null {
|
|
|
109
111
|
.filter(m => m.toLowerCase().includes(inputLower))
|
|
110
112
|
.sort((a, b) => a.length - b.length);
|
|
111
113
|
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
|
-
}
|
|
114
|
+
}
|
|
178
115
|
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
116
|
+
function formatCodexUsageSummary(usage: CodexUsageSummary): string {
|
|
117
|
+
const progressBar = (usedPercent: number) => {
|
|
118
|
+
const width = 20;
|
|
119
|
+
const usedBlocks = Math.max(0, Math.min(width, Math.round((usedPercent / 100) * width)));
|
|
120
|
+
return `[${"█".repeat(usedBlocks)}${"░".repeat(width - usedBlocks)}]`;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const formatDuration = (seconds: number | null) => {
|
|
124
|
+
if (seconds === null) return "";
|
|
125
|
+
if (seconds <= 0) return "(已到重置时间)";
|
|
126
|
+
const totalMinutes = Math.max(1, Math.floor(seconds / 60));
|
|
127
|
+
const days = Math.floor(totalMinutes / 1440);
|
|
128
|
+
const hours = Math.floor((totalMinutes % 1440) / 60);
|
|
129
|
+
const minutes = totalMinutes % 60;
|
|
130
|
+
const parts: string[] = [];
|
|
131
|
+
if (days > 0) parts.push(`${days}天`);
|
|
132
|
+
if (hours > 0) parts.push(`${hours}小时`);
|
|
133
|
+
if (minutes > 0 || parts.length === 0) parts.push(`${minutes}分钟`);
|
|
134
|
+
return `(约 ${parts.join("")}后)`;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const formatResetTime = (balance: CodexUsageSummary["fiveHour"]) => {
|
|
138
|
+
if (balance.resetAtEpochSeconds === null) return "暂无数据";
|
|
139
|
+
const date = new Date(balance.resetAtEpochSeconds * 1000);
|
|
140
|
+
const pad = (value: number) => String(value).padStart(2, "0");
|
|
141
|
+
const absolute = [
|
|
142
|
+
date.getFullYear(),
|
|
143
|
+
"-",
|
|
144
|
+
pad(date.getMonth() + 1),
|
|
145
|
+
"-",
|
|
146
|
+
pad(date.getDate()),
|
|
147
|
+
" ",
|
|
148
|
+
pad(date.getHours()),
|
|
149
|
+
":",
|
|
150
|
+
pad(date.getMinutes()),
|
|
151
|
+
].join("");
|
|
152
|
+
return `${absolute}${formatDuration(balance.resetAfterSeconds)}`;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const formatWindow = (label: string, balance: CodexUsageSummary["fiveHour"] | null) => {
|
|
156
|
+
if (!balance) return `**${label}:** 暂无数据`;
|
|
157
|
+
return [
|
|
158
|
+
`**${label}:** 已用 ${balance.usedPercent}%,剩余 ${balance.remainingPercent}%,重置: ${formatResetTime(balance)}`,
|
|
159
|
+
progressBar(balance.usedPercent),
|
|
160
|
+
].join("\n");
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return [
|
|
164
|
+
"Codex 用量:",
|
|
165
|
+
"",
|
|
166
|
+
formatWindow("5h", usage.fiveHour),
|
|
167
|
+
formatWindow("周", usage.weekly),
|
|
168
|
+
].join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function formatCursorUsageSummary(usage: CursorUsageSummary): string {
|
|
172
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
173
|
+
const dateFormatter = new Intl.DateTimeFormat("zh-CN", {
|
|
174
|
+
timeZone,
|
|
175
|
+
year: "numeric",
|
|
176
|
+
month: "2-digit",
|
|
177
|
+
day: "2-digit",
|
|
178
|
+
hour: "2-digit",
|
|
179
|
+
minute: "2-digit",
|
|
180
|
+
second: "2-digit",
|
|
181
|
+
hour12: false,
|
|
182
|
+
timeZoneName: "shortOffset",
|
|
183
|
+
});
|
|
184
|
+
const formatDate = (value: string | undefined) => {
|
|
185
|
+
const timestamp = Number(value);
|
|
186
|
+
if (!Number.isFinite(timestamp)) return "暂无数据";
|
|
187
|
+
return dateFormatter.format(new Date(timestamp));
|
|
188
|
+
};
|
|
189
|
+
const formatMoney = (value: number | undefined) => {
|
|
190
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "暂无数据";
|
|
191
|
+
return `$${(value / 100).toFixed(2)}`;
|
|
192
|
+
};
|
|
193
|
+
const formatPercent = (value: number | undefined) => {
|
|
194
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "暂无数据";
|
|
195
|
+
return `${value}%`;
|
|
196
|
+
};
|
|
197
|
+
const plan = usage.planUsage;
|
|
198
|
+
const spendLimit = usage.spendLimitUsage;
|
|
199
|
+
|
|
200
|
+
return [
|
|
201
|
+
"Cursor 用量:",
|
|
202
|
+
"",
|
|
203
|
+
`**计费周期:** ${formatDate(usage.billingCycleStart)} - ${formatDate(usage.billingCycleEnd)}`,
|
|
204
|
+
"",
|
|
205
|
+
"**Included usage:**",
|
|
206
|
+
`- Total: ${formatMoney(plan?.totalSpend)} / ${formatMoney(plan?.limit)} (${formatPercent(plan?.totalPercentUsed)})`,
|
|
207
|
+
`- Included: ${formatMoney(plan?.includedSpend)}`,
|
|
208
|
+
`- Bonus: ${formatMoney(plan?.bonusSpend)}`,
|
|
209
|
+
`- Auto: ${formatPercent(plan?.autoPercentUsed)}`,
|
|
210
|
+
`- API: ${formatPercent(plan?.apiPercentUsed)}`,
|
|
211
|
+
"",
|
|
212
|
+
"**On-Demand / Spend limit:**",
|
|
213
|
+
`- Individual used: ${formatMoney(spendLimit?.individualUsed)}`,
|
|
214
|
+
`- Pool used: ${formatMoney(spendLimit?.pooledUsed)} / ${formatMoney(spendLimit?.pooledLimit)}`,
|
|
215
|
+
`- Pool remaining: ${formatMoney(spendLimit?.pooledRemaining)}`,
|
|
216
|
+
`- Limit type: ${spendLimit?.limitType ?? "暂无数据"}`,
|
|
217
|
+
`- Display threshold: ${formatMoney(usage.displayThreshold)}`,
|
|
218
|
+
"",
|
|
219
|
+
`**Enabled:** ${usage.enabled === undefined ? "暂无数据" : String(usage.enabled)}`,
|
|
220
|
+
usage.displayMessage ? `**Message:** ${usage.displayMessage}` : "",
|
|
221
|
+
usage.autoModelSelectedDisplayMessage ? `**Auto model:** ${usage.autoModelSelectedDisplayMessage}` : "",
|
|
222
|
+
usage.namedModelSelectedDisplayMessage ? `**Named model:** ${usage.namedModelSelectedDisplayMessage}` : "",
|
|
223
|
+
usage.autoBucketModels?.length ? `**Auto bucket models:** ${usage.autoBucketModels.join(", ")}` : "",
|
|
224
|
+
].filter(Boolean).join("\n");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function usageHelpLine(tool: string): string {
|
|
228
|
+
if (tool === "codex") return "\n发送 **/usage** 查看 Codex 5h 和周用量。";
|
|
229
|
+
if (tool === "cursor") return "\n发送 **/usage** 查看 Cursor 用量。";
|
|
230
|
+
return "";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function resolveUsageTool(chatId: string): Promise<"codex" | "cursor"> {
|
|
234
|
+
try {
|
|
235
|
+
const registry = await loadSessionRegistryForBinding();
|
|
236
|
+
return registry[chatId]?.tool === "cursor" ? "cursor" : "codex";
|
|
237
|
+
} catch {
|
|
238
|
+
return "codex";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function sendUsageSummary(platform: PlatformAdapter, chatId: string, tool: "codex" | "cursor"): Promise<void> {
|
|
243
|
+
if (tool === "cursor") {
|
|
244
|
+
const content = formatCursorUsageSummary(await getCursorUsageSummary());
|
|
245
|
+
if (platform.kind === "wechat") {
|
|
246
|
+
await platform.sendText(chatId, content).catch(() => {});
|
|
247
|
+
} else {
|
|
248
|
+
await platform.sendCard(chatId, "Cursor Usage", content, "blue");
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const content = formatCodexUsageSummary(await getCodexUsageSummary());
|
|
254
|
+
if (platform.kind === "wechat") {
|
|
255
|
+
await platform.sendText(chatId, content).catch(() => {});
|
|
256
|
+
} else {
|
|
257
|
+
await platform.sendCard(chatId, "Codex Usage", content, "blue");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function sendUsageError(platform: PlatformAdapter, chatId: string, tool: "codex" | "cursor", err: unknown): Promise<void> {
|
|
262
|
+
const toolLabel = tool === "cursor" ? "Cursor" : "Codex";
|
|
263
|
+
const message = `${toolLabel} 用量获取失败:${(err as Error).message}`;
|
|
264
|
+
if (platform.kind === "wechat") {
|
|
265
|
+
await platform.sendText(chatId, message).catch(() => {});
|
|
266
|
+
} else {
|
|
267
|
+
await platform.sendCard(chatId, `${toolLabel} Usage`, message, "red");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isUntitledSessionChatName(name: string): boolean {
|
|
272
|
+
return name === "新会话" || name.startsWith("新会话-");
|
|
186
273
|
}
|
|
187
274
|
|
|
275
|
+
function shouldSendWechatProcessingAck(
|
|
276
|
+
platform: PlatformAdapter,
|
|
277
|
+
isCommandText: boolean,
|
|
278
|
+
chatType: string,
|
|
279
|
+
): boolean {
|
|
280
|
+
return platform.kind === "wechat" && chatType === "p2p" && !isCommandText;
|
|
281
|
+
}
|
|
282
|
+
|
|
188
283
|
function isNonWechatP2p(platform: PlatformAdapter, chatType: string): boolean {
|
|
189
284
|
return chatType === "p2p" && platform.kind !== "wechat";
|
|
190
285
|
}
|
|
@@ -232,49 +327,49 @@ function isRunningFromGlobalNpm(): boolean {
|
|
|
232
327
|
}
|
|
233
328
|
}
|
|
234
329
|
|
|
235
|
-
const
|
|
330
|
+
const UPDATE_LOG = join(homedir(), ".chatccc", "logs", "update-watcher.log");
|
|
236
331
|
|
|
237
332
|
function updLog(msg: string): void {
|
|
238
333
|
const ts = new Date().toISOString();
|
|
239
|
-
try { appendFileSync(
|
|
334
|
+
try { appendFileSync(UPDATE_LOG, `${ts} [UPDATE-SYNC] ${msg}\n`, "utf-8"); } catch {}
|
|
240
335
|
}
|
|
241
336
|
|
|
242
337
|
/** 同步更新 npm 全局包并 spawn 新进程重启。不依赖 systemd 或任何服务管理器。 */
|
|
243
338
|
function syncUpdateAndRestart(): void {
|
|
244
339
|
updLog(`sync update start, pid=${process.pid}`);
|
|
245
|
-
appendStartupTrace("
|
|
340
|
+
appendStartupTrace("update: sync update start", { pid: process.pid });
|
|
246
341
|
|
|
247
342
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
248
343
|
|
|
249
344
|
// 1. npm update
|
|
250
345
|
updLog(`running: ${npmCmd} update -g chatccc`);
|
|
251
|
-
appendStartupTrace("
|
|
346
|
+
appendStartupTrace("update: npm update begin", { npmCmd });
|
|
252
347
|
const t0 = Date.now();
|
|
253
348
|
try {
|
|
254
349
|
const out = execSync(`${npmCmd} update -g chatccc 2>&1`, { encoding: "utf8", timeout: 120000, windowsHide: true });
|
|
255
350
|
const elapsed = Date.now() - t0;
|
|
256
351
|
updLog(`npm update OK (${elapsed}ms): ${out.slice(0, 500)}`);
|
|
257
|
-
appendStartupTrace("
|
|
352
|
+
appendStartupTrace("update: npm update OK", { elapsedMs: elapsed, outputLen: out.length });
|
|
258
353
|
} catch (e) {
|
|
259
354
|
const elapsed = Date.now() - t0;
|
|
260
355
|
const err = e as Error & { stderr?: string; stdout?: string; status?: number };
|
|
261
356
|
updLog(`npm update failed (${elapsed}ms): message=${err.message}, stderr=${(err.stderr || "").slice(0, 500)}, stdout=${(err.stdout || "").slice(0, 200)}`);
|
|
262
|
-
appendStartupTrace("
|
|
357
|
+
appendStartupTrace("update: npm update failed", { elapsedMs: elapsed, message: err.message, stderrLen: (err.stderr || "").length });
|
|
263
358
|
|
|
264
359
|
// fallback
|
|
265
360
|
updLog(`fallback: ${npmCmd} install -g chatccc@latest`);
|
|
266
|
-
appendStartupTrace("
|
|
361
|
+
appendStartupTrace("update: npm install fallback begin", { npmCmd });
|
|
267
362
|
const t1 = Date.now();
|
|
268
363
|
try {
|
|
269
364
|
const out2 = execSync(`${npmCmd} install -g chatccc@latest 2>&1`, { encoding: "utf8", timeout: 120000, windowsHide: true });
|
|
270
365
|
const elapsed2 = Date.now() - t1;
|
|
271
366
|
updLog(`npm install fallback OK (${elapsed2}ms): ${out2.slice(0, 500)}`);
|
|
272
|
-
appendStartupTrace("
|
|
367
|
+
appendStartupTrace("update: npm install fallback OK", { elapsedMs: elapsed2, outputLen: out2.length });
|
|
273
368
|
} catch (e2) {
|
|
274
369
|
const elapsed2 = Date.now() - t1;
|
|
275
370
|
const err2 = e2 as Error & { stderr?: string; stdout?: string };
|
|
276
371
|
updLog(`npm install fallback also failed (${elapsed2}ms): message=${err2.message}, stderr=${(err2.stderr || "").slice(0, 500)}`);
|
|
277
|
-
appendStartupTrace("
|
|
372
|
+
appendStartupTrace("update: npm install fallback failed", { elapsedMs: elapsed2, message: err2.message });
|
|
278
373
|
}
|
|
279
374
|
}
|
|
280
375
|
|
|
@@ -283,22 +378,22 @@ function syncUpdateAndRestart(): void {
|
|
|
283
378
|
const binName = process.platform === "win32" ? "chatccc.cmd" : "chatccc";
|
|
284
379
|
const binPath = npmPrefix ? join(npmPrefix, binName) : "chatccc";
|
|
285
380
|
updLog(`bin path: npmPrefix=${npmPrefix || "(empty)"}, binPath=${binPath}`);
|
|
286
|
-
appendStartupTrace("
|
|
381
|
+
appendStartupTrace("update: spawn begin", { npmPrefix: npmPrefix || "(empty)", binPath });
|
|
287
382
|
|
|
288
383
|
// 3. spawn new chatccc
|
|
289
384
|
try {
|
|
290
385
|
const child = spawn(binPath, [], { detached: true, stdio: "ignore", shell: true });
|
|
291
386
|
child.unref();
|
|
292
387
|
updLog(`spawn new chatccc OK, childPid=${child.pid}, bin=${binPath}`);
|
|
293
|
-
appendStartupTrace("
|
|
388
|
+
appendStartupTrace("update: spawn OK", { childPid: child.pid, binPath });
|
|
294
389
|
} catch (e) {
|
|
295
390
|
const errMsg = (e as Error).message;
|
|
296
391
|
updLog(`spawn new chatccc failed: ${errMsg}`);
|
|
297
|
-
appendStartupTrace("
|
|
392
|
+
appendStartupTrace("update: spawn failed", { error: errMsg });
|
|
298
393
|
}
|
|
299
394
|
|
|
300
395
|
updLog("sync update done, parent exiting in 2s");
|
|
301
|
-
appendStartupTrace("
|
|
396
|
+
appendStartupTrace("update: sync update done, exiting", { pid: process.pid });
|
|
302
397
|
}
|
|
303
398
|
|
|
304
399
|
// ---------------------------------------------------------------------------
|
|
@@ -313,13 +408,17 @@ export async function handleCommand(
|
|
|
313
408
|
msgTimestamp: number,
|
|
314
409
|
chatType = "group",
|
|
315
410
|
traceId?: string,
|
|
316
|
-
): Promise<void> {
|
|
317
|
-
const tid = traceId ?? makeTraceId();
|
|
318
|
-
const
|
|
319
|
-
|
|
411
|
+
): Promise<void> {
|
|
412
|
+
const tid = traceId ?? makeTraceId();
|
|
413
|
+
const sharedPrefix = applySharedPrefix(text);
|
|
414
|
+
const promptText = sharedPrefix.text;
|
|
415
|
+
text = sharedPrefix.body;
|
|
416
|
+
const textLower = text.toLowerCase();
|
|
417
|
+
const isCommandText = !sharedPrefix.matched && textLower.startsWith("/");
|
|
418
|
+
recordChatPlatform(chatId, platform);
|
|
320
419
|
await cleanupNonWechatP2pBinding(platform, chatId, chatType, tid);
|
|
321
420
|
|
|
322
|
-
if (textLower === "/restart") {
|
|
421
|
+
if (isCommandText && textLower === "/restart") {
|
|
323
422
|
logTrace(tid, "BRANCH", { cmd: "/restart" });
|
|
324
423
|
await platform.sendText(chatId, "重启中...请几秒后发消息唤醒我").catch(() => {});
|
|
325
424
|
logTrace(tid, "DONE", { outcome: "restart" });
|
|
@@ -354,46 +453,37 @@ export async function handleCommand(
|
|
|
354
453
|
return;
|
|
355
454
|
}
|
|
356
455
|
|
|
357
|
-
if (textLower === "/
|
|
358
|
-
logTrace(tid, "BRANCH", { cmd: "/
|
|
456
|
+
if (isCommandText && textLower === "/update") {
|
|
457
|
+
logTrace(tid, "BRANCH", { cmd: "/update" });
|
|
359
458
|
const isGlobal = isRunningFromGlobalNpm();
|
|
360
|
-
appendStartupTrace("
|
|
459
|
+
appendStartupTrace("update: command received", { isGlobal, chatId });
|
|
361
460
|
if (!isGlobal) {
|
|
362
|
-
await platform.sendText(chatId, "当前进程非 npm 全局安装,无法使用 /
|
|
363
|
-
logTrace(tid, "DONE", { outcome: "
|
|
461
|
+
await platform.sendText(chatId, "当前进程非 npm 全局安装,无法使用 /update 更新。请通过 npm install -g chatccc 安装后使用。").catch(() => {});
|
|
462
|
+
logTrace(tid, "DONE", { outcome: "update_not_global" });
|
|
364
463
|
return;
|
|
365
464
|
}
|
|
366
465
|
await platform.sendText(chatId, "正在更新并重启,请稍候...").catch(() => {});
|
|
367
|
-
logTrace(tid, "DONE", { outcome: "
|
|
368
|
-
appendStartupTrace("
|
|
466
|
+
logTrace(tid, "DONE", { outcome: "update" });
|
|
467
|
+
appendStartupTrace("update: sync update begin", { fromPid: process.pid });
|
|
369
468
|
syncUpdateAndRestart();
|
|
370
469
|
setTimeout(() => process.exit(0), 2000);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (textLower === "/usage") {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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 ")) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (isCommandText && textLower === "/usage") {
|
|
474
|
+
const usageTool = await resolveUsageTool(chatId);
|
|
475
|
+
logTrace(tid, "BRANCH", { cmd: "/usage", tool: usageTool });
|
|
476
|
+
try {
|
|
477
|
+
await sendUsageSummary(platform, chatId, usageTool);
|
|
478
|
+
logTrace(tid, "DONE", { outcome: "usage", tool: usageTool });
|
|
479
|
+
} catch (err) {
|
|
480
|
+
await sendUsageError(platform, chatId, usageTool, err);
|
|
481
|
+
logTrace(tid, "DONE", { outcome: "usage_fail", tool: usageTool, error: (err as Error).message });
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (isCommandText && (textLower === "/cd" || textLower.startsWith("/cd "))) {
|
|
397
487
|
logTrace(tid, "BRANCH", {
|
|
398
488
|
cmd: "/cd",
|
|
399
489
|
arg: text.slice(3).trim() || "(none)",
|
|
@@ -516,7 +606,7 @@ export async function handleCommand(
|
|
|
516
606
|
return;
|
|
517
607
|
}
|
|
518
608
|
|
|
519
|
-
if (textLower === "/new" || textLower.startsWith("/new ")) {
|
|
609
|
+
if (isCommandText && (textLower === "/new" || textLower.startsWith("/new "))) {
|
|
520
610
|
const toolArg = text.slice(5).trim().toLowerCase();
|
|
521
611
|
const tool = toolArg || resolveDefaultAgentTool();
|
|
522
612
|
logTrace(tid, "BRANCH", { cmd: "/new", tool });
|
|
@@ -612,13 +702,13 @@ export async function handleCommand(
|
|
|
612
702
|
`**工作目录:** \`${cwd}\`\n\n` +
|
|
613
703
|
`直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
|
|
614
704
|
`发送 **/cd** 切换新建会话的默认目录。\n` +
|
|
615
|
-
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
616
|
-
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
617
|
-
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
618
|
-
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
619
|
-
|
|
620
|
-
"green",
|
|
621
|
-
);
|
|
705
|
+
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
706
|
+
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
707
|
+
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
708
|
+
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
709
|
+
usageHelpLine(tool),
|
|
710
|
+
"green",
|
|
711
|
+
);
|
|
622
712
|
console.log(
|
|
623
713
|
`[${ts()}] [NEW] P2P session created: ${sessionId} (${toolLabel})`,
|
|
624
714
|
);
|
|
@@ -699,13 +789,13 @@ export async function handleCommand(
|
|
|
699
789
|
`**工作目录:** \`${cwd}\`\n\n` +
|
|
700
790
|
`直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
|
|
701
791
|
`发送 **/cd** 切换新建会话的默认目录。\n` +
|
|
702
|
-
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
703
|
-
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
704
|
-
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
705
|
-
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
706
|
-
|
|
707
|
-
"green",
|
|
708
|
-
);
|
|
792
|
+
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
793
|
+
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
794
|
+
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
795
|
+
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
|
|
796
|
+
usageHelpLine(tool),
|
|
797
|
+
"green",
|
|
798
|
+
);
|
|
709
799
|
|
|
710
800
|
console.log(`[${ts()}] [STEP 4/4] Replied to new group → OK`);
|
|
711
801
|
logTrace(tid, "DONE", {
|
|
@@ -777,7 +867,7 @@ export async function handleCommand(
|
|
|
777
867
|
if (sessionId && descriptionTool && toolLabel) {
|
|
778
868
|
// 有会话上下文 — 路由到命令处理或 prompt
|
|
779
869
|
logTrace(tid, "BRANCH", { sessionId, tool: descriptionTool });
|
|
780
|
-
const routeKind =
|
|
870
|
+
const routeKind = isCommandText ? "command" : "prompt";
|
|
781
871
|
const chatKind = chatType === "p2p" ? "p2p chat" : "session group";
|
|
782
872
|
console.log(
|
|
783
873
|
`[${ts()}] [ROUTE] ${toolLabel} ${chatKind} ${routeKind} detected, session=${sessionId} tool=${descriptionTool}`,
|
|
@@ -786,7 +876,7 @@ export async function handleCommand(
|
|
|
786
876
|
if (
|
|
787
877
|
chatType !== "p2p" &&
|
|
788
878
|
isUntitledSessionChatName(chatInfo!.name) &&
|
|
789
|
-
!
|
|
879
|
+
!isCommandText
|
|
790
880
|
) {
|
|
791
881
|
const MAX_PREFIX = 10;
|
|
792
882
|
const prefix = text.slice(0, MAX_PREFIX);
|
|
@@ -821,7 +911,7 @@ export async function handleCommand(
|
|
|
821
911
|
if (
|
|
822
912
|
chatType === "p2p" &&
|
|
823
913
|
platform.kind === "wechat" &&
|
|
824
|
-
!
|
|
914
|
+
!isCommandText
|
|
825
915
|
) {
|
|
826
916
|
try {
|
|
827
917
|
const reg = await loadSessionRegistryForBinding();
|
|
@@ -860,7 +950,7 @@ export async function handleCommand(
|
|
|
860
950
|
}
|
|
861
951
|
}
|
|
862
952
|
|
|
863
|
-
if (textLower === "/stop") {
|
|
953
|
+
if (isCommandText && textLower === "/stop") {
|
|
864
954
|
logTrace(tid, "BRANCH", { cmd: "/stop" });
|
|
865
955
|
if (stopSession(sessionId)) {
|
|
866
956
|
console.log(`[${ts()}] [STOP] User sent /stop, session=${sessionId}`);
|
|
@@ -874,7 +964,7 @@ export async function handleCommand(
|
|
|
874
964
|
return;
|
|
875
965
|
}
|
|
876
966
|
|
|
877
|
-
if (textLower === "/cancel") {
|
|
967
|
+
if (isCommandText && textLower === "/cancel") {
|
|
878
968
|
logTrace(tid, "BRANCH", { cmd: "/cancel" });
|
|
879
969
|
if (cancelQueuedMessage(sessionId)) {
|
|
880
970
|
console.log(`[${ts()}] [CANCEL] Queue cancelled for session=${sessionId}`);
|
|
@@ -887,7 +977,7 @@ export async function handleCommand(
|
|
|
887
977
|
return;
|
|
888
978
|
}
|
|
889
979
|
|
|
890
|
-
if (textLower === "/test") {
|
|
980
|
+
if (isCommandText && textLower === "/test") {
|
|
891
981
|
logTrace(tid, "BRANCH", { cmd: "/test" });
|
|
892
982
|
const tableHeaders = ["名称", "版本", "状态"];
|
|
893
983
|
const tableRows = [
|
|
@@ -924,7 +1014,7 @@ export async function handleCommand(
|
|
|
924
1014
|
return;
|
|
925
1015
|
}
|
|
926
1016
|
|
|
927
|
-
if (textLower === "/state") {
|
|
1017
|
+
if (isCommandText && textLower === "/state") {
|
|
928
1018
|
logTrace(tid, "BRANCH", { cmd: "/state" });
|
|
929
1019
|
const status = await getSessionStatus(chatId);
|
|
930
1020
|
const isActive = isSessionRunning(sessionId);
|
|
@@ -963,7 +1053,7 @@ export async function handleCommand(
|
|
|
963
1053
|
return;
|
|
964
1054
|
}
|
|
965
1055
|
|
|
966
|
-
if (textLower === "/sessions") {
|
|
1056
|
+
if (isCommandText && textLower === "/sessions") {
|
|
967
1057
|
logTrace(tid, "BRANCH", { cmd: "/sessions" });
|
|
968
1058
|
const allSessions = await getAllSessionsStatus();
|
|
969
1059
|
const now = Date.now();
|
|
@@ -988,7 +1078,7 @@ export async function handleCommand(
|
|
|
988
1078
|
return;
|
|
989
1079
|
}
|
|
990
1080
|
|
|
991
|
-
if (textLower === "/newh") {
|
|
1081
|
+
if (isCommandText && textLower === "/newh") {
|
|
992
1082
|
logTrace(tid, "BRANCH", { cmd: "/newh" });
|
|
993
1083
|
const adapter = getAdapterForTool(descriptionTool, sessionId);
|
|
994
1084
|
let cwd: string;
|
|
@@ -1075,7 +1165,7 @@ export async function handleCommand(
|
|
|
1075
1165
|
return;
|
|
1076
1166
|
}
|
|
1077
1167
|
|
|
1078
|
-
if (textLower === "/deleteg") {
|
|
1168
|
+
if (isCommandText && textLower === "/deleteg") {
|
|
1079
1169
|
logTrace(tid, "BRANCH", { cmd: "/deleteg" });
|
|
1080
1170
|
if (chatType === "p2p") {
|
|
1081
1171
|
await platform
|
|
@@ -1113,7 +1203,7 @@ export async function handleCommand(
|
|
|
1113
1203
|
}
|
|
1114
1204
|
|
|
1115
1205
|
// /session <number>:切换到 /sessions 列表中的指定会话
|
|
1116
|
-
const sessionMatch = textLower.match(/^\/session\s+(\d+)$/);
|
|
1206
|
+
const sessionMatch = isCommandText ? textLower.match(/^\/session\s+(\d+)$/) : null;
|
|
1117
1207
|
if (sessionMatch) {
|
|
1118
1208
|
const index = parseInt(sessionMatch[1], 10) - 1;
|
|
1119
1209
|
logTrace(tid, "BRANCH", { cmd: "/session", index: index + 1 });
|
|
@@ -1239,7 +1329,7 @@ export async function handleCommand(
|
|
|
1239
1329
|
}
|
|
1240
1330
|
|
|
1241
1331
|
// /model clear — 清除当前 session 的模型覆盖
|
|
1242
|
-
if (textLower === "/model clear") {
|
|
1332
|
+
if (isCommandText && textLower === "/model clear") {
|
|
1243
1333
|
logTrace(tid, "BRANCH", { cmd: "/model clear", sessionId });
|
|
1244
1334
|
clearSessionModelOverride(sessionId);
|
|
1245
1335
|
const defaultModel = getEffectiveModelForTool(descriptionTool);
|
|
@@ -1254,7 +1344,7 @@ export async function handleCommand(
|
|
|
1254
1344
|
}
|
|
1255
1345
|
|
|
1256
1346
|
// /model <name> — 切换当前 session 的模型(支持所有 agent,模糊匹配)
|
|
1257
|
-
if (textLower.startsWith("/model ")) {
|
|
1347
|
+
if (isCommandText && textLower.startsWith("/model ")) {
|
|
1258
1348
|
const modelArg = text.slice(7).trim();
|
|
1259
1349
|
if (!modelArg) return; // 纯 "/model " 不处理,交给上面的 /model 分支
|
|
1260
1350
|
logTrace(tid, "BRANCH", { cmd: "/model", arg: modelArg, sessionId, tool: descriptionTool });
|
|
@@ -1288,7 +1378,7 @@ export async function handleCommand(
|
|
|
1288
1378
|
}
|
|
1289
1379
|
|
|
1290
1380
|
// /model — 查看当前会话的可用模型(根据会话 Agent 类型)
|
|
1291
|
-
if (textLower === "/model") {
|
|
1381
|
+
if (isCommandText && textLower === "/model") {
|
|
1292
1382
|
logTrace(tid, "BRANCH", { cmd: "/model", sessionId, tool: descriptionTool });
|
|
1293
1383
|
const models = getAllModelsForTool(descriptionTool as AgentTool);
|
|
1294
1384
|
const currentModel = getEffectiveModelForTool(descriptionTool, sessionId);
|
|
@@ -1312,7 +1402,7 @@ export async function handleCommand(
|
|
|
1312
1402
|
}
|
|
1313
1403
|
|
|
1314
1404
|
// /git <args>:在「当前会话工作目录」执行 git 命令
|
|
1315
|
-
if (textLower.startsWith("/git ") || textLower === "/git") {
|
|
1405
|
+
if (isCommandText && (textLower.startsWith("/git ") || textLower === "/git")) {
|
|
1316
1406
|
const args = text === "/git" ? "" : text.slice(5).trim();
|
|
1317
1407
|
logTrace(tid, "BRANCH", { cmd: "/git", args: args || "(none)" });
|
|
1318
1408
|
if (!args) {
|
|
@@ -1381,9 +1471,9 @@ export async function handleCommand(
|
|
|
1381
1471
|
|
|
1382
1472
|
// 并发检查:同一 session 只能有一个活跃 prompt,多余消息进入队列
|
|
1383
1473
|
if (isSessionRunning(sessionId)) {
|
|
1384
|
-
const queued = enqueueMessage(sessionId, {
|
|
1385
|
-
text, chatId, openId, msgTimestamp, chatType, traceId: tid,
|
|
1386
|
-
});
|
|
1474
|
+
const queued = enqueueMessage(sessionId, {
|
|
1475
|
+
text: promptText, chatId, openId, msgTimestamp, chatType, traceId: tid,
|
|
1476
|
+
});
|
|
1387
1477
|
if (queued) {
|
|
1388
1478
|
logTrace(tid, "QUEUED", { sessionId });
|
|
1389
1479
|
console.log(
|
|
@@ -1408,16 +1498,16 @@ export async function handleCommand(
|
|
|
1408
1498
|
return;
|
|
1409
1499
|
}
|
|
1410
1500
|
|
|
1411
|
-
if (shouldSendWechatProcessingAck(platform,
|
|
1501
|
+
if (shouldSendWechatProcessingAck(platform, isCommandText, chatType)) {
|
|
1412
1502
|
await platform.sendText(chatId, "生成中...").catch(() => {});
|
|
1413
1503
|
}
|
|
1414
1504
|
|
|
1415
1505
|
try {
|
|
1416
1506
|
logTrace(tid, "RESUME", { sessionId, tool: descriptionTool });
|
|
1417
1507
|
await resumeAndPrompt(
|
|
1418
|
-
sessionId,
|
|
1419
|
-
|
|
1420
|
-
platform,
|
|
1508
|
+
sessionId,
|
|
1509
|
+
promptText,
|
|
1510
|
+
platform,
|
|
1421
1511
|
chatId,
|
|
1422
1512
|
msgTimestamp,
|
|
1423
1513
|
descriptionTool,
|
|
@@ -1443,7 +1533,7 @@ export async function handleCommand(
|
|
|
1443
1533
|
}
|
|
1444
1534
|
|
|
1445
1535
|
// 无会话上下文 → 检查是否是 /model 查询
|
|
1446
|
-
if (textLower === "/model") {
|
|
1536
|
+
if (isCommandText && textLower === "/model") {
|
|
1447
1537
|
const defaultTool = resolveDefaultAgentTool();
|
|
1448
1538
|
const models = getAllModelsForTool(defaultTool);
|
|
1449
1539
|
let currentModel = "";
|
|
@@ -1470,7 +1560,7 @@ export async function handleCommand(
|
|
|
1470
1560
|
}
|
|
1471
1561
|
|
|
1472
1562
|
// 无会话上下文 → /sessions 仍是有效指令,不触发飞书私聊自动建群。
|
|
1473
|
-
if (textLower === "/sessions") {
|
|
1563
|
+
if (isCommandText && textLower === "/sessions") {
|
|
1474
1564
|
logTrace(tid, "BRANCH", { cmd: "/sessions", scope: "global" });
|
|
1475
1565
|
const allSessions = await getAllSessionsStatus();
|
|
1476
1566
|
const now = Date.now();
|
|
@@ -1496,7 +1586,7 @@ export async function handleCommand(
|
|
|
1496
1586
|
}
|
|
1497
1587
|
|
|
1498
1588
|
// 飞书私聊普通消息:不再绑定私聊本身,而是自动创建会话群并把私聊内容作为首轮 prompt。
|
|
1499
|
-
if (isNonWechatP2p(platform, chatType) && !
|
|
1589
|
+
if (isNonWechatP2p(platform, chatType) && !isCommandText) {
|
|
1500
1590
|
const tool = resolveDefaultAgentTool();
|
|
1501
1591
|
const toolLabel = toolDisplayName(tool);
|
|
1502
1592
|
logTrace(tid, "BRANCH", { cmd: "auto_new_from_p2p", tool });
|
|
@@ -1614,9 +1704,9 @@ export async function handleCommand(
|
|
|
1614
1704
|
try {
|
|
1615
1705
|
logTrace(tid, "RESUME", { sessionId, tool, trigger: "auto_new_from_p2p" });
|
|
1616
1706
|
await resumeAndPrompt(
|
|
1617
|
-
sessionId,
|
|
1618
|
-
|
|
1619
|
-
platform,
|
|
1707
|
+
sessionId,
|
|
1708
|
+
promptText,
|
|
1709
|
+
platform,
|
|
1620
1710
|
newChatId,
|
|
1621
1711
|
msgTimestamp,
|
|
1622
1712
|
tool,
|