chatccc 0.2.272 → 0.2.276
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 +24 -22
- package/config.sample.json +4 -4
- package/deepccc-agent/README.md +147 -79
- package/deepccc-agent/package.json +68 -65
- package/dist/deepccc-agent/src/attachments.js +192 -0
- package/dist/deepccc-agent/src/cli.js +54 -13
- package/dist/deepccc-agent/src/config.js +49 -4
- package/dist/deepccc-agent/src/context.js +299 -16
- package/dist/deepccc-agent/src/file-tools.js +33 -0
- package/dist/deepccc-agent/src/index.js +48 -16
- package/dist/deepccc-agent/src/tool-protocol.js +14 -3
- package/dist/deepccc-agent/src/web-entry.js +72 -0
- package/dist/deepccc-agent/src/web-page.js +414 -0
- package/dist/deepccc-agent/src/web-runtime.js +331 -0
- package/dist/deepccc-agent/src/web-server.js +476 -0
- package/dist/deepccc-agent/src/web-session-store.js +162 -0
- package/dist/deepccc-agent/src/web-tool-presentation.js +123 -0
- package/dist/src/adapters/ccc-adapter.js +1 -0
- package/dist/src/agent-capability-grants.js +26 -0
- package/dist/src/agent-delegate-task.js +5 -2
- package/dist/src/agent-file-rpc.js +6 -1
- package/dist/src/agent-image-rpc.js +6 -1
- package/dist/src/agent-team/web/agent-team-page.js +250 -250
- package/dist/src/cards.js +7 -4
- package/dist/src/im-skills.js +9 -2
- package/dist/src/orchestrator.js +117 -29
- package/dist/src/session-name.js +15 -0
- package/dist/src/session.js +43 -7
- package/dist/src/web-ui.js +68 -51
- package/im-skills/feishu-skill/receive-send-file.md +3 -2
- package/im-skills/feishu-skill/receive-send-image.md +3 -2
- package/im-skills/feishu-skill/send-file.mjs +6 -5
- package/im-skills/feishu-skill/send-image.mjs +6 -5
- package/im-skills/feishu-skill/skill.md +4 -2
- package/package.json +76 -76
package/dist/src/cards.js
CHANGED
|
@@ -139,6 +139,8 @@ export function buildHelpCard(userText, opts = {}) {
|
|
|
139
139
|
"发送 **/new ccc** 创建新 CCC Agent 会话",
|
|
140
140
|
"发送 **/new dsh** 创建新 DeepSeek Harness 会话",
|
|
141
141
|
"发送 **/forget** 重置当前会话(忘掉上下文,沿用当前工作目录,不切换)",
|
|
142
|
+
"发送 **/rename 名称** 修改会话展示标题(不会修改群名)",
|
|
143
|
+
"发送 **/sessions 关键词** 搜索会话,**/pin** 置顶,**/archive** 归档",
|
|
142
144
|
"发送 **/plan** 以规划模式提问(只读,不执行写操作)",
|
|
143
145
|
"发送 **/ask** 以问答模式提问(只读,不执行写操作)",
|
|
144
146
|
"发送 **/usage** 查看当前 Agent 的用量或余额",
|
|
@@ -308,7 +310,8 @@ export function buildSessionsCard(sessions, opts = {}) {
|
|
|
308
310
|
extra = ` | 本轮: ${mins}分${secs}秒`;
|
|
309
311
|
}
|
|
310
312
|
const toolLabel = sessionToolLabel(s.tool);
|
|
311
|
-
const
|
|
313
|
+
const title = s.displayTitle || s.chatName;
|
|
314
|
+
const namePart = title ? `${s.pinned ? "📌 " : ""}**${title}** ` : "";
|
|
312
315
|
const chatTag = !s.chatId
|
|
313
316
|
? " (chat id缺失)"
|
|
314
317
|
: s.chatType === "p2p"
|
|
@@ -316,7 +319,7 @@ export function buildSessionsCard(sessions, opts = {}) {
|
|
|
316
319
|
: s.chatType === "group" || (s.chatType == null && s.chatId.startsWith("oc_"))
|
|
317
320
|
? " (群聊)"
|
|
318
321
|
: "";
|
|
319
|
-
return `**${i + 1}.** ${namePart}${chatTag} \`${shortId}\` ${status} | 工具: ${toolLabel} | 轮数: ${s.turnCount} | ${s.model}${extra}`;
|
|
322
|
+
return `**${i + 1}.** ${namePart}${chatTag}${s.archivedAt ? " (已归档)" : ""} \`${shortId}\` ${status} | 工具: ${toolLabel} | 轮数: ${s.turnCount} | ${s.model}${extra}`;
|
|
320
323
|
};
|
|
321
324
|
const lines = [`共 **${sessions.length}** 个会话:`, ""];
|
|
322
325
|
const idx = { value: 0 };
|
|
@@ -337,8 +340,8 @@ export function buildSessionsCard(sessions, opts = {}) {
|
|
|
337
340
|
{ tag: "div", text: { tag: "lark_md", content: lines.join("\n") } },
|
|
338
341
|
{ tag: "hr" },
|
|
339
342
|
{ tag: "div", text: { tag: "lark_md", content: fixedPrivateSession
|
|
340
|
-
? "当前飞书私聊使用专属会话;默认 Agent 变化后,下一条普通消息会自动创建对应 Agent 的新空会话。发送 **/forget**
|
|
341
|
-
: "
|
|
343
|
+
? "当前飞书私聊使用专属会话;默认 Agent 变化后,下一条普通消息会自动创建对应 Agent 的新空会话。发送 **/rename 名称** 修改展示标题,**/pin** 置顶,**/archive** 归档,**/sessions 关键词** 搜索。发送 **/forget** 可在私聊中原地重置;群聊会话请回到对应群聊继续,私聊不支持 **/session** 切换。"
|
|
344
|
+
: "发送 **/rename 名称** 修改展示标题,**/pin** 置顶,**/archive** 归档,**/sessions 关键词** 搜索,**/sessions --archived** 查看归档。\n发送 **/session 数字** 可切换到最近一次列表中对应编号的会话;**/forget** 会保留群聊和工作目录并创建新 Session。" } },
|
|
342
345
|
{ tag: "hr" },
|
|
343
346
|
{
|
|
344
347
|
tag: "action",
|
package/dist/src/im-skills.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { readdir, readFile, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { join, dirname } from "node:path";
|
|
3
4
|
import { PROJECT_ROOT } from "./config.js";
|
|
4
5
|
export const DEFAULT_IM_SKILLS_DIR = join(PROJECT_ROOT, "im-skills");
|
|
6
|
+
export function sessionImSkillsCacheDir(cacheRoot, sessionId) {
|
|
7
|
+
if (!sessionId)
|
|
8
|
+
throw new Error("sessionId is required for the IM skills cache");
|
|
9
|
+
const sessionKey = createHash("sha256").update(sessionId, "utf8").digest("hex").slice(0, 24);
|
|
10
|
+
return join(cacheRoot, sessionKey);
|
|
11
|
+
}
|
|
5
12
|
function renderTemplate(template, variables) {
|
|
6
13
|
return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_match, key) => {
|
|
7
14
|
return variables[key] ?? "";
|
|
@@ -56,8 +63,8 @@ function promptCacheKey(input) {
|
|
|
56
63
|
const names = input.enabledSkillNames
|
|
57
64
|
? [...input.enabledSkillNames].sort().join(",")
|
|
58
65
|
: "*";
|
|
59
|
-
const { session_id, cwd, open_id } = input.variables;
|
|
60
|
-
return `${input.skillsDir ?? DEFAULT_IM_SKILLS_DIR}|${names}|${session_id}|${cwd}|${open_id}`;
|
|
66
|
+
const { session_id, cwd, open_id, agent_capability_grant } = input.variables;
|
|
67
|
+
return `${input.skillsDir ?? DEFAULT_IM_SKILLS_DIR}|${names}|${session_id}|${cwd}|${open_id}|${agent_capability_grant}`;
|
|
61
68
|
}
|
|
62
69
|
/** 带会话级缓存的 buildImSkillsPrompt。同 session + 同 cwd 时直接返回缓存的渲染结果。 */
|
|
63
70
|
export async function buildImSkillsPromptCached(input) {
|
package/dist/src/orchestrator.js
CHANGED
|
@@ -16,13 +16,13 @@ import { appendStartupTrace } from "./shared.js";
|
|
|
16
16
|
import { CLAUDE_MODEL, GIT_TIMEOUT_MS, PROJECT_ROOT, anthropicConfigDisplay, config, fileLog, getAllEffortsForTool, getAllModelsForTool, getDefaultEffortForTool, getDefaultCwd, LOG_DIR, setDefaultCwd, getRecentDirs, addRecentDir, resolveDefaultAgentTool, sessionPrefixForTool, toolDisplayName, ts, } from "./config.js";
|
|
17
17
|
import { buildHelpCard, buildEffortCard, buildFastModeCard, buildModelCard, buildStatusCard, buildCdContent, buildCdCard, buildSessionsCard, buildQueuedCard, buildQueueFullCard, buildCodexUsageCard, } from "./cards.js";
|
|
18
18
|
import { formatGitResult, gitResultHeaderTemplate, runGitCommand, } from "./git-command.js";
|
|
19
|
-
import { clearSessionModelOverride, clearSessionEffortOverride, getSessionStatus, getAllSessionsStatus, initClaudeSession, lastMsgTimestamps, resumeAndPrompt, sessionInfoMap, setSessionModelOverride, setSessionEffortOverride, switchChatBinding, recordSessionRegistry, getAdapterForTool, getEffectiveModelForTool, getEffectiveEffortForTool, getEffectiveFastModeForTool, setSessionFastModeOverride, stopSession, loadSessionRegistryForBinding, removeSessionRegistryRecord, saveSessionTool, recordChatPlatform, } from "./session.js";
|
|
19
|
+
import { clearSessionModelOverride, clearSessionEffortOverride, getSessionStatus, getAllSessionsStatus, initClaudeSession, lastMsgTimestamps, resumeAndPrompt, sessionInfoMap, setSessionModelOverride, setSessionEffortOverride, switchChatBinding, recordSessionRegistry, getAdapterForTool, getEffectiveModelForTool, getEffectiveEffortForTool, getEffectiveFastModeForTool, setSessionFastModeOverride, stopSession, loadSessionRegistryForBinding, removeSessionRegistryRecord, saveSessionTool, saveSessionPresentation, recordChatPlatform, } from "./session.js";
|
|
20
20
|
import { bindChatToSession, unbindChatFromSession, isSessionRunning, displayCards, enqueueMessage, cancelQueuedMessage, getSessionDrainSnapshot, } from "./session-chat-binding.js";
|
|
21
21
|
import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.js";
|
|
22
22
|
import { getCursorUsageSummary } from "./cursor-usage.js";
|
|
23
23
|
import { getChatGptSubscriptionStatus } from "./chatgpt-subscription.js";
|
|
24
24
|
import { applySharedPrefix } from "./shared-prefix.js";
|
|
25
|
-
import { sessionChatName } from "./session-name.js";
|
|
25
|
+
import { normalizeSessionDisplayTitle, sessionChatName, sessionDisplayTitleFromPrompt, } from "./session-name.js";
|
|
26
26
|
import { reloadRuntimeConfig } from "./runtime-reload.js";
|
|
27
27
|
import { acquireUpdateCommandGuard } from "./update-command-guard.js";
|
|
28
28
|
import { createInternalRestartEnv, INTERNAL_RESTART_ENV_VAR } from "./startup-lifecycle.js";
|
|
@@ -417,6 +417,21 @@ async function sendUsageError(platform, chatId, tool, err) {
|
|
|
417
417
|
function isUntitledSessionChatName(name) {
|
|
418
418
|
return name === "新会话" || name.startsWith("新会话-");
|
|
419
419
|
}
|
|
420
|
+
const sessionListViews = new Map();
|
|
421
|
+
function orderSessionsForCard(sessions) {
|
|
422
|
+
const tools = ["claude", "cursor", "codex", "ccc", "dsh"];
|
|
423
|
+
return tools.flatMap((tool) => sessions.filter((session) => session.tool === tool));
|
|
424
|
+
}
|
|
425
|
+
function parseSessionsCommand(text) {
|
|
426
|
+
const raw = text.trim().slice("/sessions".length).trim();
|
|
427
|
+
if (!raw)
|
|
428
|
+
return {};
|
|
429
|
+
if (raw === "--archived")
|
|
430
|
+
return { includeArchived: true, archivedOnly: true };
|
|
431
|
+
if (raw.startsWith("--all"))
|
|
432
|
+
return { includeArchived: true, query: raw.slice("--all".length).trim() || undefined };
|
|
433
|
+
return { query: raw };
|
|
434
|
+
}
|
|
420
435
|
async function fixedSessionChatName(chatId) {
|
|
421
436
|
const registry = await loadSessionRegistryForBinding();
|
|
422
437
|
const record = registry[chatId];
|
|
@@ -904,8 +919,8 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
904
919
|
logTrace(tid, "DONE", { outcome: canceled ? "safe_maintenance_canceled" : "safe_maintenance_cancel_unavailable" });
|
|
905
920
|
return;
|
|
906
921
|
}
|
|
907
|
-
if (isCommandText &&
|
|
908
|
-
logTrace(tid, "BRANCH", { cmd: "/restart safe" });
|
|
922
|
+
if (isCommandText && ["/restart safe", "/restartsf"].includes(normalizedCommandText)) {
|
|
923
|
+
logTrace(tid, "BRANCH", { cmd: "/restart safe", alias: normalizedCommandText });
|
|
909
924
|
try {
|
|
910
925
|
const job = await safeMaintenanceCoordinator.schedule("restart", safeMaintenanceRequester(platform, chatId, openId));
|
|
911
926
|
await platform.sendText(chatId, job.phase === "executing"
|
|
@@ -921,8 +936,8 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
921
936
|
}
|
|
922
937
|
return;
|
|
923
938
|
}
|
|
924
|
-
if (isCommandText &&
|
|
925
|
-
logTrace(tid, "BRANCH", { cmd: "/update safe" });
|
|
939
|
+
if (isCommandText && ["/update safe", "/updatesf"].includes(normalizedCommandText)) {
|
|
940
|
+
logTrace(tid, "BRANCH", { cmd: "/update safe", alias: normalizedCommandText });
|
|
926
941
|
const isGlobal = isRunningFromGlobalNpm();
|
|
927
942
|
if (!isGlobal) {
|
|
928
943
|
await platform.sendText(chatId, "当前进程非 npm 全局安装,无法使用 /update safe。请通过 npm install -g chatccc 安装后使用。").catch(() => { });
|
|
@@ -951,11 +966,14 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
951
966
|
return;
|
|
952
967
|
}
|
|
953
968
|
const maintenanceAllowedCommands = new Set([
|
|
954
|
-
"/stop", "/cancel", "/state", "/sessions", "/usage", "/safestatus", "/cancelsf", "/restart", "/update", "/reload", "/help",
|
|
969
|
+
"/stop", "/cancel", "/state", "/sessions", "/usage", "/safestatus", "/cancelsf", "/restart", "/restartsf", "/update", "/updatesf", "/reload", "/help",
|
|
970
|
+
"/pin", "/unpin", "/archive", "/unarchive",
|
|
955
971
|
]);
|
|
972
|
+
const maintenancePresentationCommand = normalizedCommandText.startsWith("/rename ")
|
|
973
|
+
|| normalizedCommandText.startsWith("/sessions ");
|
|
956
974
|
if (isSafeMaintenanceAdmissionClosed()
|
|
957
975
|
&& !acceptedBeforeSafeMaintenance
|
|
958
|
-
&& (!isCommandText || !maintenanceAllowedCommands.has(normalizedCommandText))) {
|
|
976
|
+
&& (!isCommandText || (!maintenanceAllowedCommands.has(normalizedCommandText) && !maintenancePresentationCommand))) {
|
|
959
977
|
await platform.sendText(chatId, "ChatCCC 正在等待安全重启或更新,当前不接受新的任务。请在维护完成后重新发送;可用 /safestatus 查看状态。").catch(() => { });
|
|
960
978
|
logTrace(tid, "DONE", { outcome: "safe_maintenance_admission_closed" });
|
|
961
979
|
return;
|
|
@@ -1513,6 +1531,27 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
1513
1531
|
console.error(`[${ts()}] [RENAME] ${platform.kind} P2P failed: ${err.message}`);
|
|
1514
1532
|
}
|
|
1515
1533
|
}
|
|
1534
|
+
// Display titles are independent from chatName. Preserve the existing IM
|
|
1535
|
+
// group-name behavior while giving session lists a durable user-facing title.
|
|
1536
|
+
if (!isCommandText) {
|
|
1537
|
+
try {
|
|
1538
|
+
const registry = await loadSessionRegistryForBinding();
|
|
1539
|
+
const current = registry[chatId];
|
|
1540
|
+
if (current && !current.displayTitle && current.namePolicy !== "fixed") {
|
|
1541
|
+
const displayTitle = sessionDisplayTitleFromPrompt(text);
|
|
1542
|
+
await recordSessionRegistry({
|
|
1543
|
+
chatId,
|
|
1544
|
+
sessionId,
|
|
1545
|
+
tool: descriptionTool,
|
|
1546
|
+
displayTitle,
|
|
1547
|
+
});
|
|
1548
|
+
await saveSessionPresentation(sessionId, { displayTitle });
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
catch (err) {
|
|
1552
|
+
console.error(`[${ts()}] [SESSION-TITLE] auto title failed: ${err.message}`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1516
1555
|
if (isCommandText && textLower === "/stop") {
|
|
1517
1556
|
logTrace(tid, "BRANCH", { cmd: "/stop" });
|
|
1518
1557
|
if (stopSession(sessionId)) {
|
|
@@ -1582,13 +1621,55 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
1582
1621
|
await sendStateCard(platform, chatId, sessionId, toolLabel, tid);
|
|
1583
1622
|
return;
|
|
1584
1623
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1624
|
+
const renameMatch = isCommandText ? text.trim().match(/^\/rename(?:\s+(.+))?$/i) : null;
|
|
1625
|
+
if (renameMatch) {
|
|
1626
|
+
logTrace(tid, "BRANCH", { cmd: "/rename" });
|
|
1627
|
+
if (!renameMatch[1]) {
|
|
1628
|
+
const registry = await loadSessionRegistryForBinding();
|
|
1629
|
+
const currentTitle = registry[chatId]?.displayTitle || registry[chatId]?.chatName || "(未命名)";
|
|
1630
|
+
await platform.sendText(chatId, `当前会话标题:${currentTitle}\n用法:/rename 新名称`).catch(() => { });
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
try {
|
|
1634
|
+
const displayTitle = normalizeSessionDisplayTitle(renameMatch[1]);
|
|
1635
|
+
await recordSessionRegistry({ chatId, sessionId, tool: descriptionTool, displayTitle });
|
|
1636
|
+
await saveSessionPresentation(sessionId, { displayTitle });
|
|
1637
|
+
await platform.sendText(chatId, `会话标题已更新为:${displayTitle}\n群名保持不变。`).catch(() => { });
|
|
1638
|
+
logTrace(tid, "DONE", { outcome: "rename_session", displayTitle });
|
|
1639
|
+
}
|
|
1640
|
+
catch (err) {
|
|
1641
|
+
await platform.sendText(chatId, `会话重命名失败:${err.message}`).catch(() => { });
|
|
1642
|
+
}
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
if (isCommandText && ["/pin", "/unpin", "/archive", "/unarchive"].includes(textLower)) {
|
|
1646
|
+
const registry = await loadSessionRegistryForBinding();
|
|
1647
|
+
const current = registry[chatId];
|
|
1648
|
+
const pinned = textLower === "/pin" ? true : textLower === "/unpin" ? false : current?.pinned;
|
|
1649
|
+
const archivedAt = textLower === "/archive" ? Date.now() : textLower === "/unarchive" ? null : current?.archivedAt;
|
|
1650
|
+
await recordSessionRegistry({ chatId, sessionId, tool: descriptionTool, pinned, archivedAt });
|
|
1651
|
+
await saveSessionPresentation(sessionId, { pinned, archivedAt });
|
|
1652
|
+
const message = textLower === "/pin" ? "会话已置顶。"
|
|
1653
|
+
: textLower === "/unpin" ? "会话已取消置顶。"
|
|
1654
|
+
: textLower === "/archive" ? "会话已归档;可用 /sessions --archived 查看。"
|
|
1655
|
+
: "会话已取消归档。";
|
|
1656
|
+
await platform.sendText(chatId, message).catch(() => { });
|
|
1657
|
+
logTrace(tid, "DONE", { outcome: textLower.slice(1) });
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
if (isCommandText && (textLower === "/sessions" || textLower.startsWith("/sessions "))) {
|
|
1661
|
+
const listOptions = parseSessionsCommand(text);
|
|
1662
|
+
logTrace(tid, "BRANCH", { cmd: "/sessions", ...listOptions });
|
|
1663
|
+
const allSessions = await getAllSessionsStatus(listOptions);
|
|
1664
|
+
const orderedSessions = orderSessionsForCard(allSessions);
|
|
1665
|
+
sessionListViews.set(chatId, orderedSessions.map((session) => session.sessionId));
|
|
1588
1666
|
const now = Date.now();
|
|
1589
|
-
const cardData =
|
|
1667
|
+
const cardData = orderedSessions.map((s) => ({
|
|
1590
1668
|
sessionId: s.sessionId,
|
|
1591
1669
|
chatName: s.chatName,
|
|
1670
|
+
displayTitle: s.displayTitle,
|
|
1671
|
+
pinned: s.pinned,
|
|
1672
|
+
archivedAt: s.archivedAt,
|
|
1592
1673
|
chatId: s.chatId,
|
|
1593
1674
|
chatType: s.chatType,
|
|
1594
1675
|
active: s.active,
|
|
@@ -1719,19 +1800,12 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
1719
1800
|
}
|
|
1720
1801
|
const index = parseInt(sessionMatch[1], 10) - 1;
|
|
1721
1802
|
logTrace(tid, "BRANCH", { cmd: "/session", index: index + 1 });
|
|
1722
|
-
const
|
|
1723
|
-
const
|
|
1724
|
-
const
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
const ordered = [
|
|
1729
|
-
...claudeOrdered,
|
|
1730
|
-
...cursorOrdered,
|
|
1731
|
-
...codexOrdered,
|
|
1732
|
-
...cccOrdered,
|
|
1733
|
-
...dshOrdered,
|
|
1734
|
-
];
|
|
1803
|
+
const viewIds = sessionListViews.get(chatId);
|
|
1804
|
+
const allSessions = await getAllSessionsStatus({ includeArchived: true, limit: 100 });
|
|
1805
|
+
const byId = new Map(allSessions.map((session) => [session.sessionId, session]));
|
|
1806
|
+
const ordered = viewIds !== undefined
|
|
1807
|
+
? viewIds.map((sessionId) => byId.get(sessionId)).filter((session) => !!session)
|
|
1808
|
+
: orderSessionsForCard(allSessions.filter((session) => !session.archivedAt).slice(0, 20));
|
|
1735
1809
|
if (ordered.length === 0) {
|
|
1736
1810
|
await platform.sendCard(chatId, "/session", "暂无历史会话。", "yellow");
|
|
1737
1811
|
logTrace(tid, "DONE", { outcome: "session_no_sessions" });
|
|
@@ -1773,6 +1847,9 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
1773
1847
|
tool: target.tool,
|
|
1774
1848
|
chatName: newName2,
|
|
1775
1849
|
namePolicy: fixedName2 ? "fixed" : "auto",
|
|
1850
|
+
displayTitle: target.displayTitle,
|
|
1851
|
+
pinned: target.pinned,
|
|
1852
|
+
archivedAt: target.archivedAt ?? null,
|
|
1776
1853
|
newDescription: `${descPrefix2} ${target.sessionId}`,
|
|
1777
1854
|
initialTurnCount: target.turnCount,
|
|
1778
1855
|
initialContextTokens: 0,
|
|
@@ -2217,13 +2294,24 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
2217
2294
|
logTrace(tid, "DONE", { outcome: "effort_no_session", defaultTool });
|
|
2218
2295
|
return;
|
|
2219
2296
|
}
|
|
2220
|
-
if (isCommandText && textLower
|
|
2221
|
-
|
|
2222
|
-
|
|
2297
|
+
if (isCommandText && (/^\/rename(?:\s|$)/.test(textLower) || ["/pin", "/unpin", "/archive", "/unarchive"].includes(textLower))) {
|
|
2298
|
+
await platform.sendText(chatId, "当前没有绑定会话。请进入具体会话后再修改标题、置顶或归档状态。").catch(() => { });
|
|
2299
|
+
logTrace(tid, "DONE", { outcome: "session_presentation_no_session" });
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
if (isCommandText && (textLower === "/sessions" || textLower.startsWith("/sessions "))) {
|
|
2303
|
+
const listOptions = parseSessionsCommand(text);
|
|
2304
|
+
logTrace(tid, "BRANCH", { cmd: "/sessions", scope: "global", ...listOptions });
|
|
2305
|
+
const allSessions = await getAllSessionsStatus(listOptions);
|
|
2306
|
+
const orderedSessions = orderSessionsForCard(allSessions);
|
|
2307
|
+
sessionListViews.set(chatId, orderedSessions.map((session) => session.sessionId));
|
|
2223
2308
|
const now = Date.now();
|
|
2224
|
-
const cardData =
|
|
2309
|
+
const cardData = orderedSessions.map((s) => ({
|
|
2225
2310
|
sessionId: s.sessionId,
|
|
2226
2311
|
chatName: s.chatName,
|
|
2312
|
+
displayTitle: s.displayTitle,
|
|
2313
|
+
pinned: s.pinned,
|
|
2314
|
+
archivedAt: s.archivedAt,
|
|
2227
2315
|
chatId: s.chatId,
|
|
2228
2316
|
chatType: s.chatType,
|
|
2229
2317
|
active: s.active,
|
package/dist/src/session-name.js
CHANGED
|
@@ -5,3 +5,18 @@ export function cwdDisplayName(cwd) {
|
|
|
5
5
|
export function sessionChatName(left, cwd) {
|
|
6
6
|
return `${left}-${cwdDisplayName(cwd)}`;
|
|
7
7
|
}
|
|
8
|
+
/** Derive a deterministic user-facing title without changing the IM chat name. */
|
|
9
|
+
export function sessionDisplayTitleFromPrompt(prompt, maxLength = 32) {
|
|
10
|
+
const normalized = prompt.replace(/\s+/g, " ").trim();
|
|
11
|
+
if (!normalized)
|
|
12
|
+
return "新会话";
|
|
13
|
+
return normalized.length <= maxLength ? normalized : `${normalized.slice(0, maxLength - 1)}…`;
|
|
14
|
+
}
|
|
15
|
+
export function normalizeSessionDisplayTitle(value, maxLength = 80) {
|
|
16
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
17
|
+
if (!normalized)
|
|
18
|
+
throw new Error("会话名称不能为空");
|
|
19
|
+
if (normalized.length > maxLength)
|
|
20
|
+
throw new Error(`会话名称不能超过 ${maxLength} 个字符`);
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
package/dist/src/session.js
CHANGED
|
@@ -14,7 +14,8 @@ import { createCccAdapter } from "./adapters/ccc-adapter.js";
|
|
|
14
14
|
import { createDshAdapter } from "./adapters/dsh-adapter.js";
|
|
15
15
|
import { killProcessTree } from "./adapters/proc-tree-kill.js";
|
|
16
16
|
import { resourceMonitor, registerProcess, unregisterProcess } from "./adapters/resource-monitor.js";
|
|
17
|
-
import { buildImSkillsPromptCached, exportSkillSubDocs } from "./im-skills.js";
|
|
17
|
+
import { buildImSkillsPromptCached, exportSkillSubDocs, sessionImSkillsCacheDir, } from "./im-skills.js";
|
|
18
|
+
import { clearAgentCapabilityGrants, issueAgentCapabilityGrant, } from "./agent-capability-grants.js";
|
|
18
19
|
import { hasResponseStalled, observeResponseProgress } from "./response-stall.js";
|
|
19
20
|
import { classifyTerminalError, formatTerminalErrorNotice, formatTerminalErrorReason, } from "./terminal-error.js";
|
|
20
21
|
import { MAX_PROCESSED, clearFeishuMessageLedgerMemory, processedMessages, } from "./feishu-message-ingress.js";
|
|
@@ -384,6 +385,7 @@ export function resetState() {
|
|
|
384
385
|
sessionModelOverrides.clear();
|
|
385
386
|
sessionEffortOverrides.clear();
|
|
386
387
|
sessionFastModeOverrides.clear();
|
|
388
|
+
clearAgentCapabilityGrants();
|
|
387
389
|
adapterCache.clear();
|
|
388
390
|
stopUnifiedDisplayLoop();
|
|
389
391
|
console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions + bindings)`);
|
|
@@ -602,6 +604,22 @@ export async function saveSessionTool(sessionId, tool, chatName) {
|
|
|
602
604
|
tool,
|
|
603
605
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
604
606
|
...(mergedChatName ? { chatName: mergedChatName } : {}),
|
|
607
|
+
...(existing?.displayTitle ? { displayTitle: existing.displayTitle } : {}),
|
|
608
|
+
...(existing?.pinned ? { pinned: true } : {}),
|
|
609
|
+
...(existing?.archivedAt ? { archivedAt: existing.archivedAt } : {}),
|
|
610
|
+
};
|
|
611
|
+
await saveSessionTools(data);
|
|
612
|
+
}
|
|
613
|
+
export async function saveSessionPresentation(sessionId, patch) {
|
|
614
|
+
const data = await loadSessionTools();
|
|
615
|
+
const existing = data[sessionId];
|
|
616
|
+
if (!existing)
|
|
617
|
+
return;
|
|
618
|
+
data[sessionId] = {
|
|
619
|
+
...existing,
|
|
620
|
+
...(patch.displayTitle !== undefined ? { displayTitle: patch.displayTitle } : {}),
|
|
621
|
+
...(patch.pinned !== undefined ? { pinned: patch.pinned } : {}),
|
|
622
|
+
...(patch.archivedAt === null ? { archivedAt: undefined } : patch.archivedAt !== undefined ? { archivedAt: patch.archivedAt } : {}),
|
|
605
623
|
};
|
|
606
624
|
await saveSessionTools(data);
|
|
607
625
|
}
|
|
@@ -674,6 +692,9 @@ export async function recordSessionRegistry(update) {
|
|
|
674
692
|
tool: update.tool,
|
|
675
693
|
chatType: update.chatType ?? existing?.chatType,
|
|
676
694
|
chatName: update.chatName ?? existing?.chatName ?? "",
|
|
695
|
+
displayTitle: update.displayTitle ?? existing?.displayTitle,
|
|
696
|
+
pinned: update.pinned ?? existing?.pinned,
|
|
697
|
+
archivedAt: update.archivedAt === null ? undefined : update.archivedAt ?? existing?.archivedAt,
|
|
677
698
|
namePolicy: update.namePolicy ?? existing?.namePolicy,
|
|
678
699
|
turnCount: update.turnCount ?? existing?.turnCount ?? 0,
|
|
679
700
|
lastContextTokens: update.lastContextTokens ?? existing?.lastContextTokens ?? 0,
|
|
@@ -809,7 +830,7 @@ export function accumulateBlockContent(block, state, toolCallMap) {
|
|
|
809
830
|
}
|
|
810
831
|
}
|
|
811
832
|
export async function switchChatBinding(args) {
|
|
812
|
-
const { chatId, chatType, oldSessionId, newSessionId, tool, chatName, namePolicy, newDescription, initialTurnCount = 0, initialContextTokens = 0, updateChatInfoFn, } = args;
|
|
833
|
+
const { chatId, chatType, oldSessionId, newSessionId, tool, chatName, namePolicy, displayTitle, pinned, archivedAt, newDescription, initialTurnCount = 0, initialContextTokens = 0, updateChatInfoFn, } = args;
|
|
813
834
|
// Step 1: 群聊场景先调用飞书 API(不可逆操作放最前)。
|
|
814
835
|
// 私聊跳过——p2p chatId 调 updateChatInfo 必然失败。
|
|
815
836
|
if (chatType !== "p2p") {
|
|
@@ -847,6 +868,9 @@ export async function switchChatBinding(args) {
|
|
|
847
868
|
chatType,
|
|
848
869
|
chatName,
|
|
849
870
|
namePolicy,
|
|
871
|
+
displayTitle,
|
|
872
|
+
pinned,
|
|
873
|
+
archivedAt,
|
|
850
874
|
turnCount: initialTurnCount,
|
|
851
875
|
lastContextTokens: initialContextTokens,
|
|
852
876
|
startTime: now,
|
|
@@ -992,16 +1016,18 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
992
1016
|
if (tid)
|
|
993
1017
|
logTrace(tid, "SESSION_START", { sessionId, tool, cwd, turn: (sessionInfoMap.get(_chatId)?.turnCount ?? 0) + 1 });
|
|
994
1018
|
console.log(`[${ts()}] Running ${adapter.displayName} session: ${sessionId} (${formatToolConfigForLog(tool, info?.model, sessionId)}, cwd=${cwd})`);
|
|
995
|
-
//
|
|
1019
|
+
// 会话专属目录防止并发覆盖;capability grant 防止跨会话投递。
|
|
996
1020
|
const feishuSkillDir = join(PROJECT_ROOT, "im-skills", "feishu-skill");
|
|
997
1021
|
const wechatImageSkillDir = join(PROJECT_ROOT, "im-skills", "wechat-image-skill");
|
|
998
1022
|
const wechatFileSkillDir = join(PROJECT_ROOT, "im-skills", "wechat-file-skill");
|
|
999
1023
|
const wechatVideoSkillDir = join(PROJECT_ROOT, "im-skills", "wechat-video-skill");
|
|
1000
|
-
const imSkillsCacheDir = join(USER_DATA_DIR, "im-skills");
|
|
1024
|
+
const imSkillsCacheDir = sessionImSkillsCacheDir(join(USER_DATA_DIR, "im-skills"), sessionId);
|
|
1025
|
+
const agentCapabilityGrant = issueAgentCapabilityGrant(sessionId);
|
|
1001
1026
|
const skillVariables = {
|
|
1002
1027
|
cwd,
|
|
1003
1028
|
session_id: sessionId,
|
|
1004
1029
|
open_id: options.initiatorOpenId,
|
|
1030
|
+
agent_capability_grant: agentCapabilityGrant,
|
|
1005
1031
|
im_skills_cache_dir: imSkillsCacheDir,
|
|
1006
1032
|
delegate_task_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/delegate-task`,
|
|
1007
1033
|
set_cwd_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/set-cwd`,
|
|
@@ -2141,7 +2167,7 @@ export async function getSessionStatus(chatId) {
|
|
|
2141
2167
|
accumulatedLength,
|
|
2142
2168
|
};
|
|
2143
2169
|
}
|
|
2144
|
-
export async function getAllSessionsStatus() {
|
|
2170
|
+
export async function getAllSessionsStatus(options = {}) {
|
|
2145
2171
|
const registry = await loadSessionRegistry();
|
|
2146
2172
|
const registryEntries = Object.values(registry)
|
|
2147
2173
|
.filter((record) => record.chatId && record.sessionId && record.tool)
|
|
@@ -2159,6 +2185,9 @@ export async function getAllSessionsStatus() {
|
|
|
2159
2185
|
sessionId,
|
|
2160
2186
|
tool: record.tool,
|
|
2161
2187
|
chatName: record.chatName ?? "",
|
|
2188
|
+
displayTitle: record.displayTitle ?? "",
|
|
2189
|
+
pinned: record.pinned ?? false,
|
|
2190
|
+
archivedAt: record.archivedAt,
|
|
2162
2191
|
turnCount: 0,
|
|
2163
2192
|
lastContextTokens: 0,
|
|
2164
2193
|
startTime: active?.startTime ?? createdAt,
|
|
@@ -2167,9 +2196,13 @@ export async function getAllSessionsStatus() {
|
|
|
2167
2196
|
sortTime: active?.startTime ?? createdAt,
|
|
2168
2197
|
};
|
|
2169
2198
|
});
|
|
2199
|
+
const query = options.query?.trim().toLowerCase() ?? "";
|
|
2170
2200
|
const entries = [...registryEntries, ...orphanEntries]
|
|
2171
|
-
.
|
|
2172
|
-
.
|
|
2201
|
+
.filter((entry) => options.archivedOnly ? !!entry.archivedAt : options.includeArchived ? true : !entry.archivedAt)
|
|
2202
|
+
.filter((entry) => !query || [entry.displayTitle, entry.chatName, entry.sessionId, entry.tool]
|
|
2203
|
+
.some((value) => String(value ?? "").toLowerCase().includes(query)))
|
|
2204
|
+
.sort((a, b) => Number(!!b.pinned) - Number(!!a.pinned) || b.sortTime - a.sortTime)
|
|
2205
|
+
.slice(0, options.limit ?? 20);
|
|
2173
2206
|
// 并行解析每个 session 的 model/effort(cursor 涉及异步 store IO)
|
|
2174
2207
|
return Promise.all(entries.map(async (info) => {
|
|
2175
2208
|
const { model, effort } = await resolveModelEffort(info.tool, info.sessionId);
|
|
@@ -2178,6 +2211,9 @@ export async function getAllSessionsStatus() {
|
|
|
2178
2211
|
chatType: info.chatType,
|
|
2179
2212
|
sessionId: info.sessionId,
|
|
2180
2213
|
chatName: info.chatName || "",
|
|
2214
|
+
displayTitle: info.displayTitle || "",
|
|
2215
|
+
pinned: info.pinned ?? false,
|
|
2216
|
+
...(info.archivedAt ? { archivedAt: info.archivedAt } : {}),
|
|
2181
2217
|
active: !!activePrompts.get(info.sessionId) &&
|
|
2182
2218
|
!activePrompts.get(info.sessionId)?.stopped &&
|
|
2183
2219
|
!activePrompts.get(info.sessionId)?.abnormalExit,
|