chatccc 0.2.270 → 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 (42) hide show
  1. package/README.md +16 -10
  2. package/config.sample.json +4 -3
  3. package/deepccc-agent/README.md +147 -61
  4. package/deepccc-agent/package.json +5 -2
  5. package/dist/deepccc-agent/src/attachments.js +192 -0
  6. package/dist/deepccc-agent/src/cli.js +59 -13
  7. package/dist/deepccc-agent/src/config.js +57 -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 +68 -21
  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 +5 -1
  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/application/task-execution-service.js +330 -97
  24. package/dist/src/agent-team/domain/task-run.js +14 -1
  25. package/dist/src/agent-team/infrastructure/task-execution-runtime.js +7 -2
  26. package/dist/src/agent-team/main-agent-bootstrap.js +24 -1
  27. package/dist/src/agent-team/repositories/json-task-run-repository.js +22 -4
  28. package/dist/src/agent-team/web/agent-team-page.js +14 -7
  29. package/dist/src/cards.js +7 -4
  30. package/dist/src/config.js +12 -0
  31. package/dist/src/im-skills.js +9 -2
  32. package/dist/src/orchestrator.js +117 -29
  33. package/dist/src/safe-maintenance.js +4 -1
  34. package/dist/src/session-name.js +15 -0
  35. package/dist/src/session.js +54 -9
  36. package/dist/src/web-ui.js +76 -32
  37. package/im-skills/feishu-skill/receive-send-file.md +3 -2
  38. package/im-skills/feishu-skill/receive-send-image.md +3 -2
  39. package/im-skills/feishu-skill/send-file.mjs +6 -5
  40. package/im-skills/feishu-skill/send-image.mjs +6 -5
  41. package/im-skills/feishu-skill/skill.md +4 -2
  42. package/package.json +1 -1
