oh-my-im 0.1.13 → 0.1.16

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 CHANGED
@@ -158,7 +158,7 @@ omi
158
158
  /sessions [pi|codex]
159
159
  /use <pi|codex> <编号或 sessionId>
160
160
  /current
161
- /new
161
+ /new # 清空当前会话的 Agent session,下一条消息使用新会话
162
162
  ```
163
163
 
164
164
  Session 超级管理员额外拥有:
@@ -79,7 +79,7 @@ function extractAssistantText(message) {
79
79
  const text = value.content.flatMap((part) => {
80
80
  const item = asObject(part);
81
81
  return item?.type === "text" && typeof item.text === "string" ? [item.text] : [];
82
- }).join("\n");
82
+ }).join("");
83
83
  return text || undefined;
84
84
  }
85
85
  function runPiOnce(prompt, sessionId, config, callbacks = {}) {
package/dist/bot-app.js CHANGED
@@ -139,7 +139,7 @@ function shortModelName(model) {
139
139
  return value.includes("/") ? value.slice(value.lastIndexOf("/") + 1) : value;
140
140
  }
141
141
  function buildCardContent(content, note) {
142
- const safeContent = content.trim() || "Codex 正在处理...";
142
+ const safeContent = content.trim() || "[OMG] 正在分析...";
143
143
  const safeNote = note?.trim();
144
144
  return safeNote ? `${safeContent}\n\n\n${safeNote}` : safeContent;
145
145
  }
@@ -559,7 +559,7 @@ export async function runApp(configOverride, options = {}) {
559
559
  if (steer && text) {
560
560
  const steered = steer(text);
561
561
  await bot.sendText(message.conversationId, steered
562
- ? "已将这条消息作为引导发送给当前 Pi 任务。"
562
+ ? "[灵感]已将这条消息作为引导发送给当前 Pi 任务。"
563
563
  : "当前 Pi 任务暂时无法接收引导,消息已排队等待处理。");
564
564
  }
565
565
  else if (state.activeAgent === "codex" && text) {
@@ -609,23 +609,24 @@ export async function runApp(configOverride, options = {}) {
609
609
  const finishedTitle = (icon, state) => `${icon} ${title}${state} 总耗时 ${formatElapsed()}`;
610
610
  const responseMode = options.getResponseMode?.() ?? "card";
611
611
  const reply = responseMode === "card"
612
- ? await bot.sendThinkingCard(message, `${label} 正在处理...`, processingTitle())
612
+ ? await bot.sendThinkingCard(message, "[OMG] 正在分析...", processingTitle())
613
613
  : { conversationId: message.conversationId, mode: "text" };
614
614
  if (responseMode === "text")
615
- await bot.sendText(message.conversationId, `${label} 正在处理…`);
615
+ await bot.sendText(message.conversationId, "[OMG] 正在分析...");
616
616
  let elapsedTimer;
617
- let latestCardContent = `${label} 正在处理...`;
617
+ let latestCardContent = "[OMG] 正在分析...";
618
618
  const agent = selectedAgent;
619
619
  let prompt = "";
620
620
  try {
621
621
  let latestStats = {};
622
622
  let streamedText = "";
623
- let lastStreamText = "";
624
623
  let toolStatus = "";
625
624
  let lastUpdateAt = 0;
626
625
  let pendingUpdate;
627
626
  let cardUpdatesStopped = false;
628
- const getCardUpdateInterval = () => Math.max(500, options.getCardUpdateIntervalMs?.() ?? 3_000);
627
+ let cardUpdateChain = Promise.resolve();
628
+ const getCardUpdateInterval = () => Math.max(0, options.getCardUpdateIntervalMs?.() ?? 3_000);
629
+ const liveCardUpdates = responseMode === "card" && getCardUpdateInterval() > 0;
629
630
  const stopCardUpdates = () => {
630
631
  cardUpdatesStopped = true;
631
632
  if (pendingUpdate)
@@ -651,7 +652,11 @@ export async function runApp(configOverride, options = {}) {
651
652
  return;
652
653
  lastUpdateAt = Date.now();
653
654
  pendingUpdate = undefined;
654
- bot.updateReply(reply, title, content, { fallbackToText: false }).catch((err) => {
655
+ cardUpdateChain = cardUpdateChain.then(async () => {
656
+ if (cardUpdatesStopped)
657
+ return;
658
+ await bot.updateReply(reply, title, content, { fallbackToText: false });
659
+ }).catch((err) => {
655
660
  log.warn(`card update skipped: ${err instanceof Error ? err.message : String(err)}`);
656
661
  });
657
662
  };
@@ -668,7 +673,7 @@ export async function runApp(configOverride, options = {}) {
668
673
  pendingUpdate.unref();
669
674
  }
670
675
  };
671
- if (responseMode === "card" && getCardUpdateInterval() > 0) {
676
+ if (liveCardUpdates) {
672
677
  elapsedTimer = setInterval(() => {
673
678
  if (!cardUpdatesStopped)
674
679
  updateCard(processingTitle(), latestCardContent);
@@ -686,18 +691,17 @@ export async function runApp(configOverride, options = {}) {
686
691
  },
687
692
  onText: (content) => {
688
693
  const stats = formatStats(latestStats);
689
- const delta = content.startsWith(lastStreamText) ? content.slice(lastStreamText.length) : content;
690
- lastStreamText = content;
691
- if (delta.trim())
692
- streamedText = streamedText ? `${streamedText}\n\n${delta.trim()}` : delta.trim();
694
+ streamedText = content;
693
695
  toolStatus = "";
694
- updateCard(processingTitle(), buildCardContent(toolStatus ? `${streamedText}\n\n${toolStatus}` : (streamedText || content), stats ? `工具:${stats}` : undefined));
696
+ if (liveCardUpdates)
697
+ updateCard(processingTitle(), buildCardContent(toolStatus ? `${streamedText}\n\n${toolStatus}` : (streamedText || content), stats ? `工具:${stats}` : undefined));
695
698
  },
696
699
  onToolUse: (toolName, stats) => {
697
700
  latestStats = stats;
698
701
  const statsText = formatStats(stats);
699
- toolStatus = `调用工具:${toolName} x${stats[toolName] ?? 1}`;
700
- updateCard(processingTitle(), buildCardContent(streamedText ? `${streamedText}\n\n${toolStatus}` : toolStatus, statsText ? `工具:${statsText}` : undefined));
702
+ toolStatus = `[OMG] 调用工具:${toolName} x${stats[toolName] ?? 1}`;
703
+ if (liveCardUpdates)
704
+ updateCard(processingTitle(), buildCardContent(streamedText ? `${streamedText}\n\n${toolStatus}` : toolStatus, statsText ? `工具:${statsText}` : undefined));
701
705
  },
702
706
  });
703
707
  if (elapsedTimer) {
@@ -710,6 +714,7 @@ export async function runApp(configOverride, options = {}) {
710
714
  clearTimeout(pendingUpdate);
711
715
  pendingUpdate = undefined;
712
716
  }
717
+ await cardUpdateChain;
713
718
  state.stopCardUpdates = undefined;
714
719
  stopCardUpdatesForCurrentTask = undefined;
715
720
  state.sessions[agent] = result.sessionId ?? state.sessions[agent];
@@ -32,8 +32,8 @@ function normalizeConfig(value) {
32
32
  : Number(source.cardUpdateIntervalMs) <= 60
33
33
  ? Number(source.cardUpdateIntervalMs) * 1_000
34
34
  : Number(source.cardUpdateIntervalMs);
35
- if (cardUpdateIntervalMs !== 0 && (cardUpdateIntervalMs < 500 || cardUpdateIntervalMs > 60_000))
36
- throw new Error("卡片更新间隔需为 0(完成后一次性发送)或 0.5-60 秒");
35
+ if (cardUpdateIntervalMs !== 0 && (cardUpdateIntervalMs < 1_000 || cardUpdateIntervalMs > 60_000))
36
+ throw new Error("卡片更新间隔需为 0(完成后一次性发送)或 1-60 秒");
37
37
  const responseMode = source.responseMode === "text" ? "text" : "card";
38
38
  if (source.showElapsed !== undefined && typeof source.showElapsed !== "boolean")
39
39
  throw new Error("总耗时显示开关格式无效");
@@ -140,7 +140,15 @@ export function startDashboard(port, hooks, options = {}) {
140
140
  responsePanel.innerHTML = '<div class="field"><label>Agent 回复方式</label><div class="agent-options"><label><input type="radio" name="responseMode" value="card" checked> 互动卡片</label><label><input type="radio" name="responseMode" value="text"> 普通文本</label></div><div class="field-help">群聊和私聊统一使用此方式。普通文本只在 Agent 完成后发送最终结果。</div></div>';
141
141
  agentSettings.querySelector('.agent-row')?.append(responsePanel);
142
142
  const cardIntervalInput = document.querySelector('#cardUpdateIntervalMs');
143
- if (cardIntervalInput) cardIntervalInput.min = '-60';
143
+ if (cardIntervalInput) {
144
+ cardIntervalInput.min = '0';
145
+ cardIntervalInput.max = '60';
146
+ cardIntervalInput.step = '1';
147
+ const field = cardIntervalInput.closest('.compact-number-field');
148
+ if (field?.lastChild?.nodeType === Node.TEXT_NODE) {
149
+ field.lastChild.textContent = ' 秒(0:完成后一次性发送,1-60:按间隔更新)';
150
+ }
151
+ }
144
152
  panel.innerHTML = '<div class="field"><label>Pi 默认模型</label><select id="piModel"><option value="">使用 Pi CLI 默认模型</option></select><div class="field-help">群聊和私聊使用 Pi 时都采用此模型。Codex 始终使用系统 Codex CLI 的默认模型。</div></div>';
145
153
  agentSettings.querySelector('.agent-row')?.append(panel);
146
154
  const loadModels = async (agent) => {
@@ -166,6 +174,10 @@ export function startDashboard(port, hooks, options = {}) {
166
174
  const response = await fetch('/api/state', { cache: 'no-store' });
167
175
  const body = await response.json();
168
176
  const models = body.config?.agentModels || {};
177
+ const intervalInput = document.querySelector('#cardUpdateIntervalMs');
178
+ if (intervalInput && body.config?.cardUpdateIntervalMs !== undefined) {
179
+ intervalInput.value = String(body.config.cardUpdateIntervalMs / 1000);
180
+ }
169
181
  const responseMode = body.config?.responseMode === 'text' ? 'text' : 'card';
170
182
  document.querySelectorAll('input[name=responseMode]').forEach((input) => { input.checked = input.value === responseMode; });
171
183
  document.querySelector('#piModel').value = models.pi || '';
@@ -404,7 +416,7 @@ async function loadConfiguredMembers(rule,group){const people=rule.querySelector
404
416
  function addConfiguredRule(group){const rule=document.createElement('article');rule.className='rule';rule.innerHTML='<div class="rule-grid configured-grid"><div class="field"><label>钉钉群</label><div class="rule-value group-value"></div></div><div class="field"><label>钉钉人员</label><div class="member-list people"></div></div><button class="delete" title="删除规则">×</button></div>';rule._targets=group.targets;rule._group=group;rule.querySelector('.group-value').textContent=group.groupName;rule.querySelector('.delete').onclick=()=>rule.remove();rules.append(rule);void loadConfiguredMembers(rule,group)}
405
417
  const keywordIds={pause:'keywordsPause',monitorOpen:'keywordsMonitorOpen',monitorStop:'keywordsMonitorStop',switchPi:'keywordsSwitchPi',switchCodex:'keywordsSwitchCodex'};
406
418
  const parseKeywords=id=>document.querySelector('#'+id).value.split(/[||]+/).map(value=>value.trim()).filter(Boolean);
407
- function renderStatic(data,replaceConfig=false){rulesPanel.querySelector('.rules-toggle').textContent='钉钉群监控绑定 ('+data.config.targets.length+')';if(replaceConfig){if(document.querySelector('#privateChatEnabled'))document.querySelector('#privateChatEnabled').checked=data.config.privateChatEnabled!==false;document.querySelectorAll('input[name=agent]').forEach(input=>{input.checked=input.value===(data.config.agent||'codex')});document.querySelector('#robotName').value=data.config.robotName||'';document.querySelector('#clientId').value=data.config.clientId||'';document.querySelector('#showElapsed').checked=data.config.showElapsed!==false;document.querySelector('#showProcessingDetails').checked=data.config.showProcessingDetails===true;document.querySelector('#cardUpdateIntervalMs').value=(data.config.cardUpdateIntervalMs||3000)/1000;document.querySelector('#clientSecret').value='';document.querySelector('#webhookUrl').value='';document.querySelector('#personalHistoryMessageLimit').value=data.config.personalHistoryMessageLimit||10;document.querySelector('#personalHistoryPollIntervalSeconds').value=data.config.personalHistoryPollIntervalSeconds??15;document.querySelector('#personalHistoryLookbackMinutes').value=data.config.personalHistoryLookbackMinutes??10;document.querySelector('#robotSenderOpenDingTalkId').value=data.config.robotSenderOpenDingTalkId||'';document.querySelector('#groupPromptPrefix').value=data.config.groupPromptSuffix||'';setBotUsers(data.config.botAllowedUserIds||[],data.config.botAllowedUserNames||{},data.config.botSuperAdminUserIds||[]);Object.entries(keywordIds).forEach(([key,id])=>{document.querySelector('#'+id).value=(data.config.commandKeywords?.[key]||[]).join('|')})}const c=document.querySelector('#connection');if(c){c.textContent=data.status.eventConnected?'事件连接正常':'事件未连接';c.className='status '+(data.status.eventConnected?'connected':'stopped');}const list=document.querySelector('#replies');if(!list)return;if(!data.replies.length){list.className='empty';list.textContent='暂无回复';return}list.className='';const recentReplies=[...data.replies].sort((a,b)=>new Date(a.createdAt||0)-new Date(b.createdAt||0)).slice(-10);const replySignature=recentReplies.map(r=>r.id+':'+r.status+':'+r.content).join('|');if(list.dataset.signature===replySignature)return;const wasAtBottom=list.scrollHeight-list.scrollTop-list.clientHeight<24;list.dataset.signature=replySignature;list.innerHTML=recentReplies.map(r=>{const state=r.status==='processing'?'处理中':r.status==='completed'?'完成':'失败',replyStyle=r.status==='processing'?'#fffbeb;color:#92400e':r.status==='completed'?'#ecfdf3;color:#166534':'#fff1f2;color:#b42318',senderNames=[...new Set((r.senderNames||[]).filter(Boolean))].join('、');const conversationLabel=r.conversationType==='personal'?'个人':'群聊 · 群名:'+esc(r.conversationName||r.groupName||'-');return '<article class="reply"><div class="meta">'+conversationLabel+' · 发送人:'+esc(senderNames||'-')+' · '+fmt(r.createdAt)+' · '+esc(r.agent==='pi'?'Pi':'Codex')+' · '+state+' · '+r.messageCount+' 条消息</div><div style="margin-top:6px;padding:8px;border-radius:4px;background:#eff6ff;color:#174ea6"><pre style="margin:0;white-space:pre-wrap;word-break:break-word;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace">'+esc(r.question||'')+'</pre></div><div style="margin-top:6px;padding:8px;border-radius:4px;background:'+replyStyle+'"><pre style="margin:0;white-space:pre-wrap;word-break:break-word;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace">'+esc(r.content)+'</pre></div></article>'}).join('');if(wasAtBottom||recentReplies.length>0)list.scrollTop=list.scrollHeight}
419
+ function renderStatic(data,replaceConfig=false){rulesPanel.querySelector('.rules-toggle').textContent='钉钉群监控绑定 ('+data.config.targets.length+')';if(replaceConfig){if(document.querySelector('#privateChatEnabled'))document.querySelector('#privateChatEnabled').checked=data.config.privateChatEnabled!==false;document.querySelectorAll('input[name=agent]').forEach(input=>{input.checked=input.value===(data.config.agent||'codex')});document.querySelector('#robotName').value=data.config.robotName||'';document.querySelector('#clientId').value=data.config.clientId||'';document.querySelector('#showElapsed').checked=data.config.showElapsed!==false;document.querySelector('#showProcessingDetails').checked=data.config.showProcessingDetails===true;document.querySelector('#cardUpdateIntervalMs').value=(data.config.cardUpdateIntervalMs??3000)/1000;document.querySelector('#clientSecret').value='';document.querySelector('#webhookUrl').value='';document.querySelector('#personalHistoryMessageLimit').value=data.config.personalHistoryMessageLimit||10;document.querySelector('#personalHistoryPollIntervalSeconds').value=data.config.personalHistoryPollIntervalSeconds??15;document.querySelector('#personalHistoryLookbackMinutes').value=data.config.personalHistoryLookbackMinutes??10;document.querySelector('#robotSenderOpenDingTalkId').value=data.config.robotSenderOpenDingTalkId||'';document.querySelector('#groupPromptPrefix').value=data.config.groupPromptSuffix||'';setBotUsers(data.config.botAllowedUserIds||[],data.config.botAllowedUserNames||{},data.config.botSuperAdminUserIds||[]);Object.entries(keywordIds).forEach(([key,id])=>{document.querySelector('#'+id).value=(data.config.commandKeywords?.[key]||[]).join('|')})}const c=document.querySelector('#connection');if(c){c.textContent=data.status.eventConnected?'事件连接正常':'事件未连接';c.className='status '+(data.status.eventConnected?'connected':'stopped');}const list=document.querySelector('#replies');if(!list)return;if(!data.replies.length){list.className='empty';list.textContent='暂无回复';return}list.className='';const recentReplies=[...data.replies].sort((a,b)=>new Date(a.createdAt||0)-new Date(b.createdAt||0)).slice(-10);const replySignature=recentReplies.map(r=>r.id+':'+r.status+':'+r.content).join('|');if(list.dataset.signature===replySignature)return;const wasAtBottom=list.scrollHeight-list.scrollTop-list.clientHeight<24;list.dataset.signature=replySignature;list.innerHTML=recentReplies.map(r=>{const state=r.status==='processing'?'处理中':r.status==='completed'?'完成':'失败',replyStyle=r.status==='processing'?'#fffbeb;color:#92400e':r.status==='completed'?'#ecfdf3;color:#166534':'#fff1f2;color:#b42318',senderNames=[...new Set((r.senderNames||[]).filter(Boolean))].join('、');const conversationLabel=r.conversationType==='personal'?'个人':'群聊 · 群名:'+esc(r.conversationName||r.groupName||'-');return '<article class="reply"><div class="meta">'+conversationLabel+' · 发送人:'+esc(senderNames||'-')+' · '+fmt(r.createdAt)+' · '+esc(r.agent==='pi'?'Pi':'Codex')+' · '+state+' · '+r.messageCount+' 条消息</div><div style="margin-top:6px;padding:8px;border-radius:4px;background:#eff6ff;color:#174ea6"><pre style="margin:0;white-space:pre-wrap;word-break:break-word;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace">'+esc(r.question||'')+'</pre></div><div style="margin-top:6px;padding:8px;border-radius:4px;background:'+replyStyle+'"><pre style="margin:0;white-space:pre-wrap;word-break:break-word;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace">'+esc(r.content)+'</pre></div></article>'}).join('');if(wasAtBottom||recentReplies.length>0)list.scrollTop=list.scrollHeight}
408
420
  async function refresh(full=false){try{const r=await fetch('/api/state',{cache:'no-store'});if(!r.ok)throw new Error('读取状态失败');const data=await r.json();renderStatic(data,full);if(full){rules.replaceChildren();const grouped=new Map();data.config.targets.forEach(t=>{const key=t.groupId;if(!grouped.has(key))grouped.set(key,{groupId:t.groupId,groupName:t.groupName,targets:[]});grouped.get(key).targets.push(t)});grouped.forEach(addConfiguredRule);const formatInput=document.querySelector('input[name=format][value='+data.config.replyFormat+']');if(formatInput)formatInput.checked=true;loaded=true}}catch(e){notice.textContent=e.message;notice.className='notice error'}}
409
421
  let latestSystemLog='',systemLogOffset;const filterSystemLog=(content,level)=>{if(level==='all')return content;const lines=content.split('\n'),entries=[];let current=[];const isEntryStart=line=>/^\[\d{4}-\d{2}-\d{2}T[^\]]+\]\s+\[[^\]]+\]\s+\[(DEBUG|INFO|WARN|ERROR)\]/.test(line);lines.forEach(line=>{if(isEntryStart(line)){if(current.length)entries.push(current);current=[line]}else if(current.length)current.push(line)});if(current.length)entries.push(current);return entries.filter(entry=>entry[0].includes('['+level+']')).map(entry=>entry.join('\n')).join('\n')};const renderSystemLog=()=>{const log=document.querySelector('#systemLog'),level=document.querySelector('#monitorLogLevel').value,filtered=filterSystemLog(latestSystemLog,level),atBottom=log.scrollHeight-log.scrollTop-log.clientHeight<30;if(log.textContent!==filtered){log.textContent=filtered||'暂无匹配日志';if(document.querySelector('#monitorAutoScroll').checked&&(atBottom||!log.dataset.loaded))log.scrollTop=log.scrollHeight;log.dataset.loaded='1'}};document.querySelector('#monitorLogLevel').onchange=renderSystemLog;document.querySelector('#monitorAutoScroll').onchange=event=>{document.querySelector('#systemLogMeta').textContent=event.target.checked?'正在恢复实时拉取...':'实时拉取已暂停';if(event.target.checked)void loadSystemMonitor()};const loadSystemMonitor=async()=>{try{const statusResponse=await fetch('/api/system-status',{cache:'no-store'}),statusBody=await statusResponse.json();if(!statusResponse.ok)throw new Error(statusBody.error||'系统状态读取失败');const status=statusBody.status||{},processes=document.querySelector('#monitorProcesses'),meta=document.querySelector('#monitorMeta');meta.textContent='模式:'+(status.mode||'-')+' · 启动:'+fmt(status.startedAt)+' · 检查:'+fmt(status.checkedAt);processes.innerHTML=(status.processes||[]).map(item=>'<article class="process-card"><strong>'+esc(item.role)+'</strong><div class="process-state '+(item.running?'running':'')+'">'+(item.running?'运行中':'已停止')+'</div><code>PID '+esc(String(item.pid||'-'))+'</code>'+(['group-worker','bot'].includes(item.role)?'<div class="process-actions"><button class="button process-action" data-role="'+esc(item.role)+'" data-action="'+(item.running?'stop':'start')+'">'+(item.running?'停止':'启动')+'</button></div>':'')+'</article>').join('')||'<div class="empty">未发现系统进程</div>';processes.querySelectorAll('.process-action').forEach(button=>button.onclick=async()=>{const role=button.dataset.role,action=button.dataset.action;button.disabled=true;try{const r=await fetch('/api/system-process/'+encodeURIComponent(role)+'/'+action,{method:'POST'});if(!r.ok){const body=await r.json().catch(()=>({}));throw new Error(body.error||'进程操作失败')}setTimeout(loadSystemMonitor,800)}catch(e){alert(e instanceof Error?e.message:String(e))}finally{button.disabled=false}});if(!document.querySelector('#monitorAutoScroll').checked){document.querySelector('#systemLogMeta').textContent='实时拉取已暂停';return}const logUrl='/api/system-logs'+(systemLogOffset===undefined?'':'?offset='+encodeURIComponent(systemLogOffset)),logResponse=await fetch(logUrl,{cache:'no-store'}),logBody=await logResponse.json();if(!logResponse.ok)throw new Error(logBody.error||'系统日志读取失败');if(logBody.reset||systemLogOffset===undefined)latestSystemLog=logBody.content||'';else latestSystemLog+=logBody.content||'';systemLogOffset=Number(logBody.nextOffset||0);if(latestSystemLog.length>1000000){latestSystemLog=latestSystemLog.slice(-1000000).replace(/^[^\n]*\n/,'')}renderSystemLog();document.querySelector('#systemLogMeta').textContent=(logBody.path||'')+' · '+Number(logBody.size||0).toLocaleString()+' bytes · tail 实时增量'}catch(e){document.querySelector('#monitorMeta').textContent=e instanceof Error?e.message:String(e)}};document.querySelector('#restartSystem').onclick=async()=>{if(!confirm('确认重启整个 oh-my-im 系统吗?当前正在执行的任务会被中断。'))return;const button=document.querySelector('#restartSystem');button.disabled=true;button.textContent='正在重启...';try{const r=await fetch('/api/system-restart',{method:'POST'}),body=await r.json().catch(()=>({}));if(!r.ok)throw new Error(body.error||'重启失败');document.querySelector('#monitorMeta').textContent='重启指令已发送,等待服务恢复...';setTimeout(()=>location.reload(),4000)}catch(e){alert(e instanceof Error?e.message:String(e));button.disabled=false;button.textContent='重启系统'}};setInterval(()=>{if(document.querySelector('.page-monitor')?.classList.contains('active'))void loadSystemMonitor()},1000);void loadSystemMonitor();setInterval(()=>{void refresh(false)},1000);document.querySelector('#add').onclick=()=>addDraftRule();document.querySelector('#save').onclick=async()=>{const saveButton=document.querySelector('#save');try{const stateResponse=await fetch('/api/state',{cache:'no-store'}),state=await stateResponse.json();if(!stateResponse.ok||!state.config)throw new Error('读取当前配置失败');const targets=[...rules.children].flatMap(rule=>{if(rule._targets&&!rule._members)return rule._targets;const groupId=rule._group?.groupId||rule.querySelector('[data-key=groupId]')?.value,groupName=rule._group?.groupName||rule.querySelector('[data-key=groupName]')?.value,members=rule._members||[],selected=[...rule.querySelectorAll('[data-sender-id]:checked')];return selected.map(box=>{const m=members.find(x=>x.senderId===box.dataset.senderId);return {groupId,groupName,senderId:box.dataset.senderId,senderName:m?m.senderName:''}})}),robotName=document.querySelector('#robotName').value.trim(),clientId=document.querySelector('#clientId').value.trim(),showElapsed=document.querySelector('#showElapsed').checked,cardUpdateIntervalMs=Number(document.querySelector('#cardUpdateIntervalMs').value||'3'),clientSecret=document.querySelector('#clientSecret').value.trim(),personalHistoryMessageLimit=Number(document.querySelector('#personalHistoryMessageLimit').value||10),personalHistoryPollIntervalSeconds=Number(document.querySelector('#personalHistoryPollIntervalSeconds').value||0),personalHistoryLookbackMinutes=Number(document.querySelector('#personalHistoryLookbackMinutes').value||10),webhookUrl=document.querySelector('#webhookUrl').value.trim(),robotSenderOpenDingTalkId=document.querySelector('#robotSenderOpenDingTalkId').value.trim(),groupPromptSuffix=document.querySelector('#groupPromptPrefix').value.trim(),botAllowedUserIds=selectedBotUsers.map(user=>user.id),botAllowedUserNames=Object.fromEntries(selectedBotUsers.map(user=>[user.id,user.name])),botSuperAdminUserIds=[...selectedSuperAdminIds],botSuperAdminUserNames=Object.fromEntries(selectedBotUsers.filter(user=>selectedSuperAdminIds.includes(user.id)).map(user=>[user.id,user.name])),commandKeywords={pause:parseKeywords(keywordIds.pause),monitorOpen:parseKeywords(keywordIds.monitorOpen),monitorStop:parseKeywords(keywordIds.monitorStop),switchPi:parseKeywords(keywordIds.switchPi),switchCodex:parseKeywords(keywordIds.switchCodex)},agent=agentSelect().value,replyFormat=document.querySelector('input[name=format]:checked')?.value; if(!replyFormat)throw new Error('请选择回复格式');notice.textContent='保存中...';notice.className='notice';saveButton.disabled=true;saveButton.textContent='保存中...';const r=await fetch('/api/config',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({...state.config,targets,botAllowedUserIds,botAllowedUserNames,robotSenderOpenDingTalkId,groupPromptSuffix,cardUpdateIntervalMs:cardUpdateIntervalMs*1000,showElapsed,personalHistoryMessageLimit,personalHistoryPollIntervalSeconds,personalHistoryLookbackMinutes,webhookUrl,botSuperAdminUserIds,botSuperAdminUserNames,commandKeywords,replyFormat,robotName,clientId,clientSecret,agent})});const b=await r.json().catch(()=>({}));if(!r.ok||b.saved!==true)throw new Error(b.error||'保存失败:服务端未确认保存');notice.textContent='已保存,钉钉规则与机器人设置已立即生效。';notice.className='notice';await refresh(true)}catch(e){console.error('[Dashboard] save failed',e);notice.textContent=e instanceof Error?e.message:String(e);notice.className='notice error'}finally{saveButton.disabled=false;saveButton.textContent='保存并生效';setTimeout(()=>{if(!notice.classList.contains('error'))notice.textContent=''},3000)}};refresh(true);
410
422
  </script></body></html>`;
