myagent-ai 1.13.4 → 1.13.6

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.
@@ -43,7 +43,7 @@ class MainAgent(BaseAgent):
43
43
  <output>
44
44
  <response>直接回复用户的内容。这是一段友好、自然的话语,用于向用户说明你正在做什么,或者回应用户的问题/问候。要求简洁、有礼貌、符合对话场景。如果用户只是问候,简单回应即可;如果用户有具体任务,要说明你的计划。</response>
45
45
  <usersays_correct>根据用户输入的"usersays"内容,结合上下文优化为新的用户输入,如果"usersays"为空,这里输出为空。</usersays_correct>
46
- <task_plan>如"context"包含非空"task_plan",则更新它,变为当前输出。否则,根据"context", 以MD 的格式,制定新任务列表。</task_plan>
46
+ <task_plan>任务计划(仅复杂任务使用):如"context"包含非空"task_plan",则更新它。否则,先评估任务复杂度——如果预计操作步骤不超过5步(如:单次查询、简单问答、格式转换、单文件修改、简单计算等简单任务),则<task_plan>输出为空,不要创建任务列表;只有当任务较复杂(预计超过5步操作,如:多文件修改、需要调研+实现+测试、涉及多个模块联动等),才以Markdown列表格式制定新任务列表。格式:每项用 "- [ ] 任务描述" "- [x] 已完成任务",含完成状态标记。</task_plan>
47
47
 
48
48
  <toolstocal>
49
49
  <tool><beforecalltext>连接词,介绍调用什么工具,达到什么目的。</beforecalltext><toolname>工具名</toolname><parms>JSON格式的参数对象,例如: {"query": "搜索关键词", "num": 5}</parms><timeout>预估超时时限(秒)</timeout><callback>true/false,要求解析器在该工具执行完后是否要回调llm大模型,将所有工具输出结果+新构造的"context"输入给llm</callback></tool>
@@ -63,7 +63,7 @@ class MainAgent(BaseAgent):
63
63
  1. 你必须且只能输出 <output> XML 结构,不要输出任何其他文本
64
64
  2. <response>: 必须输出一段直接回复用户的话语(这是用户实际看到的回复),要求简洁友好、自然流畅。不要只输出任务计划而不说话!
65
65
  3. <usersays_correct>: 如果 context 中 usersays 非空,则根据对话语境优化为更准确的用户意图表达
66
- 4. <task_plan>: 使用 Markdown 列表格式,每项包含任务描述和完成状态标记 [x]/[ ]
66
+ 4. <task_plan>: 仅用于复杂任务(预计超过5步操作)。简单任务(≤5步)输出为空。复杂任务使用 Markdown 列表格式,每项包含任务描述和完成状态标记 [x]/[ ]
67
67
  5. <toolstocal>: 列出所有需要执行的工具调用,每个工具包含完整的参数说明
68
68
  6. <parms>: **必须使用严格合法的JSON格式**,例如 {"query": "关键词", "num": 10},不要使用其他格式
69
69
  7. <timeout>: 预估超时秒数(简单操作10-30s,文件操作30-60s,网络请求60-120s,数据处理120-300s)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.13.4",
3
+ "version": "1.13.6",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/web/api_server.py CHANGED
@@ -3336,8 +3336,9 @@ class ApiServer:
3336
3336
  _v2_latest_task_plan = plan_text
3337
3337
  # 解析 Markdown 任务列表为 [{text, status}] 格式
3338
3338
  parsed_tasks = self._parse_v2_task_plan(plan_text)
3339
- if parsed_tasks:
3340
- # 合并与存储
3339
+ if parsed_tasks and len(parsed_tasks) >= 3:
3340
+ # 仅当任务项 >= 3 时才同步到 store 并推送前端
3341
+ # (小任务 <3 项视为简单任务,不创建任务列表)
3341
3342
  merged = self._merge_task_list(session_id, parsed_tasks)
3342
3343
  self._task_list_store[session_id] = merged
3343
3344
  # 额外发送 task_list_update 事件(前端侧边栏 task panel 依赖此事件)
@@ -3345,6 +3346,14 @@ class ApiServer:
3345
3346
  await _write_sse({"type": "task_list_update", "tasks": merged})
3346
3347
  except Exception:
3347
3348
  pass
3349
+ elif not parsed_tasks or len(parsed_tasks) == 0:
3350
+ # LLM 输出了空的 task_plan,清空任务列表
3351
+ if session_id in self._task_list_store:
3352
+ self._task_list_store[session_id] = []
3353
+ try:
3354
+ await _write_sse({"type": "task_list_update", "tasks": []})
3355
+ except Exception:
3356
+ pass
3348
3357
 
3349
3358
  # 转发所有事件到前端
3350
3359
  await _write_sse(event)
@@ -2419,14 +2419,14 @@ function _renderMessagesInner() {
2419
2419
  partsInner += '<div class="timeline-segment">' + renderMarkdown(msg._streamingText) + _cursor + '</div>';
2420
2420
  }
2421
2421
  if (partsInner) {
2422
- // All parts (text segments + tool calls) wrapped in ONE message-bubble
2423
- timelineHtml = '<div class="message-bubble"><div class="msg-timeline">' + partsInner + '</div></div>';
2422
+ // All parts (text segments + tool calls) wrapped in ONE message-bubble (full width during streaming)
2423
+ timelineHtml = '<div class="message-bubble msg-bubble-wrapper"><div class="msg-timeline">' + partsInner + '</div></div>';
2424
2424
  }
2425
2425
  }
2426
2426
 
2427
- // Backward compat: single bubble for messages without parts
2427
+ // Backward compat: single bubble for messages without parts (full width)
2428
2428
  const singleBubbleHtml = (!hasParts && !hasStreamingText)
2429
- ? (content ? `<div class="message-bubble">${content}${ttsIndicator}</div>` : '')
2429
+ ? (content ? `<div class="message-bubble msg-bubble-wrapper">${content}${ttsIndicator}</div>` : '')
2430
2430
  : '';
2431
2431
 
2432
2432
  // Exec events panel: only for backward compat (messages without parts loaded from DB)
