codebee 0.1.2 → 0.1.3

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/app/ui/index.html CHANGED
@@ -48,6 +48,7 @@
48
48
  <symbol id="i-bee" viewBox="0 0 24 24"><ellipse cx="12" cy="14" rx="5.2" ry="6.2"/><path d="M8 12h8M8.3 14.8h7.4M9.4 17.4h5.2"/><path d="M9.6 8.4C8 6.2 5.4 6.6 5.7 8.6c.2 1.4 2.1 1.9 3.7 1.1M14.4 8.4c1.6-2.2 4.2-1.8 3.9.2-.2 1.4-2.1 1.9-3.7 1.1"/><path d="M13.4 5.6l2.1-2.1M15.9 6.9l2.5-1.2"/></symbol>
49
49
  <symbol id="i-gamepad" viewBox="0 0 24 24"><line x1="6" x2="10" y1="11" y2="11"/><line x1="8" x2="8" y1="9" y2="13"/><line x1="15" x2="15.01" y1="12" y2="12"/><line x1="18" x2="18.01" y1="10" y2="10"/><path d="M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z"/></symbol>
50
50
  <symbol id="i-book" viewBox="0 0 24 24"><path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"/><path d="M8 8h5M8 12h8M8 16h6"/></symbol>
51
+ <symbol id="i-plus" viewBox="0 0 24 24"><path d="M12 5v14"/><path d="M5 12h14"/></symbol>
51
52
  <symbol id="i-chat" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M8 9h8M8 12.5h5"/></symbol>
52
53
  <symbol id="i-workflow" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/><path d="M10 6.5h4a3 3 0 0 1 3 3V14"/></symbol>
53
54
  <symbol id="i-phone" viewBox="0 0 24 24"><rect x="5" y="2" width="14" height="20" rx="2.5"/><path d="M12 18h.01"/></symbol>
@@ -381,7 +382,7 @@
381
382
  <div class="rd-chat-bar">
382
383
  <textarea id="rd-chat-input" rows="2" data-i18n-ph="继续说…(Enter 发送,Shift+Enter 换行;可直接粘贴截图)" placeholder="继续说…(Enter 发送,Shift+Enter 换行;可直接粘贴截图)"></textarea>
383
384
  <div class="rd-chat-tools">
384
- <button id="rd-chat-attach" class="ghost small" data-i18n="附件">附件</button>
385
+ <button id="rd-chat-attach" class="ghost" data-i18n-title="添加附件" title="添加附件" data-i18n-aria="添加附件" aria-label="添加附件"><svg class="ico" aria-hidden="true"><use href="#i-plus"></use></svg></button>
385
386
  <span id="rd-chat-att-list" class="rd-att-list"></span>
386
387
  <span id="rd-chat-hint" class="rd-hint"></span>
387
388
  <button id="rd-chat-send" class="primary round" data-i18n-title="发送" title="发送" data-i18n-aria="发送" aria-label="发送"><svg class="ico" aria-hidden="true"><use href="#i-arrow-up"></use></svg></button>
@@ -550,7 +551,10 @@
550
551
  <!-- CLI 绑定 -->
551
552
  <div id="sub-bindings" class="subpage hidden">
552
553
  <div class="panel wide">
553
- <div class="panel-head"><h2 data-i18n="CLI 绑定">CLI 绑定</h2></div>
554
+ <div class="panel-head"><h2 data-i18n="CLI 绑定">CLI 绑定</h2>
555
+ <button id="btn-autobind" class="ghost small" onclick="autoBindAll()" data-i18n="一键推荐绑定">一键推荐绑定</button>
556
+ <button id="btn-saveall-bind" class="ghost small hidden" onclick="saveAllBindings()" data-i18n="全部保存">全部保存</button>
557
+ </div>
554
558
  <p data-i18n="CodeBee 编排运行时的模型都在这里配:模型链可跨厂商混搭——每条自带供应商注入(API key + 地址),第 1 条是主模型、其余按顺序降级,瞬态错误自动切到下一条(可能是另一家厂商)。链空则按供应商默认 / 难度自动选。只影响 CodeBee 发起的调用,不改写 CLI 全局配置(要改默认模型请到「智能体管理」页)。" class="hint">CodeBee 编排运行时的模型都在这里配:模型链可跨厂商混搭——每条自带供应商注入(API key + 地址),第 1 条是主模型、其余按顺序降级,瞬态错误自动切到下一条(可能是另一家厂商)。链空则按供应商默认 / 难度自动选。只影响 CodeBee 发起的调用,不改写 CLI 全局配置(要改默认模型请到「智能体管理」页)。</p>
555
559
  <div id="binding-list" class="cards"></div>
556
560
  </div>