@@ -14,7 +14,7 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
14
14
  .status-row{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 3px 10px;color:var(--muted);font-size:11px}.workspace-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.save-state{min-width:78px;text-align:right;color:var(--success);font-weight:700}.save-state::before{content:"● ";font-size:9px}.save-state.saving{color:#b7791f}.save-state.error{color:var(--danger)}
15
15
  .board-scroll{overflow-x:auto;padding-bottom:12px}.board{display:grid;grid-template-columns:repeat(5,minmax(230px,1fr));gap:11px;min-width:1200px}.column{display:flex;flex-direction:column;min-height:calc(100vh - 230px);background:var(--soft);border:1px solid var(--line);border-radius:15px;overflow:hidden}.column.drag-over{border-color:var(--accent);box-shadow:0 0 0 3px rgba(103,88,232,.13)}.column-head{display:flex;align-items:center;gap:8px;padding:11px 10px 7px 13px}.column-dot{width:7px;height:7px;border-radius:50%;background:var(--accent)}.column-title{font-size:13px;font-weight:800}.count{margin-left:auto;display:inline-grid;place-items:center;min-width:23px;height:23px;padding:0 7px;border-radius:999px;background:var(--panel);border:1px solid var(--line);color:var(--muted);font-size:10px}.column-add{width:27px;height:27px;display:grid;place-items:center;padding:0;border-color:transparent;border-radius:8px;background:transparent;color:var(--muted);font-size:19px;font-weight:500;line-height:1}.column-add:hover{border-color:var(--line);background:var(--panel);color:var(--accent)}.task-list{flex:1;min-height:100px;padding:4px 10px 10px}.task{position:relative;background:var(--panel2);border:1px solid var(--line);border-radius:11px;padding:12px;margin:8px 0;box-shadow:0 4px 15px rgba(34,42,65,.045);cursor:pointer}.task:hover{border-color:#d1d5e1;box-shadow:0 8px 22px rgba(34,42,65,.08)}.task.dragging{opacity:.35;cursor:grabbing}.task h3{font-size:13px;line-height:1.45;margin:0 42px 6px 0}.task p{font-size:11px;line-height:1.55;color:var(--muted);margin:0;white-space:pre-wrap;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}.task-actions{display:flex;gap:4px;position:absolute;right:7px;top:7px;opacity:0;transition:opacity .15s}.task:hover .task-actions,.task:focus-within .task-actions{opacity:1}.icon-btn{padding:3px 6px;border-radius:6px;background:var(--soft);color:var(--muted);font-size:10px}.empty{padding:20px 8px;text-align:center;color:#9aa3b4;font-size:11px}
16
16
  .task.running{cursor:default;border-color:#cfc8ff}.task-run{margin-top:10px;padding-top:9px;border-top:1px solid var(--line)}.task-run-head{display:flex;align-items:center;justify-content:space-between;gap:7px}.run-status{font-size:10px;font-weight:800;color:var(--muted)}.run-status.running,.run-status.queued{color:var(--accent)}.run-status.succeeded{color:var(--success)}.run-status.failed,.run-status.interrupted{color:var(--danger)}.run-result{margin-top:6px!important;padding:7px 8px;border-radius:7px;background:var(--soft);display:-webkit-box!important;-webkit-line-clamp:3!important}.run-button{padding:5px 8px;font-size:10px;border-radius:7px}.run-button.primary{box-shadow:none}
17
- .modal-backdrop{position:fixed;inset:0;z-index:20;background:rgba(24,32,51,.36);display:grid;place-items:center;padding:20px;backdrop-filter:blur(3px)}.modal{width:min(560px,100%);background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:20px;box-shadow:0 28px 90px rgba(35,43,65,.24)}.modal h2{margin:0 0 16px}.field{display:grid;gap:7px;margin-bottom:14px}.field label{font-size:13px;color:var(--text);font-weight:650}.field textarea{min-height:150px;resize:vertical}.modal-actions{display:flex;justify-content:flex-end;gap:9px}.task-detail-modal{width:min(980px,100%);max-height:calc(100vh - 40px);padding:0;display:flex;flex-direction:column;overflow:hidden}.task-detail-scroll{min-height:0;overflow-y:auto;padding:22px 24px}.task-detail-modal>.modal-actions{flex:none;padding:14px 24px;border-top:1px solid var(--line);background:var(--panel)}.task-execution-details{margin-top:22px;padding-top:20px;border-top:1px solid var(--line)}.task-execution-title{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}.task-execution-title h3{margin:0;font-size:16px}.run-meta{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:14px}.run-meta-item{padding:9px 10px;border:1px solid var(--line);border-radius:9px;background:var(--soft);min-width:0}.run-meta-item span{display:block;color:var(--muted);font-size:10px;margin-bottom:4px}.run-meta-item strong{display:block;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.execution-timeline{display:grid;gap:9px}.timeline-entry{border:1px solid var(--line);border-radius:10px;background:var(--soft);overflow:hidden}.timeline-entry.tool_use{border-left:3px solid var(--accent)}.timeline-entry.tool_result{border-left:3px solid var(--success)}.timeline-entry.error{border-left-color:var(--danger)}.timeline-head{display:flex;justify-content:space-between;gap:12px;padding:8px 10px;border-bottom:1px solid var(--line);color:var(--muted);font-size:10px;font-weight:800}.timeline-entry pre{margin:0;padding:11px 12px;max-height:none;overflow:visible;white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--text)}.timeline-empty{padding:22px;border:1px dashed var(--line);border-radius:10px;text-align:center;color:var(--muted);font-size:12px}.directory-modal{width:min(820px,100%)}.directory-layout{display:grid;grid-template-columns:180px minmax(0,1fr);min-height:430px;border:1px solid var(--line);border-radius:12px;overflow:hidden;margin-bottom:16px}.directory-sidebar{padding:12px;background:var(--soft);border-right:1px solid var(--line)}.directory-label{margin:2px 6px 9px;color:var(--muted);font-size:11px;font-weight:800;letter-spacing:.05em}.directory-locations{display:grid;gap:4px}.directory-location{width:100%;padding:8px 9px;border:0;background:transparent;text-align:left;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.directory-location:hover{border:0;background:var(--accent-soft);color:var(--accent)}.directory-main{min-width:0;display:flex;flex-direction:column;padding:12px}.directory-toolbar{display:grid;grid-template-columns:auto auto minmax(0,1fr);gap:7px}.directory-toolbar button{padding:8px 11px}.directory-toolbar input{min-width:0;padding:8px 10px}.directory-list{flex:1;height:310px;overflow:auto;margin-top:10px;border:1px solid var(--line);border-radius:10px;background:var(--soft);padding:6px}.directory-row{width:100%;display:flex;align-items:center;gap:9px;padding:9px 10px;border:0;background:transparent;text-align:left;font-size:12px}.directory-row:hover{border:0;background:#fff}.directory-row.selected{border:0;background:var(--accent-soft);color:var(--accent)}.directory-folder-icon{color:#b7791f;font-size:15px}.directory-empty{padding:46px 16px;text-align:center;color:var(--muted);font-size:12px}.directory-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:10px;color:var(--muted);font-size:11px}.directory-footer label{display:flex;align-items:center;gap:6px;white-space:nowrap}.directory-footer input{width:15px;height:15px;margin:0}.directory-selected{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}.toast{position:fixed;right:22px;bottom:22px;z-index:30;max-width:min(420px,calc(100vw - 44px));padding:12px 15px;border-radius:10px;background:#253047;color:#fff;border:1px solid #35415a;box-shadow:0 16px 50px rgba(35,43,65,.25)}.toast.error{border-color:#ef9bad;background:#9f2942}
17
+ .modal-backdrop{position:fixed;inset:0;z-index:20;background:rgba(24,32,51,.36);display:grid;place-items:center;padding:20px;backdrop-filter:blur(3px)}.modal{width:min(560px,100%);background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:20px;box-shadow:0 28px 90px rgba(35,43,65,.24)}.modal h2{margin:0 0 16px}.field{display:grid;gap:7px;margin-bottom:14px}.field label{font-size:13px;color:var(--text);font-weight:650}.field textarea{min-height:150px;resize:vertical}.modal-actions{display:flex;justify-content:flex-end;gap:9px}.task-detail-modal{width:min(980px,100%);max-height:calc(100vh - 40px);padding:0;display:flex;flex-direction:column;overflow:hidden}.task-detail-scroll{min-height:0;overflow-y:auto;padding:22px 24px}.task-detail-modal>.modal-actions{flex:none;padding:14px 24px;border-top:1px solid var(--line);background:var(--panel)}.task-execution-details{margin-top:22px;padding-top:20px;border-top:1px solid var(--line)}.task-execution-title{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}.task-execution-title h3{margin:0;font-size:16px}.run-history-toolbar{display:flex;align-items:center;gap:8px;margin-bottom:12px}.run-history-toolbar select{min-width:0;flex:1}.run-history-toolbar button{white-space:nowrap}.run-meta{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:14px}.run-meta-item{padding:9px 10px;border:1px solid var(--line);border-radius:9px;background:var(--soft);min-width:0}.run-meta-item span{display:block;color:var(--muted);font-size:10px;margin-bottom:4px}.run-meta-item strong{display:block;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.execution-timeline{display:grid;gap:9px}.timeline-entry{border:1px solid var(--line);border-radius:10px;background:var(--soft);overflow:hidden}.timeline-entry.tool_use{border-left:3px solid var(--accent)}.timeline-entry.tool_result{border-left:3px solid var(--success)}.timeline-entry.error{border-left-color:var(--danger)}.timeline-head{display:flex;justify-content:space-between;gap:12px;padding:8px 10px;border-bottom:1px solid var(--line);color:var(--muted);font-size:10px;font-weight:800}.timeline-entry pre{margin:0;padding:11px 12px;max-height:none;overflow:visible;white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--text)}.timeline-empty{padding:22px;border:1px dashed var(--line);border-radius:10px;text-align:center;color:var(--muted);font-size:12px}.directory-modal{width:min(820px,100%)}.directory-layout{display:grid;grid-template-columns:180px minmax(0,1fr);min-height:430px;border:1px solid var(--line);border-radius:12px;overflow:hidden;margin-bottom:16px}.directory-sidebar{padding:12px;background:var(--soft);border-right:1px solid var(--line)}.directory-label{margin:2px 6px 9px;color:var(--muted);font-size:11px;font-weight:800;letter-spacing:.05em}.directory-locations{display:grid;gap:4px}.directory-location{width:100%;padding:8px 9px;border:0;background:transparent;text-align:left;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.directory-location:hover{border:0;background:var(--accent-soft);color:var(--accent)}.directory-main{min-width:0;display:flex;flex-direction:column;padding:12px}.directory-toolbar{display:grid;grid-template-columns:auto auto minmax(0,1fr);gap:7px}.directory-toolbar button{padding:8px 11px}.directory-toolbar input{min-width:0;padding:8px 10px}.directory-list{flex:1;height:310px;overflow:auto;margin-top:10px;border:1px solid var(--line);border-radius:10px;background:var(--soft);padding:6px}.directory-row{width:100%;display:flex;align-items:center;gap:9px;padding:9px 10px;border:0;background:transparent;text-align:left;font-size:12px}.directory-row:hover{border:0;background:#fff}.directory-row.selected{border:0;background:var(--accent-soft);color:var(--accent)}.directory-folder-icon{color:#b7791f;font-size:15px}.directory-empty{padding:46px 16px;text-align:center;color:var(--muted);font-size:12px}.directory-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:10px;color:var(--muted);font-size:11px}.directory-footer label{display:flex;align-items:center;gap:6px;white-space:nowrap}.directory-footer input{width:15px;height:15px;margin:0}.directory-selected{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}.toast{position:fixed;right:22px;bottom:22px;z-index:30;max-width:min(420px,calc(100vw - 44px));padding:12px 15px;border-radius:10px;background:#253047;color:#fff;border:1px solid #35415a;box-shadow:0 16px 50px rgba(35,43,65,.25)}.toast.error{border-color:#ef9bad;background:#9f2942}
18
18
  @media(max-width:1100px){.app-shell{grid-template-columns:78px minmax(0,1fr)}.sidebar{padding:22px 12px;align-items:center}.brand span:last-child,.nav-item span:last-child,.side-note{display:none}.nav-item{width:45px;height:45px;justify-content:center;padding:0}.workspace-bar{grid-template-columns:1fr 1fr}.workspace-bar input{grid-column:1/-1}.main-agent-bar{grid-template-columns:1fr}.main-agent-controls{justify-content:flex-end}.board{min-width:1200px}}
19
19
  @media(max-width:700px){.app-shell{display:block}.sidebar{display:none}.shell{padding:14px}.top{align-items:flex-start}.workspace-bar{grid-template-columns:1fr}.workspace-bar input{grid-column:auto}.main-agent-controls{display:grid;grid-template-columns:minmax(0,1fr) auto;justify-content:stretch}.main-agent-controls select{min-width:0}.board{min-width:1180px}.task-detail-scroll{padding:18px}.task-detail-modal>.modal-actions{padding:12px 18px}.run-meta{grid-template-columns:repeat(2,minmax(0,1fr))}.directory-layout{grid-template-columns:1fr}.directory-sidebar{border-right:0;border-bottom:1px solid var(--line)}.directory-locations{display:flex;overflow-x:auto}.directory-location{width:auto;min-width:max-content}.directory-toolbar{grid-template-columns:auto auto 1fr}.directory-list{height:270px}}
20
20
  </style>
@@ -60,6 +60,7 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
60
60
  <div class="field"><label for="task-description">描述</label><textarea id="task-description" maxlength="20000"></textarea></div>
61
61
  <section id="task-execution-details" class="task-execution-details hidden">
62
62
  <div class="task-execution-title"><h3>完整执行过程</h3><span id="task-run-status" class="run-status"></span></div>
63
+ <div class="run-history-toolbar"><select id="task-run-history" aria-label="选择执行记录"></select><button type="button" id="copy-task-run" class="ghost">复制记录</button></div>
63
64
  <div id="task-run-meta" class="run-meta"></div>
64
65
  <div id="task-transcript" class="execution-timeline"></div>
65
66
  </section>
@@ -107,7 +108,7 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
107
108
  <div id="toast" class="toast hidden"></div>
108
109
  <script>
109
110
  (function(){
110
- var state={board:null,binding:null,runs:[],runPollTimer:null,runPollBusy:false,workspaces:[],agentOptions:[],dragTaskId:null,modalTaskId:null,modalColumnId:null,modalDetailVersion:0,relinkBoardId:null,pendingAgentId:null,pendingWorkspacePath:null,workspaceSelectionVersion:0,directoryLocations:[],directoryPath:null,directoryParentPath:null,directorySelectedPath:null,directoryBrowseVersion:0,contactTimer:null,contactChecking:false,mutationQueue:Promise.resolve()};
111
+ var state={board:null,binding:null,runs:[],runPollTimer:null,runPollBusy:false,workspaces:[],agentOptions:[],dragTaskId:null,modalTaskId:null,modalColumnId:null,modalRunId:null,modalRunDetail:null,modalDetailVersion:0,relinkBoardId:null,pendingAgentId:null,pendingWorkspacePath:null,workspaceSelectionVersion:0,directoryLocations:[],directoryPath:null,directoryParentPath:null,directorySelectedPath:null,directoryBrowseVersion:0,contactTimer:null,contactChecking:false,mutationQueue:Promise.resolve()};
111
112
  var columns=['brainstorm','todo','doing','done','on_hold'];
112
113
  var recent=document.getElementById('recent');
113
114
  var pathInput=document.getElementById('workspace-path');
@@ -170,7 +171,7 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
170
171
  async function loadRuns(render){if(!state.board)return;var boardId=state.board.boardId;var result=await api('/api/agent-team/boards/'+encodeURIComponent(boardId)+'/runs');if(!state.board||state.board.boardId!==boardId)return;state.runs=result.runs||[];if(render!==false)renderBoard();}
171
172
  function latestRun(taskId){return state.runs.find(function(run){return run.taskId===taskId;})||null;}
172
173
  function isActiveRun(run){return !!run&&(run.state==='queued'||run.state==='running');}
173
- function runLabel(run){var labels={queued:'等待执行',running:run.stopRequestedAt?'正在停止':'主 Agent 执行中',succeeded:'执行成功',failed:'执行失败',canceled:'已停止',interrupted:'重启时中断'};return labels[run.state]||run.state;}
174
+ function runLabel(run){if(isActiveRun(run)&&run.stalledAt)return '疑似停滞';var labels={queued:'等待执行',running:run.stopRequestedAt?'正在停止':'主 Agent 执行中',succeeded:'执行成功',failed:'执行失败',canceled:'已停止',interrupted:'重启时中断'};return labels[run.state]||run.state;}
174
175
  function scheduleRunPoll(){if(state.runPollTimer){clearTimeout(state.runPollTimer);state.runPollTimer=null;}if(!state.runs.some(isActiveRun))return;state.runPollTimer=setTimeout(pollRuns,1500);}
175
176
  async function pollRuns(){if(state.runPollBusy||!state.board)return scheduleRunPoll();state.runPollBusy=true;var boardId=state.board.boardId;try{var results=await Promise.all([api('/api/agent-team/boards/'+encodeURIComponent(boardId)),api('/api/agent-team/boards/'+encodeURIComponent(boardId)+'/runs')]);if(state.board&&state.board.boardId===boardId){state.board=results[0].board;state.binding=results[0].binding||null;state.runs=results[1].runs||[];renderBoard();if(state.modalTaskId)await loadOpenTaskRunDetails(false);if(!state.runs.some(isActiveRun))setSaving('已保存','');}}catch(error){toast(error.message,true);}finally{state.runPollBusy=false;scheduleRunPoll();}}
176
177
  async function startTaskRun(task){if(!state.board)return;if(!state.binding||state.binding.status!=='ready')return toast('请先设置可用的主 Agent',true);try{setSaving('正在交给主 Agent…','saving');var result=await request('POST','/api/agent-team/boards/'+encodeURIComponent(state.board.boardId)+'/tasks/'+encodeURIComponent(task.id)+'/run',{expectedRevision:state.board.revision});state.board=result.board;state.runs=[result.run].concat(state.runs.filter(function(run){return run.runId!==result.run.runId;}));renderBoard();scheduleRunPoll();setSaving('任务执行中','saving');}catch(error){if(error.code==='revision_conflict')await reloadBoard();toast(error.message,true);setSaving('执行失败','error');}}
@@ -209,15 +210,19 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
209
210
  state.mutationQueue=state.mutationQueue.then(async function(){setSaving('创建主 Agent…','saving');mainAgentButton.disabled=true;try{var result=await request('POST','/api/agent-team/boards/'+encodeURIComponent(state.board.boardId)+'/main-agent',{expectedRevision:state.board.revision,agentId:agentId});state.board=result.board;state.binding=result.binding;state.pendingAgentId=null;renderBoard();setSaving('已保存','');toast('主 Agent 群已就绪');return true;}catch(error){setSaving('设置失败','error');renderBoard();if(error.code==='feishu_dm_required')showDmModal();else if(error.code==='revision_conflict'){try{await reloadBoard();toast('其他页面修改了项目,已载入最新版本',true);}catch(reloadError){toast(reloadError.message,true);}}else toast(error.message,true);return false;}});return state.mutationQueue;
210
211
  }
211
212
  function formatRunTime(value){if(!value)return '—';var date=new Date(value);return Number.isNaN(date.getTime())?String(value):date.toLocaleString();}
213
+ function formatRunDuration(run){if(!run)return '—';var start=Date.parse(run.startedAt||run.createdAt);var end=Date.parse(run.finishedAt||new Date().toISOString());if(!Number.isFinite(start)||!Number.isFinite(end))return '—';var seconds=Math.max(0,Math.round((end-start)/1000));if(seconds<60)return seconds+' 秒';var minutes=Math.floor(seconds/60);return minutes+' 分 '+(seconds%60)+' 秒';}
214
+ function failureLabel(code){var labels={agent_error:'Agent 执行失败',agent_timeout:'Agent 响应超时',user_stopped:'用户主动停止',stop_timeout:'停止请求超时',process_missing:'Agent 进程消失',chatccc_restart:'ChatCCC 重启中断',task_deleted:'执行期间任务被删除'};return labels[code]||code||'—';}
212
215
  function addRunMeta(root,label,value){var item=document.createElement('div');item.className='run-meta-item';var key=document.createElement('span');key.textContent=label;var content=document.createElement('strong');content.textContent=value||'—';content.title=value||'—';item.append(key,content);root.appendChild(item);}
213
216
  function transcriptLabel(entry){var labels={prompt:'任务指令',thinking:'思考',text:'主 Agent 回复',tool_use:'调用工具',tool_result:'工具结果',search:'联网搜索',compact:'上下文压缩',status:'状态变化',notice:'提示'};var label=labels[entry.type]||entry.type;if(entry.name)label+=' · '+entry.name;return label;}
214
217
  function transcriptContent(entry){if(entry.type==='tool_use')return entry.input||'';if(entry.type==='tool_result')return entry.output||'';return entry.text||'';}
215
218
  function addTranscriptEntry(root,entry){var row=document.createElement('article');row.className='timeline-entry '+entry.type+(entry.isError?' error':'');var head=document.createElement('div');head.className='timeline-head';var label=document.createElement('span');label.textContent=transcriptLabel(entry);var time=document.createElement('span');time.textContent=formatRunTime(entry.at);head.append(label,time);var body=document.createElement('pre');body.textContent=transcriptContent(entry)||'(无文本内容)';row.append(head,body);root.appendChild(row);}
216
- function renderTaskExecution(task,run){var details=document.getElementById('task-execution-details');var title=document.getElementById('task-title');var description=document.getElementById('task-description');var save=document.getElementById('task-save');var cancel=document.getElementById('cancel');var active=isActiveRun(run);title.disabled=active;description.disabled=active;save.classList.toggle('hidden',active);cancel.textContent=active?'关闭':'取消';details.classList.remove('hidden');var status=document.getElementById('task-run-status');status.className='run-status '+(run?run.state:'');status.textContent=run?runLabel(run):'尚未执行';var meta=document.getElementById('task-run-meta');meta.innerHTML='';if(run){addRunMeta(meta,'执行次数','第 '+run.attempt+' 次');addRunMeta(meta,'执行 Agent',run.agentId);addRunMeta(meta,'开始时间',formatRunTime(run.startedAt||run.createdAt));addRunMeta(meta,'结束时间',formatRunTime(run.finishedAt));}else{addRunMeta(meta,'任务状态','尚未交给主 Agent');addRunMeta(meta,'创建时间',formatRunTime(task.createdAt));addRunMeta(meta,'更新时间',formatRunTime(task.updatedAt));}var transcript=document.getElementById('task-transcript');transcript.innerHTML='';var entries=run&&Array.isArray(run.transcript)?run.transcript.slice():[];var hasText=entries.some(function(entry){return entry.type==='text';});if(run&&run.result&&!hasText)entries.push({type:'text',at:run.finishedAt||run.updatedAt,text:run.result});if(run&&run.error)entries.push({type:'notice',at:run.finishedAt||run.updatedAt,text:run.error,isError:true});if(!entries.length){var empty=document.createElement('div');empty.className='timeline-empty';empty.textContent=run?'暂时还没有可显示的执行事件':'任务尚未执行,交给主 Agent 后会在这里记录完整过程。';transcript.appendChild(empty);return;}entries.forEach(function(entry){addTranscriptEntry(transcript,entry);});}
217
- async function loadOpenTaskRunDetails(showError){if(!state.board||!state.modalTaskId)return;var task=state.board.tasks.find(function(candidate){return candidate.id===state.modalTaskId&&!candidate.deletedAt;});if(!task)return;var summary=latestRun(task.id);if(!summary){renderTaskExecution(task,null);return;}var version=++state.modalDetailVersion;try{var result=await api('/api/agent-team/boards/'+encodeURIComponent(state.board.boardId)+'/runs/'+encodeURIComponent(summary.runId));if(version!==state.modalDetailVersion||state.modalTaskId!==task.id)return;renderTaskExecution(task,result.run);}catch(error){if(showError!==false)toast(error.message,true);}}
219
+ function taskRuns(taskId){return state.runs.filter(function(run){return run.taskId===taskId;});}
220
+ function renderRunHistory(taskId){var select=document.getElementById('task-run-history');var runs=taskRuns(taskId);select.innerHTML='';if(!runs.length){var empty=document.createElement('option');empty.value='';empty.textContent='尚无执行记录';select.appendChild(empty);select.disabled=true;document.getElementById('copy-task-run').disabled=true;return;}select.disabled=false;runs.forEach(function(run){var option=document.createElement('option');option.value=run.runId;option.textContent='第 '+run.attempt+' 次 · '+runLabel(run)+' · '+formatRunTime(run.startedAt||run.createdAt);select.appendChild(option);});if(!state.modalRunId||!runs.some(function(run){return run.runId===state.modalRunId;}))state.modalRunId=runs[0].runId;select.value=state.modalRunId;document.getElementById('copy-task-run').disabled=false;}
221
+ function renderTaskExecution(task,run){state.modalRunDetail=run;renderRunHistory(task.id);var details=document.getElementById('task-execution-details');var title=document.getElementById('task-title');var description=document.getElementById('task-description');var save=document.getElementById('task-save');var cancel=document.getElementById('cancel');var active=isActiveRun(run);title.disabled=active;description.disabled=active;save.classList.toggle('hidden',active);cancel.textContent=active?'关闭':'取消';details.classList.remove('hidden');var status=document.getElementById('task-run-status');status.className='run-status '+(run?run.state:'');status.textContent=run?runLabel(run):'尚未执行';var meta=document.getElementById('task-run-meta');meta.innerHTML='';if(run){addRunMeta(meta,'执行次数','第 '+run.attempt+' 次');addRunMeta(meta,'执行 Agent',run.agentId);addRunMeta(meta,'耗时',formatRunDuration(run));addRunMeta(meta,'最后进度',formatRunTime(run.lastProgressAt));addRunMeta(meta,'开始时间',formatRunTime(run.startedAt||run.createdAt));addRunMeta(meta,'结束时间',formatRunTime(run.finishedAt));addRunMeta(meta,'Run ID',run.runId);addRunMeta(meta,'Trace ID',run.traceId);if(run.failureCode)addRunMeta(meta,'失败类型',failureLabel(run.failureCode));if(run.boardSyncPending)addRunMeta(meta,'看板同步','等待自动对账');}else{addRunMeta(meta,'任务状态','尚未交给主 Agent');addRunMeta(meta,'创建时间',formatRunTime(task.createdAt));addRunMeta(meta,'更新时间',formatRunTime(task.updatedAt));}var transcript=document.getElementById('task-transcript');transcript.innerHTML='';var entries=run&&Array.isArray(run.transcript)?run.transcript.slice():[];var hasText=entries.some(function(entry){return entry.type==='text';});if(run&&run.result&&!hasText)entries.push({type:'text',at:run.finishedAt||run.updatedAt,text:run.result});if(run&&run.error)entries.push({type:'notice',at:run.finishedAt||run.updatedAt,text:failureLabel(run.failureCode)+':'+run.error,isError:true});if(run&&run.syncError)entries.push({type:'notice',at:run.updatedAt,text:'看板对账失败:'+run.syncError,isError:true});if(!entries.length){var empty=document.createElement('div');empty.className='timeline-empty';empty.textContent=run?'暂时还没有可显示的执行事件':'任务尚未执行,交给主 Agent 后会在这里记录完整过程。';transcript.appendChild(empty);return;}entries.forEach(function(entry){addTranscriptEntry(transcript,entry);});}
222
+ async function loadOpenTaskRunDetails(showError){if(!state.board||!state.modalTaskId)return;var task=state.board.tasks.find(function(candidate){return candidate.id===state.modalTaskId&&!candidate.deletedAt;});if(!task)return;renderRunHistory(task.id);var summary=taskRuns(task.id).find(function(run){return run.runId===state.modalRunId;})||latestRun(task.id);if(!summary){renderTaskExecution(task,null);return;}state.modalRunId=summary.runId;var version=++state.modalDetailVersion;try{var result=await api('/api/agent-team/boards/'+encodeURIComponent(state.board.boardId)+'/runs/'+encodeURIComponent(summary.runId));if(version!==state.modalDetailVersion||state.modalTaskId!==task.id)return;renderTaskExecution(task,result.run);}catch(error){if(showError!==false)toast(error.message,true);}}
218
223
  function showCreate(columnId){if(!state.board)return toast('请先打开一个工作目录',true);state.modalTaskId=null;state.modalColumnId=columnId;document.getElementById('modal-title').textContent='添加任务';document.getElementById('task-title').value='';document.getElementById('task-description').value='';document.getElementById('task-title').disabled=false;document.getElementById('task-description').disabled=false;document.getElementById('task-save').classList.remove('hidden');document.getElementById('cancel').textContent='取消';document.getElementById('task-execution-details').classList.add('hidden');document.getElementById('modal').classList.remove('hidden');setTimeout(function(){document.getElementById('task-title').focus();},0);}
219
- function showEdit(task){state.modalTaskId=task.id;state.modalColumnId=task.columnId;document.getElementById('modal-title').textContent='任务详情';document.getElementById('task-title').value=task.title;document.getElementById('task-description').value=task.description;renderTaskExecution(task,latestRun(task.id));document.getElementById('modal').classList.remove('hidden');loadOpenTaskRunDetails(true);}
220
- function closeModal(){state.modalDetailVersion++;document.getElementById('modal').classList.add('hidden');state.modalTaskId=null;}
224
+ function showEdit(task){state.modalTaskId=task.id;state.modalColumnId=task.columnId;state.modalRunId=(latestRun(task.id)||{}).runId||null;document.getElementById('modal-title').textContent='任务详情';document.getElementById('task-title').value=task.title;document.getElementById('task-description').value=task.description;renderTaskExecution(task,latestRun(task.id));document.getElementById('modal').classList.remove('hidden');loadOpenTaskRunDetails(true);}
225
+ function closeModal(){state.modalDetailVersion++;document.getElementById('modal').classList.add('hidden');state.modalTaskId=null;state.modalRunId=null;state.modalRunDetail=null;}
221
226
  function removeTask(task){if(!confirm('删除任务“'+task.title+'”?'))return;mutate(function(){return request('DELETE','/api/agent-team/boards/'+encodeURIComponent(state.board.boardId)+'/tasks/'+encodeURIComponent(task.id),{expectedRevision:state.board.revision});});}
222
227
 
223
228
  document.querySelectorAll('[data-add]').forEach(function(button){button.addEventListener('click',function(){showCreate(button.dataset.add);});});
@@ -227,6 +232,8 @@ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
227
232
  });
228
233
  document.getElementById('task-form').addEventListener('submit',function(event){event.preventDefault();var title=document.getElementById('task-title').value;var description=document.getElementById('task-description').value;var taskId=state.modalTaskId;var columnId=state.modalColumnId;closeModal();mutate(function(){var root='/api/agent-team/boards/'+encodeURIComponent(state.board.boardId)+'/tasks';return taskId?request('PATCH',root+'/'+encodeURIComponent(taskId),{expectedRevision:state.board.revision,title:title,description:description}):request('POST',root,{expectedRevision:state.board.revision,title:title,description:description,columnId:columnId});});});
229
234
  document.getElementById('cancel').addEventListener('click',closeModal);document.getElementById('modal').addEventListener('click',function(event){if(event.target===event.currentTarget)closeModal();});
235
+ document.getElementById('task-run-history').addEventListener('change',function(event){state.modalRunId=event.currentTarget.value||null;loadOpenTaskRunDetails(true);});
236
+ document.getElementById('copy-task-run').addEventListener('click',async function(){if(!state.modalRunDetail)return;var text=JSON.stringify(state.modalRunDetail,null,2);try{if(navigator.clipboard&&navigator.clipboard.writeText)await navigator.clipboard.writeText(text);else{var area=document.createElement('textarea');area.value=text;area.style.position='fixed';area.style.opacity='0';document.body.appendChild(area);area.select();document.execCommand('copy');area.remove();}toast('执行记录已复制');}catch(error){toast('复制失败:'+error.message,true);}});
230
237
  document.querySelectorAll('[data-feature]').forEach(function(button){button.addEventListener('click',function(){showFeatureModal(button.dataset.feature);});});
231
238
  document.getElementById('feature-modal-ok').addEventListener('click',closeFeatureModal);document.getElementById('feature-modal').addEventListener('click',function(event){if(event.target===event.currentTarget)closeFeatureModal();});
232
239
  document.getElementById('directory-cancel').addEventListener('click',closeDirectoryModal);document.getElementById('directory-modal').addEventListener('click',function(event){if(event.target===event.currentTarget)closeDirectoryModal();});document.getElementById('directory-confirm').addEventListener('click',confirmDirectorySelection);document.getElementById('directory-up').addEventListener('click',function(){if(state.directoryParentPath)browseDirectory(state.directoryParentPath);});document.getElementById('directory-refresh').addEventListener('click',function(){if(state.directoryPath)browseDirectory(state.directoryPath);});document.getElementById('directory-show-hidden').addEventListener('change',function(){if(state.directoryPath)browseDirectory(state.directoryPath);});document.getElementById('directory-path').addEventListener('keydown',function(event){if(event.key==='Enter'){event.preventDefault();browseDirectory(event.currentTarget.value);}});
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",
@@ -270,6 +270,16 @@ function normalizePositiveInteger(raw, fallback) {
270
270
  const value = typeof raw === "string" ? Number(raw.trim()) : Number(raw);
271
271
  return Number.isInteger(value) && value > 0 ? value : fallback;
272
272
  }
273
+ function normalizeNullablePositiveInteger(raw, label) {
274
+ if (raw === undefined || raw === null || (typeof raw === "string" && raw.trim() === "")) {
275
+ return null;
276
+ }
277
+ const value = typeof raw === "string" ? Number(raw.trim()) : Number(raw);
278
+ if (Number.isInteger(value) && value > 0)
279
+ return value;
280
+ console.warn(`[CONFIG] ${label} 必须为正整数或留空,已按留空处理。`);
281
+ return null;
282
+ }
273
283
  function normalizeRawStreamAgentLogConfig(raw) {
274
284
  const obj = typeof raw === "object" && raw !== null
275
285
  ? raw
@@ -317,6 +327,7 @@ function loadConfig() {
317
327
  subModel: "",
318
328
  alternativeModel: "",
319
329
  effort: "",
330
+ maxOutputTokens: null,
320
331
  provider: "",
321
332
  gitCoAuthor: null,
322
333
  compactionTimeoutMs: DEFAULT_CCC_COMPACTION_TIMEOUT_MS,
@@ -527,6 +538,7 @@ function loadConfig() {
527
538
  subModel: normalizeOptionalConfigField(cccRaw.subModel, { label: "ccc.subModel" }),
528
539
  alternativeModel: normalizeOptionalConfigField(cccRaw.alternativeModel, { label: "ccc.alternativeModel" }),
529
540
  effort: normalizeOptionalConfigField(cccRaw.effort, { label: "ccc.effort" }),
541
+ maxOutputTokens: normalizeNullablePositiveInteger(cccRaw.maxOutputTokens, "ccc.maxOutputTokens"),
530
542
  provider: normalizeCccProviderOverride(cccRaw.provider),
531
543
  gitCoAuthor: typeof cccRaw.gitCoAuthor === "boolean" ? cccRaw.gitCoAuthor : null,
532
544
  compactionTimeoutMs: normalizePositiveInteger(cccRaw.compactionTimeoutMs, DEFAULT_CCC_COMPACTION_TIMEOUT_MS),
@@ -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,
@@ -1,8 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
+ import { tmpdir } from "node:os";
4
5
  import { USER_DATA_DIR } from "./config.js";
5
- export const SAFE_MAINTENANCE_FILE = join(USER_DATA_DIR, "state", "safe-maintenance.json");
6
+ export const SAFE_MAINTENANCE_FILE = process.env.VITEST
7
+ ? join(tmpdir(), `chatccc-vitest-safe-maintenance-${process.pid}-${randomUUID()}.json`)
8
+ : join(USER_DATA_DIR, "state", "safe-maintenance.json");
6
9
  export const SAFE_MAINTENANCE_STABLE_IDLE_MS = 1_000;
7
10
  const EMPTY_SNAPSHOT = {
8
11
  activeSessionIds: [],
@@ -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
+ }