@@ -819,7 +819,7 @@ async function handleBatch(events, groupId, cardState, sessions, cardClient, get
819
819
  if (responseMode === "card" && storedCard?.status === "processing") {
820
820
  card = { groupId, cardBizId: storedCard.cardBizId };
821
821
  try {
822
- await cardClient.update(card, processingTitle(), `[等一等] 正在分析...`);
822
+ await cardClient.update(card, processingTitle(), `[OMG] 正在分析...`);
823
823
  }
824
824
  catch (err) {
825
825
  if (!isMissingCardError(err))
@@ -831,7 +831,7 @@ async function handleBatch(events, groupId, cardState, sessions, cardClient, get
831
831
  }
832
832
  if (responseMode === "card" && !card) {
833
833
  const cardBizId = randomUUID();
834
- card = await cardClient.create(groupId, cardBizId, processingTitle(), `[等一等] 正在分析...`);
834
+ card = await cardClient.create(groupId, cardBizId, processingTitle(), `[OMG] 正在分析...`);
835
835
  cardState.cards[groupId] = { cardBizId, status: "processing" };
836
836
  await saveCardState(cardState);
837
837
  }
@@ -849,7 +849,7 @@ async function handleBatch(events, groupId, cardState, sessions, cardClient, get
849
849
  question: batchQuestion(events),
850
850
  senderNames: batchSenderNames(events, getDashboardConfig()),
851
851
  senderDetails: batchSenderDetails(events, getDashboardConfig()),
852
- content: `[等一等] 正在分析...`,
852
+ content: `[OMG] 正在分析...`,
853
853
  agent,
854
854
  messageCount: events.length,
855
855
  };
@@ -934,7 +934,6 @@ async function handleBatch(events, groupId, cardState, sessions, cardClient, get
934
934
  }, 1_000);
935
935
  elapsedTimer.unref();
936
936
  }
937
- let lastText = "";
938
937
  let streamedText = "";
939
938
  let toolStatus = "";
940
939
  const result = await runAgent(agent, buildPrompt(events, dashboardConfig.groupPromptSuffix), sessionId, agentConfig, {
@@ -944,20 +943,18 @@ async function handleBatch(events, groupId, cardState, sessions, cardClient, get
944
943
  queue.steer = steer; },
945
944
  onToolUse: (toolName, stats) => {
946
945
  log.info(`codex tool=${toolName} count=${stats[toolName] ?? 1}`);
947
- toolStatus = `调用工具:${toolName} x${stats[toolName] ?? 1}`;
948
- liveReply.content = streamedText ? `${streamedText}\n\n${toolStatus}` : toolStatus;
946
+ toolStatus = `[OMG] 调用工具:${toolName} x${stats[toolName] ?? 1}`;
947
+ liveReply.content = streamedText ? `${streamedText}\n\n\n${toolStatus}` : toolStatus;
949
948
  latestVisibleContent = liveReply.content;
950
949
  updateCard(processingTitle(), liveReply.content);
951
950
  },
952
951
  onText: (text) => {
953
- const delta = text.startsWith(lastText) ? text.slice(lastText.length) : text;
954
- lastText = text;
955
- const visible = delta.trim();
952
+ streamedText = text;
953
+ const visible = streamedText.trim();
956
954
  if (visible) {
957
- streamedText = streamedText ? `${streamedText}\n\n${visible}` : visible;
958
955
  toolStatus = "";
959
956
  liveReply.content = streamedText;
960
- latestVisibleContent = text;
957
+ latestVisibleContent = streamedText;
961
958
  log.debug(`codex output: ${visible.slice(0, 2_000)}`);
962
959
  updateCard(processingTitle(), streamedText.slice(-8_000));
963
960
  }
@@ -1067,7 +1064,7 @@ async function enqueueGroupEvent(event, groupId, queues, cardState, sessions, ca
1067
1064
  if (queue.activeAgent === "pi" && queue.steer && content) {
1068
1065
  const steered = queue.steer(content);
1069
1066
  if (steered) {
1070
- void sendRobotText(groupId, getDashboardConfig(), "已将这条消息作为引导发送给当前 Pi 任务。")
1067
+ void sendRobotText(groupId, getDashboardConfig(), "[灵感]已将这条消息作为引导发送给当前 Pi 任务。")
1071
1068
  .catch((err) => log.warn(`steer acknowledgement failed: ${String(err)}`));
1072
1069
  log.info(`steered message=${event.message_id || "unknown"} group=${groupId}`);
1073
1070
  return;
@@ -1156,7 +1153,7 @@ function startGroupListener(seen, queues, cardState, sessions, cardClient, getDa
1156
1153
  // metadata is complete. Never feed our own steer acknowledgement back into
1157
1154
  // the active Pi task, even if robot detection missed that event.
1158
1155
  const eventContent = (event.content || event.text || "").trim();
1159
- if (queues.get(groupId)?.running && /^已将这条消息作为引导发送给当前 Pi 任务[。.!!]?$/u.test(eventContent)) {
1156
+ if (queues.get(groupId)?.running && /^\[灵感\]已将这条消息作为引导发送给当前 Pi 任务[。.!!]?$/u.test(eventContent)) {
1160
1157
  log.debug(`ignored self steer acknowledgement group=${groupId}`);
1161
1158
  return;
1162
1159
  }
@@ -1238,6 +1235,30 @@ function startGroupListener(seen, queues, cardState, sessions, cardClient, getDa
1238
1235
  log.info(`ignored mentioned group message group=${groupId} sender=${getSenderId(event)}`);
1239
1236
  return;
1240
1237
  }
1238
+ if (rawContent === "/new") {
1239
+ if (!acceptsTarget(event, config)) {
1240
+ log.debug(`ignored new session command from unmonitored sender=${getSenderId(event)}`);
1241
+ return;
1242
+ }
1243
+ const commandKey = `${groupId}:${getSenderId(event)}:new-session`;
1244
+ const now = Date.now();
1245
+ const previous = recentCommands.get(commandKey);
1246
+ if (previous && now - previous < commandDeduplicationMs)
1247
+ return;
1248
+ recentCommands.set(commandKey, now);
1249
+ void monitorCommandChain.then(async () => {
1250
+ if (queues.get(groupId)?.running) {
1251
+ await sendRobotText(groupId, getDashboardConfig(), "当前 Agent 任务正在运行,请先暂停后再新建会话。");
1252
+ return;
1253
+ }
1254
+ sessions.delete(`pi:${groupId}`);
1255
+ sessions.delete(`codex:${groupId}`);
1256
+ await saveGroupSessions(sessions);
1257
+ await sendRobotText(groupId, getDashboardConfig(), "已清空当前群会话的 Agent session,下一条消息将使用新会话处理。");
1258
+ log.info(`group sessions cleared group=${groupId}`);
1259
+ }).catch((err) => log.error(`new session command failed group=${groupId}: ${String(err)}`));
1260
+ return;
1261
+ }
1241
1262
  runtime.lastEventAt = new Date().toISOString();
1242
1263
  const command = parseMonitorCommand({ senderId: getSenderId(event), content: rawContent }, config.commandKeywords);
1243
1264
  log.info(`group event classified group=${groupId} sender=${getSenderId(event)} command=${typeof command === "object" ? `${command.type}:${command.agent}` : command || "message"} configuredTargets=${config.targets.filter((target) => target.groupId === groupId).length}`);
package/dist/omi.js CHANGED
@@ -42,21 +42,21 @@ async function checkForUpdate() {
42
42
  try {
43
43
  const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, { signal: AbortSignal.timeout(3_000) });
44
44
  if (!response.ok)
45
- return;
45
+ return true;
46
46
  const latest = (await response.json()).version?.trim();
47
47
  if (!latest || compareVersions(latest, current) <= 0)
48
- return;
48
+ return true;
49
49
  console.log(`发现 ${packageName} 新版本:v${latest}(当前 v${current})`);
50
50
  console.log(`GitHub:${repositoryUrl}`);
51
51
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
52
52
  console.log(`如需升级,请执行:npm install -g ${packageName}@latest`);
53
- return;
53
+ return true;
54
54
  }
55
55
  const readline = createInterface({ input: process.stdin, output: process.stdout });
56
56
  try {
57
57
  const answer = (await readline.question("是否立即升级?[y/N] ")).trim().toLowerCase();
58
58
  if (answer !== "y" && answer !== "yes")
59
- return;
59
+ return true;
60
60
  }
61
61
  finally {
62
62
  readline.close();
@@ -65,12 +65,14 @@ async function checkForUpdate() {
65
65
  const result = spawnSync("npm", ["install", "-g", `${packageName}@latest`], { stdio: "inherit" });
66
66
  if (result.status !== 0) {
67
67
  console.error("升级失败,请稍后手动执行:", `npm install -g ${packageName}@latest`);
68
- return;
68
+ return true;
69
69
  }
70
70
  console.log(`升级完成,请重新执行:${process.argv.slice(2).join(" ") || "omi"}`);
71
+ return false;
71
72
  }
72
73
  catch {
73
74
  // Version checking must never prevent the already installed version from starting.
75
+ return true;
74
76
  }
75
77
  }
76
78
  function printHelp() {
@@ -282,8 +284,8 @@ if (args.includes("-h") || args.includes("--help") || command === "help") {
282
284
  printHelp();
283
285
  }
284
286
  else if (command === "start" || command === "listen") {
285
- await checkForUpdate();
286
- start(noListen ? "bot" : "listen");
287
+ if (await checkForUpdate())
288
+ start(noListen ? "bot" : "listen");
287
289
  }
288
290
  else if (command === "stop") {
289
291
  await stop();
@@ -292,18 +294,20 @@ else if (command === "status") {
292
294
  status();
293
295
  }
294
296
  else if (command === "restart") {
295
- await checkForUpdate();
296
- const current = readState();
297
- const mode = current?.mode ?? "listen";
298
- await stop();
299
- start(mode, current?.workspace || launchWorkspace);
297
+ if (await checkForUpdate()) {
298
+ const current = readState();
299
+ const mode = current?.mode ?? "listen";
300
+ await stop();
301
+ start(mode, current?.workspace || launchWorkspace);
302
+ }
300
303
  }
301
304
  else if (command === "update") {
302
- await checkForUpdate();
303
- const current = readState();
304
- const mode = current?.mode ?? "listen";
305
- await stop();
306
- start(mode, current?.workspace || launchWorkspace);
305
+ if (await checkForUpdate()) {
306
+ const current = readState();
307
+ const mode = current?.mode ?? "listen";
308
+ await stop();
309
+ start(mode, current?.workspace || launchWorkspace);
310
+ }
307
311
  }
308
312
  else {
309
313
  console.error(`Unknown command: ${command}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-im",
3
- "version": "0.1.13",
3
+ "version": "0.1.16",
4
4
  "author": {
5
5
  "name": "杜振训",
6
6
  "email": "duzhenxun@126.com"