package/app/ui/style.css CHANGED
@@ -2526,75 +2526,81 @@ details.sf-dir > summary i, a.sf-file i { font-style: normal; color: var(--muted
2526
2526
  }
2527
2527
 
2528
2528
  /* ---------------- 作品信息(独立 TAB:番茄/七猫平台卡片,字段内联逐条复制) ---------------- */
2529
- .rd-bookmeta { padding: 2px 0 20px; max-width: 1180px; }
2530
- .bm-head { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
2531
- .bm-head .ico { color: var(--accent); }
2532
- .bm-head .hint { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2529
+ .rd-bookmeta { padding: 4px 0 24px; max-width: 1120px; }
2530
+ .bm-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 6px; padding: 0 2px; }
2531
+ .bm-head .ico { color: var(--accent); width: 16px; height: 16px; }
2532
+ .bm-head .hint { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
2533
2533
  .bm-warn { color: var(--warn); font-size: 12px; flex: none; }
2534
- .bm-guide { display: flex; gap: 12px; align-items: flex-start; padding: 14px 16px; margin-bottom: 16px;
2535
- border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel); }
2536
- .bm-guide .ico { flex: none; margin-top: 1px; width: 18px; height: 18px; color: var(--accent); }
2534
+ .bm-guide { display: flex; gap: 12px; align-items: flex-start; padding: 14px 18px; margin: 12px 0 16px;
2535
+ border: 1px dashed var(--border-strong); border-radius: var(--r-md); background: none; }
2536
+ .bm-guide .ico { flex: none; margin-top: 2px; width: 18px; height: 18px; color: var(--accent); }
2537
2537
  .bm-guide b { color: var(--text); font-size: 13.5px; font-weight: 600; }
2538
2538
  .bm-guide p { margin: 5px 0 0; font-size: 12.5px; line-height: 1.75; color: var(--muted); }
2539
2539
 
2540
- /* 平台卡片:两列自适应;窄屏自动堆叠 */
2541
- .bm-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 16px; }
2540
+ /* 平台卡片:两列自适应;窄屏自动堆叠。悬浮轻抬 + 描边渐亮,观感呼应蜂巢格 */
2541
+ .bm-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 18px; }
2542
2542
  .bm-card { display: flex; flex-direction: column; border: 1px solid var(--line);
2543
- border-radius: var(--r-md); background: var(--panel); overflow: hidden; min-width: 0; }
2543
+ border-radius: var(--r-lg); background: var(--panel); overflow: hidden; min-width: 0;
2544
+ box-shadow: var(--shadow); transition: border-color .18s, transform .18s; }
2545
+ .bm-card:hover { border-color: var(--border-strong); transform: translateY(-1px); }
2544
2546
  .bm-card.st-running { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); }
2545
- .bm-card-head { display: flex; align-items: center; gap: 10px; padding: 11px 16px;
2547
+ .bm-card-head { display: flex; align-items: center; gap: 10px; padding: 12px 18px;
2546
2548
  background: var(--panel2); border-bottom: 1px solid var(--line); }
2547
- .bm-plat-name { display: inline-flex; align-items: center; gap: 7px; font-weight: 600; font-size: 13.5px; }
2548
- .bm-plat-name .ico { width: 15px; height: 15px; color: var(--accent); }
2549
- .bm-st { flex: none; font-size: 11.5px; line-height: 1; padding: 4px 9px; border-radius: 999px;
2550
- border: 1px solid var(--line); color: var(--muted); }
2551
- .bm-st.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, transparent);
2549
+ .bm-plat-name { display: inline-flex; align-items: center; gap: 7px; font-weight: 700; font-size: 14px;
2550
+ letter-spacing: .3px; }
2551
+ .bm-plat-name .ico { width: 16px; height: 16px; color: var(--accent); }
2552
+ .bm-st { flex: none; font-size: 11.5px; line-height: 1; padding: 4px 10px; border-radius: 999px;
2553
+ border: 1px solid var(--line); color: var(--muted); font-weight: 500; }
2554
+ .bm-st.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent);
2552
2555
  background: color-mix(in srgb, var(--ok) 12%, transparent); }
2553
- .bm-st.run { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent);
2556
+ .bm-st.run { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, transparent);
2554
2557
  background: color-mix(in srgb, var(--accent) 12%, transparent); animation: bm-pulse 1.6s ease-in-out infinite; }
2555
- .bm-st.bad { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 45%, transparent);
2558
+ .bm-st.bad { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, transparent);
2556
2559
  background: color-mix(in srgb, var(--bad) 12%, transparent); }
2557
2560
  @keyframes bm-pulse { 50% { opacity: .5; } }
2558
- .bm-card-head button { font-size: 12.5px; padding: 4px 11px; }
2561
+ .bm-card-head button { font-size: 12.5px; padding: 5px 12px; }
2559
2562
 