@@ -3459,6 +3459,9 @@ const ttsManager = {
3459
3459
  _audioPlaying: false, // 队列是否正在播放
3460
3460
  _stopRequested: false, // 是否已请求停止
3461
3461
  _streamMsgIndex: -1, // 流式模式对应的消息索引
3462
+ _pendingFetches: 0, // 正在进行的 TTS 请求计数(防竞态)
3463
+ _flushed: false, // streamFlush 已调用(防竞态停止)
3464
+ _firstChunkSent: false, // 是否已发送第一个音频块(降低首句延迟)
3462
3465
 
3463
3466
  init() {
3464
3467
  // Load TTS enabled state from localStorage
@@ -3545,6 +3548,9 @@ const ttsManager = {
3545
3548
  this._audioQueue = [];
3546
3549
  this._audioPlaying = false;
3547
3550
  this._stopRequested = false;
3551
+ this._pendingFetches = 0;
3552
+ this._flushed = false;
3553
+ this._firstChunkSent = false;
3548
3554
  // 保持 _streamActive = true 和 isPlaying = true
3549
3555
  // 这样后续的 text_delta 会继续往新的缓冲区添加文本
3550
3556
  // 新的句子会被合成并入队播放
@@ -3574,6 +3580,9 @@ const ttsManager = {
3574
3580
  this._streamMsgIndex = msgIndex;
3575
3581
  this.currentMsgIndex = msgIndex;
3576
3582
  this.isPlaying = true;
3583
+ this._pendingFetches = 0;
3584
+ this._flushed = false;
3585
+ this._firstChunkSent = false;
3577
3586
  },
3578
3587
 
3579
3588
  /**
@@ -3588,6 +3597,27 @@ const ttsManager = {
3588
3597
 
3589
3598
  this._streamBuffer += delta;
3590
3599
 
3600
+ // 首句快速通道:第一个音频块降低延迟,缓冲区达到 25 字即触发
3601
+ if (!this._firstChunkSent && this._streamBuffer.length >= 25) {
3602
+ var lastSep = -1;
3603
+ for (var k = 0; k < this._streamBuffer.length; k++) {
3604
+ var c = this._streamBuffer[k];
3605
+ if (c === ',' || c === ',' || c === '。' || c === '!' || c === '?' || c === ';' || c === ' ' || c === '\n') {
3606
+ lastSep = k;
3607
+ }
3608
+ }
3609
+ if (lastSep > 0) {
3610
+ var firstChunk = this._streamBuffer.substring(0, lastSep + 1).trim();
3611
+ this._streamBuffer = this._streamBuffer.substring(lastSep + 1);
3612
+ var cleanFirst = this._cleanForStreamTTS(firstChunk);
3613
+ if (cleanFirst) {
3614
+ this._firstChunkSent = true;
3615
+ this._enqueueTTS(cleanFirst);
3616
+ }
3617
+ }
3618
+ return;
3619
+ }
3620
+
3591
3621
  // 检测句子边界:中文句号/感叹号/问号,英文句号+空格,或换行
3592
3622
  var boundaryPattern = /[。!?]|\.(?:\s|$)|\n/;
3593
3623
  var boundaryIdx = -1;
@@ -3643,8 +3673,15 @@ const ttsManager = {
3643
3673
  this._enqueueTTS(cleanText);
3644
3674
  }
3645
3675
  }
3646
- // 标记流式阶段结束(队列播完后自动清理状态)
3647
- this._streamActive = false;
3676
+ // 标记已刷新,但不立即停止 —— 等队列和 pending fetch 全部完成后再停止
3677
+ this._flushed = true;
3678
+ // 如果没有 pending fetch 且队列为空,立即停止
3679
+ if (this._pendingFetches === 0 && this._audioQueue.length === 0 && !this._audioPlaying) {
3680
+ this.isPlaying = false;
3681
+ this._streamActive = false;
3682
+ this.currentMsgIndex = -1;
3683
+ this.updatePlayingIndicator();
3684
+ }
3648
3685
  },
3649
3686
 
3650
3687
  /**
@@ -3683,6 +3720,7 @@ const ttsManager = {
3683
3720
  _enqueueTTS(text) {
3684
3721
  if (this._stopRequested) return;
3685
3722
  var self = this;
3723
+ self._pendingFetches++;
3686
3724
 
3687
3725
  (async function() {
3688
3726
  try {
@@ -3719,6 +3757,15 @@ const ttsManager = {
3719
3757
  }
3720
3758
  } catch (e) {
3721
3759
  console.error('TTS stream chunk error:', e);
3760
+ } finally {
3761
+ self._pendingFetches--;
3762
+ // 如果已 flush 且没有更多 pending,检查是否该停止
3763
+ if (self._flushed && self._pendingFetches === 0 && self._audioQueue.length === 0 && !self._audioPlaying) {
3764
+ self.isPlaying = false;
3765
+ self._streamActive = false;
3766
+ self.currentMsgIndex = -1;
3767
+ self.updatePlayingIndicator();
3768
+ }
3722
3769
  }
3723
3770
  })();
3724
3771
  },
@@ -3736,15 +3783,16 @@ const ttsManager = {
3736
3783
  }
3737
3784
 
3738
3785
  if (this._audioQueue.length === 0) {
3739
- // 队列空了,检查流式是否已结束
3740
- if (!this._streamActive) {
3741
- // 流结束且队列为空 → 播放完成
3786
+ // 队列空了,检查流式是否已结束 AND 没有 pending fetches
3787
+ if (this._flushed && this._pendingFetches === 0) {
3788
+ // 流已结束、无 pending 请求、队列空 → 播放完成
3742
3789
  this.isPlaying = false;
3790
+ this._streamActive = false;
3743
3791
  this._audioPlaying = false;
3744
3792
  this.currentMsgIndex = -1;
3745
3793
  this.updatePlayingIndicator();
3746
3794
  }
3747
- // 如果流还在继续,等待新的音频入队
3795
+ // 如果流还在继续或还有 pending fetches,等待新的音频入队
3748
3796
  return;
3749
3797
  }
3750
3798
 
@@ -472,59 +472,7 @@ function updateStreamingMessage(msgIdx) {
472
472
  }
473
473
  }
474
474
 
475
- // V2 Reasoning block (from v2_reasoning events)
476
- if (msg._v2Reasoning) {
477
- let v2ReasoningBlock = null;
478
- const allThoughts2 = contentArea.querySelectorAll('.thought-block');
479
- for (const tb of allThoughts2) {
480
- const label = tb.querySelector('.thought-label');
481
- if (label && label.textContent.includes('V2 推理')) {
482
- v2ReasoningBlock = tb;
483
- break;
484
- }
485
- }
486
- if (!v2ReasoningBlock && msg.thought) {
487
- // Skip if V1 thought already exists - don't duplicate
488
- } else {
489
- const v2Len = msg._v2Reasoning.length;
490
- const v2WordCount = msg.streaming
491
- ? '<span class="thought-word-count">' + v2Len + ' 字</span>'
492
- : '';
493
- if (v2ReasoningBlock) {
494
- // Incremental update for V2 reasoning block
495
- const label = v2ReasoningBlock.querySelector('.thought-label');
496
- if (label) label.innerHTML = 'V2 推理过程' + v2WordCount;
497
- const badge = v2ReasoningBlock.querySelector('.thought-badge');
498
- if (badge) badge.textContent = msg.streaming ? '推理中...' : '已完成';
499
- const thoughtContent = v2ReasoningBlock.querySelector('.thought-content');
500
- if (thoughtContent && msg.streaming) {
501
- const prevLen = v2ReasoningBlock._lastV2Len || 0;
502
- if (msg._v2Reasoning.length > prevLen) {
503
- const newText = msg._v2Reasoning.substring(prevLen);
504
- thoughtContent.insertAdjacentHTML('beforeend', renderMarkdown(newText));
505
- v2ReasoningBlock._lastV2Len = msg._v2Reasoning.length;
506
- // 自动滚动 V2 推理框内部内容到底部
507
- thoughtContent.scrollTop = thoughtContent.scrollHeight;
508
- }
509
- } else if (thoughtContent && !msg.streaming) {
510
- thoughtContent.innerHTML = renderMarkdown(msg._v2Reasoning);
511
- v2ReasoningBlock._lastV2Len = msg._v2Reasoning.length;
512
- }
513
- } else if (!msg.thought) {
514
- const v2Html = `<details class="thought-block ${msg.streaming ? 'streaming' : ''}" ${msg.streaming ? 'open' : ''}>
515
- <summary>
516
- <span class="thought-icon">🧠</span>
517
- <span class="thought-label">V2 推理过程${v2WordCount}</span>
518
- ${msg.streaming ? '<span class="thought-badge">推理中...</span>' : '<span class="thought-badge">已完成</span>'}
519
- </summary>
520
- <div class="thought-content">${renderMarkdown(msg._v2Reasoning)}</div>
521
- </details>`;
522
- contentArea.insertAdjacentHTML('afterbegin', v2Html);
523
- const newBlock = contentArea.querySelector(':scope > .thought-block');
524
- if (newBlock) newBlock._lastV2Len = msg._v2Reasoning.length;
525
- }
526
- }
527
- }
475
+ // V2 Reasoning: content is already rendered in msg.parts timeline, skip separate block to avoid duplication
528
476
 
529
477
  // Update content - timeline (interleaved text + exec events) or single bubble (backward compat)
530
478
  const hasParts = Array.isArray(msg.parts);
@@ -1430,15 +1378,20 @@ async function sendMessage() {
1430
1378
  if (evt.tasks && Array.isArray(evt.tasks) && !_finishReceived) {
1431
1379
  state.taskItems = evt.tasks;
1432
1380
  renderTaskList();
1433
- // 显示任务面板头部(不自动展开内容区)
1434
1381
  var panel = document.getElementById('taskPanel');
1435
- if (panel && state.taskItems.length > 0) {
1436
- panel.classList.remove('hidden');
1437
- // Use change detection + auto-fade (but don't force expand)
1438
- if (typeof hasTaskListChanged === 'function' && typeof triggerTaskAutoFade === 'function') {
1439
- if (hasTaskListChanged()) {
1440
- triggerTaskAutoFade();
1382
+ if (panel) {
1383
+ if (state.taskItems.length > 0) {
1384
+ // 有任务时才显示面板
1385
+ panel.classList.remove('hidden');
1386
+ // Use change detection + auto-fade (but don't force expand)
1387
+ if (typeof hasTaskListChanged === 'function' && typeof triggerTaskAutoFade === 'function') {
1388
+ if (hasTaskListChanged()) {
1389
+ triggerTaskAutoFade();
1390
+ }
1441
1391
  }
1392
+ } else {
1393
+ // 空任务列表时隐藏面板
1394
+ panel.classList.add('hidden');
1442
1395
  }
1443
1396
  }
1444
1397
  }
@@ -1457,11 +1410,14 @@ async function sendMessage() {
1457
1410
  if (evt.data && evt.data.task_plan) {
1458
1411
  state.messages[msgIdx]._v2TaskPlan = evt.data.task_plan;
1459
1412
  }
1460
- // ── finish 标签:任务完成后清空任务面板 ──
1413
+ // ── finish 标签:任务完成后清空任务面板并隐藏 ──
1461
1414
  if (evt.data && evt.data.finish) {
1462
1415
  _finishReceived = true;
1463
1416
  state.taskItems = [];
1464
1417
  if (typeof renderTaskList === 'function') renderTaskList();
1418
+ // 隐藏任务面板
1419
+ var finishPanel = document.getElementById('taskPanel');
1420
+ if (finishPanel) finishPanel.classList.add('hidden');
1465
1421
  }
1466
1422
  throttledStreamUpdate(msgIdx);
1467
1423
  } else if (evt.type === 'v2_tool_start') {