chatccc 0.2.187 → 0.2.189
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/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +13 -13
- package/config.sample.json +5 -0
- 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-delegate-task-rpc.test.ts +165 -165
- package/src/__tests__/builtin-config.test.ts +33 -0
- package/src/__tests__/card-plain-text.test.ts +5 -5
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/cards.test.ts +77 -77
- 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 -125
- package/src/__tests__/claude-adapter.test.ts +592 -592
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/config-reload.test.ts +1 -0
- package/src/__tests__/config-sample.test.ts +11 -0
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +180 -115
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/orchestrator.test.ts +150 -2
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/session.test.ts +10 -0
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/web-ui.test.ts +209 -130
- package/src/adapters/claude-adapter.ts +566 -566
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +10 -6
- package/src/adapters/cursor-adapter.ts +46 -46
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/adapters/resource-monitor.ts +140 -140
- package/src/agent-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +81 -81
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +189 -197
- package/src/builtin/index.ts +168 -167
- package/src/cards.ts +130 -89
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -242
- package/src/codex-reset-actions.ts +184 -184
- package/src/config.ts +38 -0
- package/src/feishu-api.ts +219 -190
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/index.ts +2 -2
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +201 -9
- package/src/platform-adapter.ts +9 -1
- package/src/privacy.ts +118 -118
- package/src/session-chat-binding.ts +6 -6
- package/src/session-name.ts +8 -8
- package/src/session.ts +44 -16
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
- package/src/turn-cards.ts +117 -117
- package/src/web-ui.ts +142 -24
package/src/orchestrator.ts
CHANGED
|
@@ -23,7 +23,9 @@ import {
|
|
|
23
23
|
anthropicConfigDisplay,
|
|
24
24
|
config,
|
|
25
25
|
fileLog,
|
|
26
|
+
getAllEffortsForTool,
|
|
26
27
|
getAllModelsForTool,
|
|
28
|
+
getDefaultEffortForTool,
|
|
27
29
|
getDefaultCwd,
|
|
28
30
|
setDefaultCwd,
|
|
29
31
|
getRecentDirs,
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
} from "./config.ts";
|
|
38
40
|
import {
|
|
39
41
|
buildHelpCard,
|
|
42
|
+
buildEffortCard,
|
|
40
43
|
buildModelCard,
|
|
41
44
|
buildStatusCard,
|
|
42
45
|
buildCdContent,
|
|
@@ -53,6 +56,7 @@ import {
|
|
|
53
56
|
} from "./git-command.ts";
|
|
54
57
|
import {
|
|
55
58
|
clearSessionModelOverride,
|
|
59
|
+
clearSessionEffortOverride,
|
|
56
60
|
getSessionStatus,
|
|
57
61
|
getAllSessionsStatus,
|
|
58
62
|
initClaudeSession,
|
|
@@ -60,10 +64,12 @@ import {
|
|
|
60
64
|
resumeAndPrompt,
|
|
61
65
|
sessionInfoMap,
|
|
62
66
|
setSessionModelOverride,
|
|
67
|
+
setSessionEffortOverride,
|
|
63
68
|
switchChatBinding,
|
|
64
69
|
recordSessionRegistry,
|
|
65
70
|
getAdapterForTool,
|
|
66
71
|
getEffectiveModelForTool,
|
|
72
|
+
getEffectiveEffortForTool,
|
|
67
73
|
stopSession,
|
|
68
74
|
loadSessionRegistryForBinding,
|
|
69
75
|
removeSessionRegistryRecord,
|
|
@@ -86,7 +92,7 @@ import { delegateAgentTask } from "./agent-delegate-task.ts";
|
|
|
86
92
|
import { applySharedPrefix } from "./shared-prefix.ts";
|
|
87
93
|
import { cwdDisplayName, sessionChatName } from "./session-name.ts";
|
|
88
94
|
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
89
|
-
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
95
|
+
import type { ChatAvatarUsageHints, PlatformAdapter } from "./platform-adapter.ts";
|
|
90
96
|
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
91
97
|
|
|
92
98
|
// ---------------------------------------------------------------------------
|
|
@@ -186,6 +192,39 @@ function formatCodexUsageSummary(usage: CodexUsageSummary, chatGptSubscription:
|
|
|
186
192
|
return lines.join("\n");
|
|
187
193
|
};
|
|
188
194
|
|
|
195
|
+
const formatSubscriptionFailureReason = (result: ChatGptSubscriptionResult) => {
|
|
196
|
+
const port = result.chromeCdp.port;
|
|
197
|
+
switch (result.code) {
|
|
198
|
+
case "chrome_cdp_unreachable":
|
|
199
|
+
return `Chrome CDP 端口 ${port} 不可访问。请确认常驻 Chrome 已启动,或重启 ChatCCC。`;
|
|
200
|
+
case "chrome_cdp_occupied":
|
|
201
|
+
return `${port} 端口可访问,但不是健康的 Chrome CDP。请释放该端口或修改 chromeDevtools.port。`;
|
|
202
|
+
case "chatgpt_page_missing":
|
|
203
|
+
return `没有可用的 ChatGPT 页面。请在 ${port} 端口对应的 Chrome 浏览器中打开 https://chatgpt.com/ 并登录。`;
|
|
204
|
+
case "chatgpt_session_missing":
|
|
205
|
+
return `请在 ${port} 端口对应的 Chrome 浏览器中登录 ChatGPT。`;
|
|
206
|
+
case "chatgpt_subscription_failed":
|
|
207
|
+
return "ChatGPT 订阅接口探测失败。可能是页面未加载完成、ChatGPT 接口变更或网络异常。";
|
|
208
|
+
case "chrome_cdp_disabled":
|
|
209
|
+
return "";
|
|
210
|
+
case "ok":
|
|
211
|
+
return "";
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const formatChatGptSubscriptionFailure = () => {
|
|
216
|
+
if (!chatGptSubscription || chatGptSubscription.ok || !chatGptSubscription.chromeCdp.enabled) return "";
|
|
217
|
+
const lines = [
|
|
218
|
+
"**ChatGPT 订阅查询失败:**",
|
|
219
|
+
`- 原因: ${formatSubscriptionFailureReason(chatGptSubscription) || "暂无数据"}`,
|
|
220
|
+
];
|
|
221
|
+
const detail = chatGptSubscription.reason?.replace(/\s+/g, " ").trim();
|
|
222
|
+
if (detail) {
|
|
223
|
+
lines.push(`- 详情: ${detail.length > 240 ? `${detail.slice(0, 240)}...` : detail}`);
|
|
224
|
+
}
|
|
225
|
+
return lines.join("\n");
|
|
226
|
+
};
|
|
227
|
+
|
|
189
228
|
const formatChatGptSubscription = () => {
|
|
190
229
|
if (!chatGptSubscription?.ok || !chatGptSubscription.subscription) return "";
|
|
191
230
|
const subscription = chatGptSubscription.subscription;
|
|
@@ -221,7 +260,7 @@ function formatCodexUsageSummary(usage: CodexUsageSummary, chatGptSubscription:
|
|
|
221
260
|
"Codex 用量:",
|
|
222
261
|
"",
|
|
223
262
|
formatChatGptSubscription(),
|
|
224
|
-
|
|
263
|
+
formatChatGptSubscriptionFailure(),
|
|
225
264
|
formatResetCredits(),
|
|
226
265
|
"",
|
|
227
266
|
formatWindow("5h", usage.fiveHour),
|
|
@@ -291,23 +330,46 @@ function usageHelpLine(tool: string): string {
|
|
|
291
330
|
return "";
|
|
292
331
|
}
|
|
293
332
|
|
|
294
|
-
async function
|
|
333
|
+
async function resolveUsageTarget(chatId: string): Promise<{ tool: "codex" | "cursor"; sessionId?: string }> {
|
|
295
334
|
try {
|
|
296
335
|
const registry = await loadSessionRegistryForBinding();
|
|
297
|
-
|
|
336
|
+
const record = registry[chatId];
|
|
337
|
+
return {
|
|
338
|
+
tool: record?.tool === "cursor" ? "cursor" : "codex",
|
|
339
|
+
sessionId: record?.sessionId,
|
|
340
|
+
};
|
|
298
341
|
} catch {
|
|
299
|
-
return "codex";
|
|
342
|
+
return { tool: "codex" };
|
|
300
343
|
}
|
|
301
344
|
}
|
|
302
345
|
|
|
303
|
-
|
|
346
|
+
function refreshUsageAvatar(
|
|
347
|
+
platform: PlatformAdapter,
|
|
348
|
+
chatId: string,
|
|
349
|
+
tool: "codex" | "cursor",
|
|
350
|
+
status: "busy" | "idle",
|
|
351
|
+
usageHints: ChatAvatarUsageHints,
|
|
352
|
+
): void {
|
|
353
|
+
platform.setChatAvatar(chatId, tool, status, usageHints).catch((err) => {
|
|
354
|
+
console.warn(`[${ts()}] [AVATAR] usage refresh failed: chatId=${chatId} tool=${tool} ${(err as Error).message}`);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function sendUsageSummary(
|
|
359
|
+
platform: PlatformAdapter,
|
|
360
|
+
chatId: string,
|
|
361
|
+
tool: "codex" | "cursor",
|
|
362
|
+
avatarStatus: "busy" | "idle" = "idle",
|
|
363
|
+
): Promise<void> {
|
|
304
364
|
if (tool === "cursor") {
|
|
305
|
-
const
|
|
365
|
+
const usage = await getCursorUsageSummary();
|
|
366
|
+
const content = formatCursorUsageSummary(usage);
|
|
306
367
|
if (platform.kind === "wechat") {
|
|
307
368
|
await platform.sendText(chatId, content).catch(() => {});
|
|
308
369
|
} else {
|
|
309
370
|
await platform.sendCard(chatId, "Cursor Usage", content, "blue");
|
|
310
371
|
}
|
|
372
|
+
refreshUsageAvatar(platform, chatId, tool, avatarStatus, { cursorUsage: usage });
|
|
311
373
|
return;
|
|
312
374
|
}
|
|
313
375
|
|
|
@@ -323,6 +385,7 @@ async function sendUsageSummary(platform: PlatformAdapter, chatId: string, tool:
|
|
|
323
385
|
} else {
|
|
324
386
|
await platform.sendCard(chatId, "Codex Usage", content, "blue");
|
|
325
387
|
}
|
|
388
|
+
refreshUsageAvatar(platform, chatId, tool, avatarStatus, { codexUsage: usage });
|
|
326
389
|
}
|
|
327
390
|
|
|
328
391
|
async function sendUsageError(platform: PlatformAdapter, chatId: string, tool: "codex" | "cursor", err: unknown): Promise<void> {
|
|
@@ -538,10 +601,12 @@ export async function handleCommand(
|
|
|
538
601
|
}
|
|
539
602
|
|
|
540
603
|
if (isCommandText && textLower === "/usage") {
|
|
541
|
-
const
|
|
604
|
+
const usageTarget = await resolveUsageTarget(chatId);
|
|
605
|
+
const usageTool = usageTarget.tool;
|
|
606
|
+
const avatarStatus = usageTarget.sessionId && isSessionRunning(usageTarget.sessionId) ? "busy" : "idle";
|
|
542
607
|
logTrace(tid, "BRANCH", { cmd: "/usage", tool: usageTool });
|
|
543
608
|
try {
|
|
544
|
-
await sendUsageSummary(platform, chatId, usageTool);
|
|
609
|
+
await sendUsageSummary(platform, chatId, usageTool, avatarStatus);
|
|
545
610
|
logTrace(tid, "DONE", { outcome: "usage", tool: usageTool });
|
|
546
611
|
} catch (err) {
|
|
547
612
|
await sendUsageError(platform, chatId, usageTool, err);
|
|
@@ -1469,6 +1534,95 @@ export async function handleCommand(
|
|
|
1469
1534
|
}
|
|
1470
1535
|
|
|
1471
1536
|
// /git <args>:在「当前会话工作目录」执行 git 命令
|
|
1537
|
+
if (isCommandText && textLower === "/effort clear") {
|
|
1538
|
+
logTrace(tid, "BRANCH", { cmd: "/effort clear", sessionId, tool: descriptionTool });
|
|
1539
|
+
const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
|
|
1540
|
+
const toolLabel = toolDisplayName(descriptionTool);
|
|
1541
|
+
if (efforts.length === 0) {
|
|
1542
|
+
const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
|
|
1543
|
+
await (platform.kind === "wechat"
|
|
1544
|
+
? platform.sendText(chatId, msg)
|
|
1545
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
1546
|
+
).catch(() => {});
|
|
1547
|
+
logTrace(tid, "DONE", { outcome: "effort_unsupported", tool: descriptionTool });
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
clearSessionEffortOverride(sessionId);
|
|
1552
|
+
const defaultEffort = getDefaultEffortForTool(descriptionTool as AgentTool);
|
|
1553
|
+
const msg = `已清除当前 ${toolLabel} 会话的 effort 覆盖,恢复使用: \`${defaultEffort || "(未指定)"}\``;
|
|
1554
|
+
await (platform.kind === "wechat"
|
|
1555
|
+
? platform.sendText(chatId, msg)
|
|
1556
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "green")
|
|
1557
|
+
).catch(() => {});
|
|
1558
|
+
logTrace(tid, "DONE", { outcome: "effort_cleared", sessionId, tool: descriptionTool });
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
if (isCommandText && textLower.startsWith("/effort ")) {
|
|
1563
|
+
const effortArg = text.slice(8).trim();
|
|
1564
|
+
if (!effortArg) return;
|
|
1565
|
+
logTrace(tid, "BRANCH", { cmd: "/effort", arg: effortArg, sessionId, tool: descriptionTool });
|
|
1566
|
+
|
|
1567
|
+
const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
|
|
1568
|
+
const toolLabel = toolDisplayName(descriptionTool);
|
|
1569
|
+
if (efforts.length === 0) {
|
|
1570
|
+
const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
|
|
1571
|
+
await (platform.kind === "wechat"
|
|
1572
|
+
? platform.sendText(chatId, msg)
|
|
1573
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
1574
|
+
).catch(() => {});
|
|
1575
|
+
logTrace(tid, "DONE", { outcome: "effort_unsupported", tool: descriptionTool });
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const target = findModelMatch(effortArg, efforts);
|
|
1580
|
+
if (!target) {
|
|
1581
|
+
const msg = `未找到匹配 "${effortArg}" 的 effort。当前 ${toolLabel} 可选 effort:\n${efforts.map(e => ` \`${e}\``).join("\n")}`;
|
|
1582
|
+
await (platform.kind === "wechat"
|
|
1583
|
+
? platform.sendText(chatId, msg)
|
|
1584
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
1585
|
+
).catch(() => {});
|
|
1586
|
+
logTrace(tid, "DONE", { outcome: "effort_not_found", arg: effortArg, tool: descriptionTool });
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
setSessionEffortOverride(sessionId, target);
|
|
1591
|
+
const msg = `已切换当前 ${toolLabel} 会话 effort 为: \`${target}\``;
|
|
1592
|
+
await (platform.kind === "wechat"
|
|
1593
|
+
? platform.sendText(chatId, msg)
|
|
1594
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "green")
|
|
1595
|
+
).catch(() => {});
|
|
1596
|
+
logTrace(tid, "DONE", { outcome: "effort_switched", arg: effortArg, target, sessionId, tool: descriptionTool });
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
if (isCommandText && textLower === "/effort") {
|
|
1601
|
+
logTrace(tid, "BRANCH", { cmd: "/effort", sessionId, tool: descriptionTool });
|
|
1602
|
+
const efforts = getAllEffortsForTool(descriptionTool as AgentTool);
|
|
1603
|
+
const currentEffort = getEffectiveEffortForTool(descriptionTool, sessionId);
|
|
1604
|
+
const toolLabel = toolDisplayName(descriptionTool);
|
|
1605
|
+
|
|
1606
|
+
if (efforts.length === 0) {
|
|
1607
|
+
const msg = `当前 ${toolLabel} 不支持 effort 切换。`;
|
|
1608
|
+
await (platform.kind === "wechat"
|
|
1609
|
+
? platform.sendText(chatId, msg)
|
|
1610
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
1611
|
+
).catch(() => {});
|
|
1612
|
+
} else if (platform.kind === "wechat") {
|
|
1613
|
+
const lines = [currentEffort ? `当前 effort (${toolLabel}): ${currentEffort}` : `当前 effort (${toolLabel}): 未指定`];
|
|
1614
|
+
lines.push("", "可切换 effort:");
|
|
1615
|
+
for (const e of efforts) lines.push(` ${e}`);
|
|
1616
|
+
lines.push("", "输入 /effort <effort> 切换 effort");
|
|
1617
|
+
await platform.sendText(chatId, lines.join("\n")).catch(() => {});
|
|
1618
|
+
} else {
|
|
1619
|
+
const card = buildEffortCard(currentEffort, efforts, descriptionTool);
|
|
1620
|
+
await platform.sendRawCard(chatId, card);
|
|
1621
|
+
}
|
|
1622
|
+
logTrace(tid, "DONE", { outcome: "effort_query", tool: descriptionTool });
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1472
1626
|
if (isCommandText && (textLower.startsWith("/git ") || textLower === "/git")) {
|
|
1473
1627
|
const args = text === "/git" ? "" : text.slice(5).trim();
|
|
1474
1628
|
logTrace(tid, "BRANCH", { cmd: "/git", args: args || "(none)" });
|
|
@@ -1627,6 +1781,44 @@ export async function handleCommand(
|
|
|
1627
1781
|
}
|
|
1628
1782
|
|
|
1629
1783
|
// 无会话上下文 → /sessions 仍是有效指令,不触发飞书私聊自动建群。
|
|
1784
|
+
if (isCommandText && textLower === "/effort") {
|
|
1785
|
+
const defaultTool = resolveDefaultAgentTool();
|
|
1786
|
+
const efforts = getAllEffortsForTool(defaultTool);
|
|
1787
|
+
const currentEffort = getDefaultEffortForTool(defaultTool);
|
|
1788
|
+
const toolLabel = toolDisplayName(defaultTool);
|
|
1789
|
+
|
|
1790
|
+
if (efforts.length === 0) {
|
|
1791
|
+
const msg = `当前默认 agent (${toolLabel}) 不支持 effort 切换。`;
|
|
1792
|
+
await (platform.kind === "wechat"
|
|
1793
|
+
? platform.sendText(chatId, msg)
|
|
1794
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "red")
|
|
1795
|
+
).catch(() => {});
|
|
1796
|
+
} else if (platform.kind === "wechat") {
|
|
1797
|
+
const lines = [currentEffort ? `当前默认 effort (${toolLabel}): ${currentEffort}` : `当前默认 effort (${toolLabel}): 未指定`];
|
|
1798
|
+
lines.push("", "可切换 effort:");
|
|
1799
|
+
for (const e of efforts) lines.push(` ${e}`);
|
|
1800
|
+
lines.push("", "在会话中输入 /effort <effort> 切换 effort");
|
|
1801
|
+
await platform.sendText(chatId, lines.join("\n")).catch(() => {});
|
|
1802
|
+
} else {
|
|
1803
|
+
const card = buildEffortCard(currentEffort, efforts, defaultTool);
|
|
1804
|
+
await platform.sendRawCard(chatId, card);
|
|
1805
|
+
}
|
|
1806
|
+
logTrace(tid, "DONE", { outcome: "effort_query", defaultTool });
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
if (isCommandText && textLower.startsWith("/effort ")) {
|
|
1811
|
+
const defaultTool = resolveDefaultAgentTool();
|
|
1812
|
+
const toolLabel = toolDisplayName(defaultTool);
|
|
1813
|
+
const msg = `当前没有绑定会话。请先进入 Claude/Codex 会话,再输入 /effort <effort> 切换当前会话的 effort。当前默认 agent: ${toolLabel}`;
|
|
1814
|
+
await (platform.kind === "wechat"
|
|
1815
|
+
? platform.sendText(chatId, msg)
|
|
1816
|
+
: platform.sendCard(chatId, "Effort 切换", msg, "yellow")
|
|
1817
|
+
).catch(() => {});
|
|
1818
|
+
logTrace(tid, "DONE", { outcome: "effort_no_session", defaultTool });
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1630
1822
|
if (isCommandText && textLower === "/sessions") {
|
|
1631
1823
|
logTrace(tid, "BRANCH", { cmd: "/sessions", scope: "global" });
|
|
1632
1824
|
const allSessions = await getAllSessionsStatus();
|
package/src/platform-adapter.ts
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* 每个方法内部自行管理认证,消费者不感知 token 等认证细节。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { CursorUsageSummary } from "./cursor-usage.ts";
|
|
9
|
+
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
10
|
+
|
|
11
|
+
export interface ChatAvatarUsageHints {
|
|
12
|
+
codexUsage?: CodexUsageSummary | null;
|
|
13
|
+
cursorUsage?: CursorUsageSummary | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
export interface PlatformAdapter {
|
|
9
17
|
/** 平台标识,用于区分不同平台的行为(如 wechat、feishu 等) */
|
|
10
18
|
kind?: string;
|
|
@@ -40,7 +48,7 @@ export interface PlatformAdapter {
|
|
|
40
48
|
disbandChat(chatId: string): Promise<void>;
|
|
41
49
|
|
|
42
50
|
/** 设置群头像 */
|
|
43
|
-
setChatAvatar(chatId: string, tool: string, status: string): Promise<void>;
|
|
51
|
+
setChatAvatar(chatId: string, tool: string, status: string, usageHints?: ChatAvatarUsageHints): Promise<void>;
|
|
44
52
|
|
|
45
53
|
/** 从群描述中提取 session 信息 */
|
|
46
54
|
extractSessionInfo(
|
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/session-name.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export function cwdDisplayName(cwd: string): string {
|
|
2
|
-
const trimmed = cwd.trim().replace(/[\\/]+$/, "");
|
|
3
|
-
return trimmed.split(/[\\/]/).filter(Boolean).pop() || trimmed || "cwd";
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export function sessionChatName(left: string, cwd: string): string {
|
|
7
|
-
return `${left}-${cwdDisplayName(cwd)}`;
|
|
8
|
-
}
|
|
1
|
+
export function cwdDisplayName(cwd: string): string {
|
|
2
|
+
const trimmed = cwd.trim().replace(/[\\/]+$/, "");
|
|
3
|
+
return trimmed.split(/[\\/]/).filter(Boolean).pop() || trimmed || "cwd";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function sessionChatName(left: string, cwd: string): string {
|
|
7
|
+
return `${left}-${cwdDisplayName(cwd)}`;
|
|
8
|
+
}
|