2560
- /* 字段区:标签固定宽右对齐、值占满;行 hover 才点亮复制按钮(默认低调不抢视线) */
2561
- .bm-fields { padding: 4px 16px 12px; }
2562
- .bm-f { display: flex; align-items: baseline; gap: 12px; padding: 9px 0;
2563
+ /* 字段区:标签固定宽右对齐、值占满;整卡 padding 拉开呼吸感;hover 才点亮复制按钮 */
2564
+ .bm-fields { padding: 6px 18px 14px; }
2565
+ .bm-f { display: flex; align-items: baseline; gap: 14px; padding: 11px 0;
2563
2566
  border-bottom: 1px solid var(--line); min-width: 0; }
2564
- .bm-f:last-child { border-bottom: 0; padding-bottom: 4px; }
2565
- .bm-f-k { flex: none; width: 5.2em; color: var(--muted); font-size: 12px; text-align: right; }
2566
- .bm-f-v { flex: 1 1 auto; min-width: 0; font-size: 13.5px; line-height: 1.65; color: var(--text);
2567
+ .bm-f:last-child { border-bottom: 0; padding-bottom: 6px; }
2568
+ .bm-f-k { flex: none; width: 5.2em; color: var(--muted); font-size: 12px; text-align: justify;
2569
+ text-align-last: justify; }
2570
+ .bm-f-v { flex: 1 1 auto; min-width: 0; font-size: 13.5px; line-height: 1.7; color: var(--text);
2567
2571
  word-break: break-all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2568
2572
  .bm-f-v.bm-f--scroll { max-height: 7.5em; overflow-y: auto; white-space: pre-wrap;
2569
- text-overflow: clip; font-size: 13px; line-height: 1.75; }
2570
- .bm-chips { flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; }
2571
- .bm-chip { font-size: 12px; line-height: 1; padding: 5px 10px; border-radius: 999px;
2572
- background: var(--panel2); border: 1px solid var(--line); color: var(--text); }
2573
+ text-overflow: clip; font-size: 13px; line-height: 1.8; padding: 2px 0; }
2574
+ .bm-chips { flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: 7px; min-width: 0; }
2575
+ .bm-chip { font-size: 12px; line-height: 1; padding: 5px 11px; border-radius: 999px;
2576
+ background: var(--panel2); border: 1px solid var(--line); color: var(--text);
2577
+ transition: border-color .15s; }
2578
+ .bm-card:hover .bm-chip { border-color: var(--border-strong); }
2573
2579
 
2574
2580
  /* 复制按钮:图标 + 文字,默认淡出,行 hover / 键盘聚焦时点亮 */
2575
- .bm-copy { flex: none; display: inline-flex; align-items: center; gap: 4px;
2576
- font-size: 11.5px; line-height: 1; padding: 4px 8px; border-radius: var(--r-sm);
2581
+ .bm-copy { flex: none; display: inline-flex; align-items: center; gap: 4px; align-self: center;
2582
+ font-size: 11.5px; line-height: 1; padding: 4px 9px; border-radius: 999px;
2577
2583
  border: 1px solid transparent; background: none; color: var(--muted); cursor: pointer;
2578
2584
  opacity: .55; transition: opacity .15s, color .15s, background .15s, border-color .15s; }
2579
2585
  .bm-copy .ico { width: 12px; height: 12px; }
2580
2586
  .bm-f:hover .bm-copy { opacity: 1; }
2581
- .bm-copy:hover, .bm-copy:focus-visible { color: var(--accent); border-color: var(--line);
2587
+ .bm-copy:hover, .bm-copy:focus-visible { color: var(--accent); border-color: var(--border-strong);
2582
2588
  background: var(--panel2); opacity: 1; }
2583
2589
  .bm-copy:active { transform: translateY(1px); }
2584
2590
 
2585
2591
  /* 生成中骨架屏 */
2586
- .bm-skwrap { padding: 6px 16px 14px; }
2587
- .bm-sk { display: flex; gap: 12px; align-items: center; padding: 9px 0;
2592
+ .bm-skwrap { padding: 6px 18px 16px; }
2593
+ .bm-sk { display: flex; gap: 14px; align-items: center; padding: 11px 0;
2588
2594
  border-bottom: 1px solid var(--line); }
2589
2595
  .bm-sk:last-child { border-bottom: 0; }
2590
2596
  .bm-sk-l { flex: none; width: 5.2em; height: 9px; border-radius: 5px; background: var(--line); }
2591
2597
  .bm-sk-v { flex: 1 1 auto; height: 9px; border-radius: 5px; background: var(--line);
2592
2598
  opacity: .55; animation: bm-pulse 1.6s ease-in-out infinite; }
2593
- .bm-empty { color: var(--muted); font-size: 13px; padding: 16px; text-align: center; }
2594
- .bm-errhint { color: var(--bad); font-size: 12.5px; line-height: 1.7; padding: 14px 16px;
2599
+ .bm-empty { color: var(--muted); font-size: 13px; padding: 24px; text-align: center; }
2600
+ .bm-errhint { color: var(--bad); font-size: 12.5px; line-height: 1.7; padding: 14px 18px;
2595
2601
  word-break: break-all; }
2596
- .bm-src { color: var(--muted); font-size: 11.5px; padding: 9px 16px; border-top: 1px solid var(--line);
2597
- background: var(--panel2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2602
+ .bm-src { margin: 0 18px; padding: 10px 0 2px; border-top: 1px solid var(--line);
2603
+ color: var(--muted); font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2598
2604
  .rd-bookmeta .ico.spin { animation: ico-spin 1.2s linear infinite; }
2599
2605
 
2600
2606
  /* 文件内容弹窗·文本预览/编辑(目录文件弹窗专属):
@@ -2708,29 +2714,51 @@ html.code-wrap #file-pop .fp-edit { white-space: pre-wrap; word-break: break-wor
2708
2714
  .keys-box .krow .row-ops { display: flex; align-items: center; gap: 6px; flex: none; }
2709
2715
  .tag.warn { color: var(--warn, #b45309); border-color: var(--warn, #b45309); }
2710
2716
 
2711
- /* ============ 对话分区(直连任务默认视图) ============ */
2712
- .rd-chat { display: flex; flex-direction: column; gap: 12px; min-height: 0; }
2713
- .rd-chat-flow { display: flex; flex-direction: column; gap: 10px; overflow-y: auto;
2714
- max-height: calc(100vh - 320px); padding: 4px 2px; scroll-behavior: smooth; }
2715
- .chat-row { display: flex; justify-content: flex-start; }
2717
+ /* ============ 对话分区(直连任务默认视图):Kimi/DeepSeek 式对话 ============
2718
+ 智能体回复整幅平铺(不套气泡框),用户消息右侧浅灰圆角块,底部居中大输入条。
2719
+ 列宽收敛到 820px,读起来像平常聊天而不是「表单里的两个框」。 */
2720
+ .rd-chat { display: flex; flex-direction: column; min-height: 0; }
2721
+ .rd-chat-flow { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 22px;
2722
+ max-width: 820px; width: 100%; margin: 0 auto; padding: 10px 12px 20px;
2723
+ max-height: calc(100vh - 320px); scroll-behavior: smooth; }
2724
+ .chat-row { display: flex; gap: 12px; align-items: flex-start; }
2716
2725
  .chat-row.me { justify-content: flex-end; }
2717
- .chat-bubble { max-width: 78%; border: 1px solid var(--border); border-radius: 12px;
2718
- padding: 8px 12px; background: var(--panel2); font-size: 13.5px; line-height: 1.6;
2719
- overflow-wrap: anywhere; }
2720
- .chat-bubble.me { background: color-mix(in srgb, var(--accent) 12%, var(--panel));
2721
- border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); }
2722
- .chat-bubble.agent { background: var(--bg); }
2723
- .chat-meta { font-size: 11px; color: var(--muted); margin-bottom: 4px; }
2724
- .chat-bubble.me .chat-meta { text-align: right; }
2725
- .chat-body { margin: 0; font: inherit; white-space: pre-wrap; max-height: 420px;
2726
- overflow-y: auto; }
2727
- .chat-atts { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
2728
- .rd-chat-bar { border: 1px solid var(--border); border-radius: 12px; background: var(--panel);
2729
- padding: 4px 6px 6px; transition: border-color .15s; }
2730
- .rd-chat-bar:focus-within { border-color: var(--accent); }
2726
+ /* 头像:智能体侧一个小圆标(蜜蜂),用户侧不显示 */
2727
+ .chat-avatar { flex: none; width: 28px; height: 28px; border-radius: 50%; margin-top: 1px;
2728
+ display: inline-flex; align-items: center; justify-content: center;
2729
+ background: var(--panel2); border: 1px solid var(--border); color: var(--muted); }
2730
+ .chat-avatar .ico { width: 15px; height: 15px; }
2731
+ /* 智能体回复:正文平铺,无边框无底色(像官网那样整幅读) */
2732
+ .chat-bubble { overflow-wrap: anywhere; }
2733
+ .chat-bubble.agent { flex: 1; min-width: 0; background: transparent; border: none; padding: 0;
2734
+ font-size: 14.5px; line-height: 1.75; color: var(--text); }
2735
+ /* 用户消息:右侧浅灰圆角块 */
2736
+ .chat-bubble.me { max-width: 74%; background: var(--panel2); border: 1px solid var(--border);
2737
+ border-radius: 16px; border-top-right-radius: 6px; padding: 10px 14px;
2738
+ font-size: 14.5px; line-height: 1.65; }
2739
+ .chat-meta { font-size: 11.5px; color: var(--muted); margin-bottom: 6px; }
2740
+ .chat-bubble.me .chat-meta { text-align: right; margin-bottom: 4px; }
2741
+ .chat-body { margin: 0; font: inherit; white-space: pre-wrap; }
2742
+ .chat-atts { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
2743
+ /* 底部输入条:整块圆角卡片,+ 在左、发送在右(贴底常驻) */
2744
+ .rd-chat-bar { max-width: 820px; width: calc(100% - 24px); margin: 0 auto 6px;
2745
+ border: 1px solid var(--border-strong); border-radius: 18px; background: var(--panel);
2746
+ padding: 8px 10px 9px; transition: border-color .15s, box-shadow .15s; }
2747
+ .rd-chat-bar:focus-within { border-color: var(--accent);
2748
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 10%, transparent); }
2731
2749
  .rd-chat-bar textarea { border: none; background: transparent; resize: none;
2732
- padding: 8px 6px 4px; font-size: 14px; width: 100%; }
2750
+ padding: 8px 6px 4px; font-size: 15px; line-height: 1.6; width: 100%;
2751
+ min-height: 52px; max-height: 220px; }
2733
2752
  .rd-chat-bar textarea:hover, .rd-chat-bar textarea:focus { border: none; box-shadow: none; }
2734
- .rd-chat-tools { display: flex; align-items: center; gap: 8px; padding: 0 4px; flex-wrap: wrap; }
2753
+ .rd-chat-tools { display: flex; align-items: center; gap: 8px; padding: 0 2px; flex-wrap: wrap; }
2754
+ /* 附件:圆形 + 按钮(Kimi 式左下角) */
2755
+ .rd-chat-tools #rd-chat-attach { width: 30px; height: 30px; border-radius: 50%; padding: 0;
2756
+ display: inline-flex; align-items: center; justify-content: center; flex: none; }
2757
+ .rd-chat-tools #rd-chat-attach .ico { width: 16px; height: 16px; }
2735
2758
  .rd-chat-tools .primary.round { width: 32px; height: 32px; border-radius: 50%; padding: 0;
2736
2759
  display: inline-flex; align-items: center; justify-content: center; margin-left: auto; }
2760
+ .rd-chat-tools .primary.round .ico { width: 16px; height: 16px; }
2761
+ @media (max-width: 720px) {
2762
+ .rd-chat-flow { gap: 18px; padding: 8px 6px 16px; }
2763
+ .chat-bubble.me { max-width: 86%; }
2764
+ }
package/bin/tutti.js CHANGED
@@ -1,147 +1,147 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- /*
4
- * CodeBee 启动垫片:在本机找 Python 3.8+,以子进程方式运行包内 app/main.py。
5
- * npm 只负责分发与版本管理,不携带解释器——所以运行时才探测 python,
6
- * 找不到就报错并给出安装指引(不写 postinstall,避免在别人机器上制造环境事故)。
7
- *
8
- * 安全边界:
9
- * - 子进程可执行文件全部是本文件内的字面量(py / python / python3)——包括
10
- * TUTTI_PYTHON 覆盖时:它只允许在这三个字面量里选一个,绝不拼接任意路径;
11
- * - 参数数组全由字面量常量拼成,不启用 shell;
12
- * - 探测只传 `--version`(不向解释器传递任何代码);Windows 商店的 python.exe
13
- * 假占位会非零退出,被 catch 后自然跳过;
14
- * - 用户参数逐个过白名单校验(仅字母数字与 _ - . : = / @ % 空格),
15
- * 含 shell 元字符/控制字符的参数直接拒绝。
16
- */
17
- const { execFileSync, spawn } = require('child_process');
18
- const path = require('path');
19
-
20
- const MIN_MAJOR = 3;
21
- const MIN_MINOR = 8;
22
- const MAIN = path.join(__dirname, '..', 'app', 'main.py');
23
- const SAFE_ARG = /^[A-Za-z0-9_\-.:=/@% ]+$/;
24
-
25
- // 解析 `--version` 输出:形如 "Python 3.12.4",返回 {ver, major, minor} 或 null。
26
- // 纯字符串切分,不用正则匹配执行路径。
27
- function parseVersion(text) {
28
- const at = String(text).indexOf('Python ');
29
- if (at === -1) return null;
30
- const parts = String(text).slice(at + 7).trim().split(' ')[0].split('.');
31
- const major = Number(parts[0]);
32
- const minor = Number(parts[1]);
33
- if (!Number.isFinite(major) || !Number.isFinite(minor)) return null;
34
- return { ver: major + '.' + minor, major: major, minor: minor };
35
- }
36
-
37
- function probePy() {
38
- try {
39
- return parseVersion(execFileSync('py', ['-3', '--version'],
40
- { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }));
41
- } catch (e) { return null; }
42
- }
43
- function probePython() {
44
- try {
45
- return parseVersion(execFileSync('python', ['--version'],
46
- { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }));
47
- } catch (e) { return null; }
48
- }
49
- function probePython3() {
50
- try {
51
- return parseVersion(execFileSync('python3', ['--version'],
52
- { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }));
53
- } catch (e) { return null; }
54
- }
55
-
56
- // 探测顺序:win32 优先 py 启动器(跨版本选真 3.x),其余按 python3 → python。
57
- function resolvePython() {
58
- const order = process.platform === 'win32'
59
- ? [['py', probePy], ['python', probePython], ['python3', probePython3]]
60
- : [['python3', probePython3], ['python', probePython]];
61
- for (const [name, probe] of order) {
62
- const hit = probe();
63
- if (!hit) continue;
64
- if (hit.major > MIN_MAJOR || (hit.major === MIN_MAJOR && hit.minor >= MIN_MINOR)) {
65
- return { name: name };
66
- }
67
- return { tooOld: true, name: name, ver: hit.ver };
68
- }
69
- return null;
70
- }
71
-
72
- function guide() {
73
- const url = { win32: 'https://www.python.org/downloads/windows/',
74
- darwin: 'https://www.python.org/downloads/macos/',
75
- linux: 'https://docs.python.org/3/using/unix.html' }[process.platform]
76
- || 'https://www.python.org/downloads/';
77
- return 'CodeBee 需要本机安装 Python 3.8+(npm 包不含解释器)。\n'
78
- + ' · 安装:' + url + '\n'
79
- + ' · Windows 安装时勾选 “Add python.exe to PATH”(自带 py 启动器)\n'
80
- + '装好后重新运行:codebee';
81
- }
82
-
83
- const py = resolvePython();
84
- if (!py) {
85
- console.error('[CodeBee] 未找到可用的 Python。\n' + guide());
86
- process.exit(1);
87
- }
88
- if (py.tooOld) {
89
- console.error('[CodeBee] 找到 ' + py.name + '(' + py.ver + '),但版本过低。\n' + guide());
90
- process.exit(1);
91
- }
92
-
93
- const userArgs = [];
94
- for (const a of process.argv.slice(2)) {
95
- if (typeof a === 'string' && SAFE_ARG.test(a)) {
96
- userArgs.push(a);
97
- } else {
98
- console.error('[CodeBee] 参数含非法字符,已拒绝:' + String(a).slice(0, 80));
99
- process.exit(2);
100
- }
101
- }
102
-
103
- // CLI 转发契约:codebee 之后的用户参数(已过白名单)原样作为 main.py 的 argv。
104
- // 控制台日志即见:GBK 控制台下强制 UTF-8 输出(否则中文 print 触发 UnicodeEncodeError 静默卡死)。
105
- const childEnv = Object.assign({}, process.env, { PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' });
106
- // TUTTI_PYTHON 覆盖解释器:只允许在三个字面量里选(py/python/python3,.exe
107
- // 后缀可选、大小写不限)——环境变量永远只做「选择题」,不做「填空题」;
108
- // 机器上有多个解释器且探测选错时,用它切换。
109
- let exe = py.name;
110
- const ovRaw = String(process.env.TUTTI_PYTHON || '').trim().toLowerCase();
111
- const ov = ovRaw.endsWith('.exe') ? ovRaw.slice(0, -4) : ovRaw;
112
- if (ov === 'python') {
113
- exe = 'python';
114
- } else if (ov === 'python3') {
115
- exe = 'python3';
116
- } else if (ov === 'py') {
117
- exe = 'py';
118
- }
119
-
120
- let child;
121
- if (exe === 'py') {
122
- child = spawn('py', ['-3', MAIN, ...userArgs], { stdio: 'inherit', env: childEnv });
123
- } else if (exe === 'python') {
124
- child = spawn('python', [MAIN, ...userArgs], { stdio: 'inherit', env: childEnv });
125
- } else {
126
- child = spawn('python3', [MAIN, ...userArgs], { stdio: 'inherit', env: childEnv });
127
- }
128
- console.log('[CodeBee] 正在启动(Python: ' + exe + ')… 浏览器将自动打开 http://127.0.0.1:8765;'
129
- + '本窗口保持开着就是服务,Ctrl+C 退出。');
130
-
131
- for (const sig of ['SIGINT', 'SIGTERM']) {
132
- process.on(sig, () => { try { child.kill(sig); } catch (e) { /* 已退出 */ } });
133
- }
134
- const t0 = Date.now();
135
- child.on('exit', (code, signal) => {
136
- if (!signal && code != null && code !== 0 && Date.now() - t0 < 4000) {
137
- // 秒退:多半是 py 启动器指到了坏解释器(如损坏的 Anaconda)。给排障出口。
138
- console.error('[CodeBee] 启动失败:Python 进程 ' + (code ? '退出码 ' + code : '异常终止') + '。'
139
- + '\n · 运行 python --version 确认可用且为 3.8+'
140
- + '\n · 若装有多个 Python,可设环境变量 TUTTI_PYTHON 切换(cmd:set TUTTI_PYTHON=python)');
141
- }
142
- process.exit(signal ? 1 : (code == null ? 0 : code));
143
- });
144
- child.on('error', (e) => {
145
- console.error('[CodeBee] 无法启动 Python:' + e.message);
146
- process.exit(1);
147
- });
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /*
4
+ * CodeBee 启动垫片:在本机找 Python 3.8+,以子进程方式运行包内 app/main.py。
5
+ * npm 只负责分发与版本管理,不携带解释器——所以运行时才探测 python,
6
+ * 找不到就报错并给出安装指引(不写 postinstall,避免在别人机器上制造环境事故)。
7
+ *
8
+ * 安全边界:
9
+ * - 子进程可执行文件全部是本文件内的字面量(py / python / python3)——包括
10
+ * TUTTI_PYTHON 覆盖时:它只允许在这三个字面量里选一个,绝不拼接任意路径;
11
+ * - 参数数组全由字面量常量拼成,不启用 shell;
12
+ * - 探测只传 `--version`(不向解释器传递任何代码);Windows 商店的 python.exe
13
+ * 假占位会非零退出,被 catch 后自然跳过;
14
+ * - 用户参数逐个过白名单校验(仅字母数字与 _ - . : = / @ % 空格),
15
+ * 含 shell 元字符/控制字符的参数直接拒绝。
16
+ */
17
+ const { execFileSync, spawn } = require('child_process');
18
+ const path = require('path');
19
+
20
+ const MIN_MAJOR = 3;
21
+ const MIN_MINOR = 8;
22
+ const MAIN = path.join(__dirname, '..', 'app', 'main.py');
23
+ const SAFE_ARG = /^[A-Za-z0-9_\-.:=/@% ]+$/;
24
+
25
+ // 解析 `--version` 输出:形如 "Python 3.12.4",返回 {ver, major, minor} 或 null。
26
+ // 纯字符串切分,不用正则匹配执行路径。
27
+ function parseVersion(text) {
28
+ const at = String(text).indexOf('Python ');
29
+ if (at === -1) return null;
30
+ const parts = String(text).slice(at + 7).trim().split(' ')[0].split('.');
31
+ const major = Number(parts[0]);
32
+ const minor = Number(parts[1]);
33
+ if (!Number.isFinite(major) || !Number.isFinite(minor)) return null;
34
+ return { ver: major + '.' + minor, major: major, minor: minor };
35
+ }
36
+
37
+ function probePy() {
38
+ try {
39
+ return parseVersion(execFileSync('py', ['-3', '--version'],
40
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }));
41
+ } catch (e) { return null; }
42
+ }
43
+ function probePython() {
44
+ try {
45
+ return parseVersion(execFileSync('python', ['--version'],
46
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }));
47
+ } catch (e) { return null; }
48
+ }
49
+ function probePython3() {
50
+ try {
51
+ return parseVersion(execFileSync('python3', ['--version'],
52
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }));
53
+ } catch (e) { return null; }
54
+ }
55
+
56
+ // 探测顺序:win32 优先 py 启动器(跨版本选真 3.x),其余按 python3 → python。
57
+ function resolvePython() {
58
+ const order = process.platform === 'win32'
59
+ ? [['py', probePy], ['python', probePython], ['python3', probePython3]]
60
+ : [['python3', probePython3], ['python', probePython]];
61
+ for (const [name, probe] of order) {
62
+ const hit = probe();
63
+ if (!hit) continue;
64
+ if (hit.major > MIN_MAJOR || (hit.major === MIN_MAJOR && hit.minor >= MIN_MINOR)) {
65
+ return { name: name };
66
+ }
67
+ return { tooOld: true, name: name, ver: hit.ver };
68
+ }
69
+ return null;
70
+ }
71
+
72
+ function guide() {
73
+ const url = { win32: 'https://www.python.org/downloads/windows/',
74
+ darwin: 'https://www.python.org/downloads/macos/',
75
+ linux: 'https://docs.python.org/3/using/unix.html' }[process.platform]
76
+ || 'https://www.python.org/downloads/';
77
+ return 'CodeBee 需要本机安装 Python 3.8+(npm 包不含解释器)。\n'
78
+ + ' · 安装:' + url + '\n'
79
+ + ' · Windows 安装时勾选 “Add python.exe to PATH”(自带 py 启动器)\n'
80
+ + '装好后重新运行:codebee';
81
+ }
82
+
83
+ const py = resolvePython();
84
+ if (!py) {
85
+ console.error('[CodeBee] 未找到可用的 Python。\n' + guide());
86
+ process.exit(1);
87
+ }
88
+ if (py.tooOld) {
89
+ console.error('[CodeBee] 找到 ' + py.name + '(' + py.ver + '),但版本过低。\n' + guide());
90
+ process.exit(1);
91
+ }
92
+
93
+ const userArgs = [];
94
+ for (const a of process.argv.slice(2)) {
95
+ if (typeof a === 'string' && SAFE_ARG.test(a)) {
96
+ userArgs.push(a);
97
+ } else {
98
+ console.error('[CodeBee] 参数含非法字符,已拒绝:' + String(a).slice(0, 80));
99
+ process.exit(2);
100
+ }
101
+ }
102
+
103
+ // CLI 转发契约:codebee 之后的用户参数(已过白名单)原样作为 main.py 的 argv。
104
+ // 控制台日志即见:GBK 控制台下强制 UTF-8 输出(否则中文 print 触发 UnicodeEncodeError 静默卡死)。
105
+ const childEnv = Object.assign({}, process.env, { PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1', PYTHONUNBUFFERED: '1' });
106
+ // TUTTI_PYTHON 覆盖解释器:只允许在三个字面量里选(py/python/python3,.exe
107
+ // 后缀可选、大小写不限)——环境变量永远只做「选择题」,不做「填空题」;
108
+ // 机器上有多个解释器且探测选错时,用它切换。
109
+ let exe = py.name;
110
+ const ovRaw = String(process.env.TUTTI_PYTHON || '').trim().toLowerCase();
111
+ const ov = ovRaw.endsWith('.exe') ? ovRaw.slice(0, -4) : ovRaw;
112
+ if (ov === 'python') {
113
+ exe = 'python';
114
+ } else if (ov === 'python3') {
115
+ exe = 'python3';
116
+ } else if (ov === 'py') {
117
+ exe = 'py';
118
+ }
119
+
120
+ let child;
121
+ if (exe === 'py') {
122
+ child = spawn('py', ['-3', MAIN, ...userArgs], { stdio: 'inherit', env: childEnv });
123
+ } else if (exe === 'python') {
124
+ child = spawn('python', [MAIN, ...userArgs], { stdio: 'inherit', env: childEnv });
125
+ } else {
126
+ child = spawn('python3', [MAIN, ...userArgs], { stdio: 'inherit', env: childEnv });
127
+ }
128
+ console.log('[CodeBee] 正在启动(Python: ' + exe + ')… 浏览器将自动打开 http://127.0.0.1:8765;'
129
+ + '本窗口保持开着就是服务,Ctrl+C 退出。');
130
+
131
+ for (const sig of ['SIGINT', 'SIGTERM']) {
132
+ process.on(sig, () => { try { child.kill(sig); } catch (e) { /* 已退出 */ } });
133
+ }
134
+ const t0 = Date.now();
135
+ child.on('exit', (code, signal) => {
136
+ if (!signal && code != null && code !== 0 && Date.now() - t0 < 4000) {
137
+ // 秒退:多半是 py 启动器指到了坏解释器(如损坏的 Anaconda)。给排障出口。
138
+ console.error('[CodeBee] 启动失败:Python 进程 ' + (code ? '退出码 ' + code : '异常终止') + '。'
139
+ + '\n · 运行 python --version 确认可用且为 3.8+'
140
+ + '\n · 若装有多个 Python,可设环境变量 TUTTI_PYTHON 切换(cmd:set TUTTI_PYTHON=python)');
141
+ }
142
+ process.exit(signal ? 1 : (code == null ? 0 : code));
143
+ });
144
+ child.on('error', (e) => {
145
+ console.error('[CodeBee] 无法启动 Python:' + e.message);
146
+ process.exit(1);
147
+ });
package/package.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "name": "codebee",
3
- "version": "0.1.2",
4
- "description": "CodeBee · 多智能体编排台 — 统一编排本机 AI 编码 CLI(Codex / Claude Code / QwenCode / OpenCode / Aider…):目标→拆解→路由→执行→验证→跨厂商评审→修复→报告。纯 Python 标准库实现,本地 Web UI。",
5
- "license": "MIT",
6
- "bin": {
7
- "codebee": "bin/tutti.js"
8
- },
9
- "files": [
10
- "app",
11
- "bin",
12
- "README.md",
13
- "LICENSE"
14
- ],
15
- "engines": {
16
- "node": ">=14"
17
- },
18
- "os": [
19
- "win32",
20
- "darwin",
21
- "linux"
22
- ],
23
- "keywords": [
24
- "agent",
25
- "orchestration",
26
- "codex",
27
- "claude",
28
- "llm",
29
- "cli",
30
- "multi-agent"
31
- ],
32
- "repository": {
33
- "type": "git",
34
- "url": "git+https://github.com/Vercel-By-WXP/CodeBee.git"
35
- },
36
- "publishConfig": {
37
- "access": "public"
38
- }
39
- }
1
+ {
2
+ "name": "codebee",
3
+ "version": "0.1.3",
4
+ "description": "CodeBee · 多智能体编排台 — 统一编排本机 AI 编码 CLI(Codex / Claude Code / QwenCode / OpenCode / Aider…):目标→拆解→路由→执行→验证→跨厂商评审→修复→报告。纯 Python 标准库实现,本地 Web UI。",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "codebee": "bin/tutti.js"
8
+ },
9
+ "files": [
10
+ "app",
11
+ "bin",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=14"
17
+ },
18
+ "os": [
19
+ "win32",
20
+ "darwin",
21
+ "linux"
22
+ ],
23
+ "keywords": [
24
+ "agent",
25
+ "orchestration",
26
+ "codex",
27
+ "claude",
28
+ "llm",
29
+ "cli",
30
+ "multi-agent"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/Vercel-By-WXP/CodeBee.git"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }