codebee 0.1.44 → 0.1.46

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/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
6
6
 
7
7
  ## 未发布
8
8
 
9
+ ## v0.1.46(2026-09-22)
10
+
11
+ ### ✨ 新功能
12
+
13
+ - 对话时间线支持复制与编辑重发:自己发的消息悬停出现「复制 / 编辑」,「编辑」把原文载入输入框,改完再发;智能体的回答悬停可「复制」(自动剥掉协议尾,复制到的就是看到的内容)。
14
+
15
+ ## v0.1.45(2026-09-22)
16
+
17
+ ### 🛠 问题修复
18
+
19
+ - 智能体读取超大文件(超 64KB 的日志/长文)不再只给开头:改为保留开头 44KB 与结尾 16KB、中段标注省略——报错和结论常在文件末尾,此前最关键的信息会被截掉;结尾处的中文也不会因字节截断而乱码。
20
+
9
21
  ## v0.1.44(2026-09-22)
10
22
 
11
23
  ### ✨ 新功能
package/README.md CHANGED
@@ -23,6 +23,16 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
23
23
  <!-- relnotes:start -->
24
24
  ### ✨ 新功能 · New
25
25
 
26
+ - 对话时间线支持复制与编辑重发:自己发的消息悬停出现「复制 / 编辑」,编辑载入原文改完再发;智能体回答悬停可复制(自动剥协议尾)。
27
+ - Chat timeline messages now support copy & edit-to-resend: hover your own message for Copy / Edit (edit loads the original into the input box); agent replies offer Copy with protocol tails stripped.
28
+
29
+ ### 🛠 问题修复 · Fixes
30
+
31
+ - 智能体读超大文件不再只给开头:保留开头 44KB+结尾 16KB、中段标注省略——文件末尾的报错与结论不再被截掉,结尾中文不乱码。
32
+ - Reading oversized files no longer keeps only the head: 44KB head + 16KB tail with the middle elided — end-of-file errors and conclusions survive, and trailing Chinese text stays intact.
33
+
34
+ ### ✨ 新功能 · New
35
+
26
36
  - 经验库新增「有效做法」记忆:高分一次通过的任务会把「做对了什么」提炼成可复用步骤,下次同类任务自动注入照做;绿色「做法」徽章一眼区分。
27
37
  - The lesson library now also captures "what worked": high-scoring first-pass runs distill reusable procedures, auto-injected into similar future tasks — marked with a green "Best practice" badge.
28
38
 
@@ -23,6 +23,8 @@ from . import modelhub, runner
23
23
 
24
24
  MAX_TOOL_ITERS = 16 # 单步工具循环上限(防模型打转)
25
25
  READ_MAX_BYTES = 64 * 1024 # read_file 单次读取上限
26
+ READ_HEAD_BYTES = 44 * 1024 # TokenJuice 借鉴(openhuman):超限文件头尾保留、
27
+ READ_TAIL_BYTES = 16 * 1024 # 中段省略——日志/代码的报错常在尾部,纯截头会丢关键信息
26
28
  LIST_MAX_ENTRIES = 200
27
29
  MAX_IMAGES = 8 # 单次最多随消息传几张图(请求体防爆)
28
30
  IMAGE_MAX_EDGE = 1568 # 长边上限(视觉模型通行建议值,超出等比缩)
@@ -268,7 +270,32 @@ def _tool_read_file(workdir, args):
268
270
  return "(文件不存在: %s)" % rel
269
271
  data = p.read_bytes()[:READ_MAX_BYTES + 1]
270
272
  truncated = len(data) > READ_MAX_BYTES
