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.
Files changed (35) hide show
  1. package/README.md +24 -22
  2. package/config.sample.json +4 -4
  3. package/deepccc-agent/README.md +147 -79
  4. package/deepccc-agent/package.json +68 -65
  5. package/dist/deepccc-agent/src/attachments.js +192 -0
  6. package/dist/deepccc-agent/src/cli.js +54 -13
  7. package/dist/deepccc-agent/src/config.js +49 -4
  8. package/dist/deepccc-agent/src/context.js +299 -16
  9. package/dist/deepccc-agent/src/file-tools.js +33 -0
  10. package/dist/deepccc-agent/src/index.js +48 -16
  11. package/dist/deepccc-agent/src/tool-protocol.js +14 -3
  12. package/dist/deepccc-agent/src/web-entry.js +72 -0
  13. package/dist/deepccc-agent/src/web-page.js +414 -0
  14. package/dist/deepccc-agent/src/web-runtime.js +331 -0
  15. package/dist/deepccc-agent/src/web-server.js +476 -0
  16. package/dist/deepccc-agent/src/web-session-store.js +162 -0
  17. package/dist/deepccc-agent/src/web-tool-presentation.js +123 -0
  18. package/dist/src/adapters/ccc-adapter.js +1 -0
  19. package/dist/src/agent-capability-grants.js +26 -0
  20. package/dist/src/agent-delegate-task.js +5 -2
  21. package/dist/src/agent-file-rpc.js +6 -1
  22. package/dist/src/agent-image-rpc.js +6 -1
  23. package/dist/src/agent-team/web/agent-team-page.js +250 -250
  24. package/dist/src/cards.js +7 -4
  25. package/dist/src/im-skills.js +9 -2
  26. package/dist/src/orchestrator.js +117 -29
  27. package/dist/src/session-name.js +15 -0
  28. package/dist/src/session.js +43 -7
  29. package/dist/src/web-ui.js +68 -51
  30. package/im-skills/feishu-skill/receive-send-file.md +3 -2
  31. package/im-skills/feishu-skill/receive-send-image.md +3 -2
  32. package/im-skills/feishu-skill/send-file.mjs +6 -5
  33. package/im-skills/feishu-skill/send-image.mjs +6 -5
  34. package/im-skills/feishu-skill/skill.md +4 -2
  35. 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 namePart = s.chatName ? `**${s.chatName}** ` : "";
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** 可在私聊中原地重置(忘掉当前上下文)。群聊会话请回到对应群聊继续,私聊不支持 **/session** 切换。"
341
- : "在会话群内发送 **/forget** 可重置当前会话(忘掉上下文,创建新 Session,保留工作目录和群聊)。\n发送 **/session 数字**(如 `/session 1`)可将当前群聊切换到列表中对应编号的会话。" } },
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",
@@ -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) {
@@ -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 && normalizedCommandText === "/restart safe") {
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 && normalizedCommandText === "/update safe") {
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
- if (isCommandText && textLower === "/sessions") {
1586
- logTrace(tid, "BRANCH", { cmd: "/sessions" });
1587
- const allSessions = await getAllSessionsStatus();
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 = allSessions.map((s) => ({
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 allSessions = await getAllSessionsStatus();
1723
- const claudeOrdered = allSessions.filter((s) => s.tool === "claude");
1724
- const cursorOrdered = allSessions.filter((s) => s.tool === "cursor");
1725
- const codexOrdered = allSessions.filter((s) => s.tool === "codex");
1726
- const cccOrdered = allSessions.filter((s) => s.tool === "ccc");
1727
- const dshOrdered = allSessions.filter((s) => s.tool === "dsh");
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 === "/sessions") {
2221
- logTrace(tid, "BRANCH", { cmd: "/sessions", scope: "global" });
2222
- const allSessions = await getAllSessionsStatus();
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 = allSessions.map((s) => ({
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,
@@ -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
+ }
@@ -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
- // 构建 IM skills prompt(sessionId 方式,无 token)
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
- .sort((a, b) => b.sortTime - a.sortTime)
2172
- .slice(0, 20);
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,