271
- head = data[:READ_MAX_BYTES]
273
+ if truncated:
274
+ # 头尾保留、中段省略(TokenJuice 借鉴):头 44k 给结构/开头上下文,
275
+ # 尾 16k 给报错/结论(日志与代码的关键信息常在末尾),中段标注省略量
276
+ try:
277
+ size = p.stat().st_size
278
+ except OSError:
279
+ size = len(data)
280
+ with open(p, "rb") as fh:
281
+ head = fh.read(READ_HEAD_BYTES)
282
+ fh.seek(max(0, size - READ_TAIL_BYTES))
283
+ tail = fh.read(READ_TAIL_BYTES)
284
+ head = _trim_partial_utf8(head)
285
+ # 尾段开头的残缺多字节序列剥头(对偶于 _trim_partial_utf8 剥尾)
286
+ while tail:
287
+ try:
288
+ tail.decode("utf-8")
289
+ break
290
+ except UnicodeDecodeError:
291
+ tail = tail[1:]
292
+ text = runner.decode_output(head) \
293
+ + "\n…(中段省略 %d 字节;头 %dKB + 尾 %dKB 保留)\n" % (
294
+ max(0, size - READ_HEAD_BYTES - READ_TAIL_BYTES),
295
+ READ_HEAD_BYTES // 1024, READ_TAIL_BYTES // 1024) \
296
+ + runner.decode_output(tail)
297
+ return "…(文件超 64KB,已头尾保留读取)\n" + text
298
+ head = data
272
299
  if not head:
273
300
  return "(空文件)"
274
301
  if _looks_binary(head):
@@ -284,10 +311,8 @@ def _tool_read_file(workdir, args):
284
311
  return ("(二进制文件,无法按文本读取: %s — %s,%d B。read_file 只支持文本文件;"
285
312
  "请如实告知用户该文件内容无法以文本方式查看,不要反复重读。)"
286
313
  % (rel, fmt, size))
287
- if truncated:
288
- head = _trim_partial_utf8(head)
289
314
  text = runner.decode_output(head)
290
- return ("…(超过 64KB 已截断)\n" if truncated else "") + text
315
+ return text
291
316
 
292
317
 
293
318
  def _tool_write_file(workdir, args):
@@ -307,7 +332,7 @@ def _tool_write_file(workdir, args):
307
332
  TOOLS_SPEC = [
308
333
  {"name": "list_files", "description": "列出工作目录(或其子目录)下的文件",
309
334
  "args": {"path": "子目录相对路径,留空=根目录"}},
310
- {"name": "read_file", "description": "读取工作目录内一个文本文件(UTF-8/GBK 自动识别,超 64KB 截断;图片等二进制文件无法读取)",
335
+ {"name": "read_file", "description": "读取工作目录内一个文本文件(UTF-8/GBK 自动识别,超 64KB 时保留开头与结尾、中段省略;图片等二进制文件无法读取)",
311
336
  "args": {"path": "文件相对路径"}},
312
337
  {"name": "write_file", "description": "把文本内容写入工作目录内一个文件(UTF-8,父目录自动创建)",
313
338
  "args": {"path": "文件相对路径", "content": "完整文本内容"}},
package/app/ui/app.js CHANGED
@@ -6530,6 +6530,17 @@ function bindDirector() {
6530
6530
  * 进程,运行中递的话只在下一步生效。 */
6531
6531
  let chatRunId = null;
6532
6532
  let chatAtts = [];
6533
+ let chatTimelineItems = []; // renderChat 每次重建时间线时刷新;复制/编辑重发按索引回查
6534
+
6535
+ /* 聊天正文协议清洗:剥 DIRECT_DONE/<followups> 尾巴与「本轮做了什么」复读头。
6536
+ * 渲染与「复制」共用同一口径——复制出来的就是看到的内容。 */
6537
+ function chatCleanText(raw) {
6538
+ return String(raw || "")
6539
+ .replace(/^\s*本轮做了什么\s*[::][^\n]*\n+(回复\s*[::]\s*\n?)?/, "")
6540
+ .replace(/\n*DIRECT_DONE:.*\s*$/s, "")
6541
+ .replace(/\n*<followups>[\s\S]*?<\/followups>\s*$/s, "")
6542
+ .trim();
6543
+ }
6533
6544
  let chatSig = "";
6534
6545
 
6535
6546
  function chatEngineIsDirect(run) {
@@ -6608,7 +6619,8 @@ async function renderChat(run, active) {
6608
6619
  const attName = (a) => { const p = typeof a === "string" ? a
6609
6620
  : ((a && (a.name || a.path)) || "");
6610
6621
  return String(p).split(/[\\/]/).pop() || ""; };
6611
- let html = items.map((it) => {
6622
+ chatTimelineItems = items; // 复制/编辑重发按索引回查(见 rd-chat-flow 委托)
6623
+ let html = items.map((it, idx) => {
6612
6624
  if (it.kind === "user") {
6613
6625
  // 用户消息:右侧浅灰圆角块,元信息在块内顶部。
6614
6626
  // 附件胶囊可点开预览:data-att 带 _attachments/ 相对路径(老数据裸文件名
@@ -6625,10 +6637,15 @@ async function renderChat(run, active) {
6625
6637
  ? "/api/runs/" + encodeURIComponent(chatRunId) + "/file?name=" + encodeURIComponent(rel) : "";
6626
6638
  return { nm, rel, img: src };
6627
6639
  }).filter(Boolean);
6640
+ // 悬停动作:复制原文 / 编辑重发(载入输入框改完再发,2026-09-22 用户诉求)
6641
+ const acts = '<span class="chat-acts2">' +
6642
+ '<button type="button" class="chat-act" data-cact="copy" data-ci="' + idx + '">' + esc(t("复制")) + "</button>" +
6643
+ '<button type="button" class="chat-act" data-cact="edit" data-ci="' + idx + '">' + esc(t("编辑")) + "</button>" +
6644
+ "</span>";
6628
6645
  return '<div class="chat-row me">' +
6629
6646
  '<div class="chat-bubble me">' +
6630
6647
  '<div class="chat-meta">' + esc(it.who || "") + " " + esc(chatTime(it.at)) +
6631
- (it.consumed ? "" : " · " + t("待送达")) + "</div>" +
6648
+ (it.consumed ? "" : " · " + t("待送达")) + acts + "</div>" +
6632
6649
  esc(it.text || t("(仅附件)")) +
6633
6650
  (atts.length
6634
6651
  ? '<div class="chat-atts">' + atts.map((a) =>
@@ -6644,11 +6661,7 @@ async function renderChat(run, active) {
6644
6661
  // <followups> 块后端已剥并转成结构化字段,这里再兜一次底防老数据/漏剥;
6645
6662
  // 开场「本轮做了什么:…」+「回复:」是模型复读协议措辞的元描述(提示词已禁,
6646
6663
  // 这里兜历史数据),只剥消息开头的第一行
6647
- const body = String(it.text || "")
6648
- .replace(/^\s*本轮做了什么\s*[::][^\n]*\n+(回复\s*[::]\s*\n?)?/, "")
6649
- .replace(/\n*DIRECT_DONE:.*\s*$/s, "")
6650
- .replace(/\n*<followups>[\s\S]*?<\/followups>\s*$/s, "")
6651
- .trim();
6664
+ const body = chatCleanText(it.text);
6652
6665
  // 运行中还没有正文:三点打字动画(终态无正文才落「无文本输出」占位)
6653
6666
  const bodyHtml = body ? chatBodyHTML(body)
6654
6667
  : (it.status === "running"
@@ -6656,13 +6669,17 @@ async function renderChat(run, active) {
6656
6669
  : esc(t("(本轮无文本输出)")));
6657
6670
  // 元信息只留 名字+时间(+失败标记):路由/供应商细节属于「步骤」页签,塞这里就是噪音
6658
6671
  const metaBad = it.status === "failed" || it.status === "cancelled";
6672
+ // 智能体回复:头像 + 整幅正文(不套气泡框),元信息小字落在正文下方;
6673
+ // 悬停可复制回答原文(协议清洗后)
6674
+ const actsA = body ? '<span class="chat-acts2">' +
6675
+ '<button type="button" class="chat-act" data-cact="copy" data-ci="' + idx + '">' + esc(t("复制")) + "</button></span>" : "";
6659
6676
  // 智能体回复:头像 + 整幅正文(不套气泡框),元信息小字落在正文下方
6660
6677
  return '<div class="chat-row">' +
6661
6678
  '<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="#i-bee"></use></svg></span>' +
6662
6679
  '<div class="chat-bubble agent">' +
6663
6680
  '<div class="chat-body">' + bodyHtml + "</div>" +
6664
6681
  '<div class="chat-meta">' + esc(it.who || "") + " " + esc(chatTime(it.at)) +
6665
- (metaBad ? " · " + t(it.status === "failed" ? "失败" : "已取消") : "") + "</div>" +
6682
+ (metaBad ? " · " + t(it.status === "failed" ? "失败" : "已取消") : "") + actsA + "</div>" +
6666
6683
  (it.log && it.run ? '<div class="chat-actions"><button class="chat-log-btn" type="button" data-chat-log-run="' +
6667
6684
  esc(it.run) + '" data-chat-log-rel="' + esc(it.log) + '" title="' + esc(t("查看日志")) + '">' + esc(t("CLI 日志")) + "</button></div>" : "") +
6668
6685
  (Array.isArray(it.followups) && it.followups.length
@@ -7144,6 +7161,27 @@ function bindChat() {
7144
7161
  artPopup(file.dataset.fileRun, file.dataset.fileName || "", Number(file.dataset.fileSize) || 0);
7145
7162
  return;
7146
7163
  }
7164
+ // 复制 / 编辑重发(2026-09-22 用户诉求:已发送的对话可复制修改再次发送)。
7165
+ // 编辑=原文载入输入框,用户改完按发送即再次发出(不静默自动重发)
7166
+ const act = e.target.closest("[data-cact]");
7167
+ if (act) {
7168
+ const it = chatTimelineItems[Number(act.dataset.ci)];
7169
+ if (!it) return;
7170
+ const txt = it.kind === "user" ? String(it.text || "") : chatCleanText(it.text);
7171
+ if (act.dataset.cact === "copy") {
7172
+ copyText(txt);
7173
+ toast(t("已复制"));
7174
+ } else if (act.dataset.cact === "edit") {
7175
+ const ta = $("rd-chat-input");
7176
+ if (ta && txt) {
7177
+ ta.value = txt;
7178
+ ta.focus();
7179
+ ta.selectionStart = ta.selectionEnd = ta.value.length;
7180
+ toast(t("已载入原文,可修改后发送"));
7181
+ }
7182
+ }
7183
+ return;
7184
+ }
7147
7185
  const chip = e.target.closest(".att-open");
7148
7186
  if (chip) window.chatAttPopup(chip.dataset.att);
7149
7187
  });
package/app/ui/i18n.js CHANGED
@@ -242,6 +242,7 @@
242
242
  "双击弹窗查看该文件的变更": "Double-click to view this file's changes in a popup",
243
243
  "复制": "Copy",
244
244
  "已复制": "Copied",
245
+ "已载入原文,可修改后发送": "Original text loaded — edit and send",
245
246
  "选择文件夹": "Choose folder",
246
247
  "此电脑": "This PC",
247
248
  "上级": "Up",
package/app/ui/style.css CHANGED
@@ -5696,3 +5696,15 @@ button.request-busy > * { opacity: .18; }
5696
5696
  /* 气泡图片缩略图:限制在气泡内(曾压住输入框边框线——2026-09-21 用户反馈) */
5697
5697
  .chat-img img { box-sizing: border-box; }
5698
5698
  .chat-bubble { overflow-wrap: anywhere; }
5699
+
5700
+ /* 对话气泡悬停动作(复制/编辑重发,2026-09-22):默认隐没在元信息行,
5701
+ * 悬停该条消息才浮现,不与正文抢注意力 */
5702
+ .chat-meta { display: flex; align-items: center; gap: 6px; }
5703
+ .chat-acts2 { margin-left: auto; display: inline-flex; gap: 2px;
5704
+ opacity: 0; transition: opacity .15s; }
5705
+ .chat-row:hover .chat-acts2 { opacity: 1; }
5706
+ .chat-act { appearance: none; border: none; background: transparent;
5707
+ color: var(--muted); font: inherit; font-size: 11px; cursor: pointer;
5708
+ padding: 1px 7px; border-radius: 6px; }
5709
+ .chat-act:hover { background: color-mix(in srgb, var(--text) 8%, transparent);
5710
+ color: var(--text); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebee",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
4
4
  "description": "CodeBee · 多智能体编排台 — 统一编排本机 AI 编码 CLI(Codex / Claude Code / QwenCode / OpenCode / Aider…):目标→拆解→路由→执行→验证→跨厂商评审→修复→报告。纯 Python 标准库实现,本地 Web UI。",
5
5
  "license": "MIT",
6
6
  "bin": {