magic-agent 1.0.109 → 1.0.111

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.
@@ -385,13 +385,14 @@ textarea{font:inherit}
385
385
  .rowmenu__item:active{background:var(--subtle)}
386
386
  .rowmenu__item--danger{color:var(--error-text)}
387
387
  .rowmenu__l{flex:1;min-width:0}
388
- /* 环境切换菜单(挂在任务页标题下):已保存的连接 + 「添加连接」。
388
+ /* 环境切换菜单(挂在任务页标题下):已保存的连接 + 「环境设置」/「扫码或粘贴地址」。
389
389
  与会话行长按菜单同一套浮层范式(scrim 挡住下层,点空白即关)。 */
390
390
  .envmenu{position:fixed;z-index:1201;width:min(320px,calc(100vw - 24px));
391
+ max-height:min(70vh,560px);overflow-y:auto;
391
392
  background:var(--raised);border:1px solid var(--border);border-radius:18px;
392
393
  box-shadow:0 18px 44px -14px var(--app-shadow);padding:6px}
393
394
  .envmenu__scrim{position:fixed;inset:0;z-index:1200}
394
- .envmenu__head{padding:10px 12px 6px;font-size:12px;font-weight:700;letter-spacing:.04em;color:var(--muted)}
395
+ .envmenu__head{display:block;padding:10px 12px 6px;font-size:12px;font-weight:700;letter-spacing:.04em;color:var(--muted)}
395
396
  .envmenu__item{display:flex;align-items:center;gap:12px;width:100%;padding:11px 12px;
396
397
  border-radius:12px;text-align:left}
397
398
  .envmenu__item:active{background:var(--subtle)}
@@ -405,7 +406,7 @@ textarea{font:inherit}
405
406
  .envmenu__s{font-size:12px;line-height:16px;color:var(--muted);
406
407
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
407
408
  .envmenu__check{flex:0 0 auto;color:var(--primary-action)}
408
- .envmenu__sep{height:1px;margin:6px 4px;background:var(--border)}
409
+ .envmenu__sep{display:block;height:1px;margin:6px 4px;background:var(--border)}
409
410
  .envmenu__add{display:flex;align-items:center;gap:10px;width:100%;padding:12px;border-radius:12px;
410
411
  font-size:15px;font-weight:500;color:var(--primary-action);text-align:left}
411
412
  .envmenu__add:active{background:var(--subtle)}
@@ -416,8 +417,10 @@ textarea{font:inherit}
416
417
  box-shadow:0 12px 30px -12px var(--app-shadow)}
417
418
  button.setrow{font-family:inherit}
418
419
  /* 默认引擎/模型选择器:左列「自动」与正在浏览的引擎不能都画成选中态(两个浅蓝块会让人
419
- 分不清哪个是已保存的默认)。is-on = 已保存;is-cur = 右列此刻在浏览谁。 */
420
- .pick__citem.is-cur{background:var(--subtle)}
420
+ 分不清哪个是已保存的默认)。is-on = 已保存(实底);is-cur = 右列此刻在浏览谁。
421
+ 浏览态**不铺底**,只留一道左侧细条 + 主色文字 —— 有底色就会被读成「第二个选中项」。 */
422
+ .pick__citem.is-cur{box-shadow:inset 2px 0 0 var(--primary-action)}
423
+ .pick__citem.is-cur .pick__cl{color:var(--primary-action)}
421
424
 
422
425
  /* ══════════ 发现:插件目录 ══════════
423
426
  两项 magic-* 工程的 CLI(magic-shorts / magic-video)。数据来自桌面端 `/desk/plugins`:
@@ -461,6 +464,20 @@ button.setrow{font-family:inherit}
461
464
  .setrow__v{flex:0 0 auto;font-size:14px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:50%}
462
465
  .setrow--danger{color:var(--error-text)}
463
466
  .setrow--danger .setrow__l,.setrow--danger .setrow__v{color:var(--error-text)}
467
+ /* 环境编辑(我的 → 环境):名称 / 地址 / 令牌三栏。
468
+ 环境不能只靠扫码「连过才记得」—— 用户要显式配置 IP 和名字(2026-10-22)。 */
469
+ .envfield{display:block;width:100%;height:44px;margin-top:6px;padding:0 12px;border-radius:10px;
470
+ background:var(--subtle);border:1px solid transparent;color:var(--ink);font-size:15px;
471
+ -webkit-appearance:none;outline:none}
472
+ .envfield:focus{border-color:var(--primary-action)}
473
+ .envfield::placeholder{color:var(--faint)}
474
+ .envlabel{display:block;margin-top:14px;font-size:12px;font-weight:700;color:var(--muted)}
475
+ .envhint{margin-top:10px;font-size:12px;line-height:1.6;color:var(--faint)}
476
+ .envacts{display:flex;gap:8px;margin-top:16px}
477
+ .envacts .btn{flex:1;margin-top:0}
478
+ .envdel{margin-top:14px;width:100%;height:40px;border-radius:var(--r-full);
479
+ color:var(--error-text);font-size:14px;font-weight:600}
480
+ .envdel:active{background:var(--subtle)}
464
481
  /* 两行式配置行(团队模式):左列是「名字 + 这条适合什么」,右列是引擎。
465
482
  规则描述必须常显 —— 用户要的是「每类适合什么」一眼可见,而不是藏进详情页。 */
466
483
  .setrow__main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
@@ -472,10 +489,8 @@ button.setrow{font-family:inherit}
472
489
  font:13px/1.55 -apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;-webkit-appearance:none}
473
490
  .teamedit::placeholder{color:var(--faint)}
474
491
  .teamedit__acts{display:flex;gap:8px;margin-bottom:10px}
475
- /* 团队抽屉里的分区标题(规则 / 交给谁)与行尾箭头:点开才有的配置必须看得出「这里能点」。 */
476
- .pick__cap--team{padding:10px 4px 4px;font-size:11px;font-weight:700;letter-spacing:.04em;color:var(--muted)}
492
+ /* 团队行尾箭头:点开才有的配置必须看得出「这里能点」。 */
477
493
  .setrow__chev{flex:0 0 auto;color:var(--faint);font-size:16px;line-height:1}
478
- .pick__cols--team{height:min(36vh,36dvh,300px)}
479
494
  .seg{display:inline-flex;background:var(--subtle);border-radius:var(--r-full);padding:2px;gap:2px}
480
495
  .seg__b{height:28px;padding:0 12px;border-radius:var(--r-full);font-size:13px;color:var(--muted)}
481
496
  .seg__b.is-on{background:var(--raised);color:var(--ink);font-weight:600}
@@ -495,27 +510,27 @@ button.setrow{font-family:inherit}
495
510
  · 启动页不显示底栏(showLauncher 里 hidden),无需留白
496
511
 
497
512
  min-height 而不是 height:底栏还要吞 safe-area(34px),
498
- border-box 下固定 height 会让内部 tab 被压到 26px 高、图标文字挤成一团。
499
- **74**(2026-10-22 定稿):68 时图标 23 + 选中浅底 28 + 标签 12 ≈ 50,上下只剩 18px;
500
- 74 给到上下各约 21px,选中浅底才有完整的一圈呼吸,不会被栏边切掉。 */
513
+ border-box 下固定 height 会让内部 tab 被压到 26px 高。
514
+ **56**(2026-10-22 定稿「底栏只保留图标不需要文字了」):
515
+ 去掉标签后只剩 25px 图标 + 44×34 选中浅底,56 给到浅底上下各 11px,
516
+ 栏体也矮下来 —— 对话区因此多出 18px。 */
501
517
  .tabbar{position:absolute;left:0;right:0;bottom:0;z-index:60;
502
518
  display:flex;align-items:stretch;
503
- min-height:74px;
519
+ min-height:56px;
504
520
  background:color-mix(in srgb,var(--raised) 86%,transparent);
505
521
  -webkit-backdrop-filter:blur(18px) saturate(1.4);backdrop-filter:blur(18px) saturate(1.4);
506
522
  box-shadow:0 -1px 0 var(--border),0 -14px 30px -26px var(--app-shadow);
507
523
  padding-bottom:env(safe-area-inset-bottom)}
508
- .tab{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;
509
- padding:6px 0;
510
- color:var(--muted);font-size:10.5px;letter-spacing:.02em;transition:color .16s}
524
+ /* 只有图标:文案从底栏拿掉(2026-10-22 用户),语义交给 aria-label,不靠视觉标签。
525
+ 命中区仍是 flex:1 × 56,比"图标 + 文字"那版更高更宽,点起来更稳。 */
526
+ .tab{flex:1;display:flex;align-items:center;justify-content:center;
527
+ color:var(--muted);transition:color .16s}
511
528
  .tab.is-on{color:var(--primary-action)}
512
- /* 选中态不只换颜色,还给一块 38×28 的浅底(`--activity` 是现成的品牌色低透明令牌):
513
- 颜色是给"看得清"的,形状是给"摸得到"的 —— 暗色下主色是浅蓝,光靠颜色区分太弱。 */
514
- .tab .icw{display:grid;place-items:center;width:38px;height:28px;border-radius:9px;
529
+ /* 选中态不只换颜色,还给一块 44×34 的浅底(`--activity` 是现成的品牌色低透明令牌):
530
+ 没有文字之后,颜色是唯一的语义线索,必须再补一个形状 —— 暗色下主色是浅蓝,光靠颜色区分太弱。 */
531
+ .tab .icw{display:grid;place-items:center;width:44px;height:34px;border-radius:11px;
515
532
  transition:background .18s ease}
516
533
  .tab.is-on .icw{background:var(--activity)}
517
- .tab .lb{line-height:1.15;transition:font-weight .16s}
518
- .tab.is-on .lb{font-weight:600}
519
534
  .tab svg{display:block;transition:transform .16s}
520
535
  .tab:active svg{transform:scale(.92)}
521
536
  /* ── 「快速新建」:**半嵌入**底栏的圆形主按钮(2026-10-22 定稿)
@@ -533,42 +548,43 @@ button.setrow{font-family:inherit}
533
548
  于是看起来像按钮自带发光轮廓。
534
549
 
535
550
  实现:
536
- ① **洞**:`::before` 是一块悬在栏顶边界内的半圆(宽 38*2 / 高 38,下两角半径 38),
551
+ ① **洞**:`::before` 是一块悬在栏顶边界内的半圆(宽 74 / 高 37,下两角半径 37),
537
552
  填槽底 `--fab-well`,在底栏上边缘切出一个半圆缺口。
538
- 高度只到 38,**不侵入上方内容**(试过 78px 整圆,会盖住列表底部)。
539
- ② **嵌**:按钮直径 60,圆心落在栏顶边线上(上露 30 / 下沉 30),
553
+ 高度只到 37,**不侵入上方内容**(试过整圆深度,会盖住列表底部)。
554
+ ② **嵌**:按钮直径 68,圆心落在栏顶边线上(上露 34 / 下沉 34),
540
555
  于是下半实打实坐进洞里。圆心压边线而不是抬高,是「嵌」与「放」的分界。
541
- ③ **接触影**:`radial-gradient` 把暗环**锚定在 r=30(按钮半径)**上,
542
- 从 29.5px 起最暗、到 33px 淡完。这是唯一能精确控制「暗在哪一圈」的写法 ——
556
+ ③ **接触影**:`radial-gradient` 把暗环**锚定在 r=34(按钮半径)**上,
557
+ 从 33.5px 起最暗、到 37px 淡完。这是唯一能精确控制「暗在哪一圈」的写法 ——
543
558
  `box-shadow: inset` 的暗边永远从元素边界向内,管不到元素外的那道槽。
544
- 实测梯度(正下方):浅色 189→237→255,暗色 21→27→31,两端都单调、无回亮。
545
- 几何全用相对量(`bottom:calc(100% - 30px)`、`top:0`),安全区变化时洞和按钮一起走。 */
546
- .tabfab{flex:0 0 76px}
559
+ 几何全用相对量(`bottom:calc(100% - 34px)`、`top:0`),安全区变化时洞和按钮一起走。
560
+
561
+ **两页一致**:会话页不再把「+」收进栏内(2026-10-22 用户:「圆没有突出了 …两个效果要一致」)。
562
+ 凸出的圆在列表页和对话页长得完全一样,只用 composer 的下内边距给它让出空间。 */
563
+ .tabfab{flex:0 0 84px}
547
564
  /* 洞 = 半圆 + 接触影。
548
565
  槽底是 `--fab-well`(栏面稍暗一档的**同色系**),不是 `--canvas`:
549
566
  用页面底色在暗色下会成黑箍,在浅色下又几乎看不见 —— 两头都不对。
550
567
  接触影必须画在槽底**之上**,且暗环两端用硬锚点,
551
568
  避免模糊半径把亮度峰值推到按钮外面(那正是前几版读成光晕的原因)。
552
569
 
553
- **露出的槽宽只有 3px**(洞半径 33 − 按钮半径 30)。曾经用 38(露 8px):
554
- 那圈灰色槽底读作「按钮下面垫了个盘子」,正是用户否掉的形态。
555
- 3px 只够出一条接触缝 —— 看得见槽、看不清槽底,就等于"切出来的"。
556
- 洞的高也取 33 而不是更深:槽只要在栏顶边界内切出缺口,深了只是浪费。 */
570
+ **露出的槽宽只有 3px**(洞半径 37 − 按钮半径 34)。曾经用更大:那圈灰色槽底读作「按钮下面垫了个盘子」,
571
+ 正是用户否掉的形态。3px 只够出一条接触缝 —— 看得见槽、看不清槽底,就等于"切出来的"。
572
+ 洞的高也取 37 而不是更深:槽只要在栏顶边界内切出缺口,深了只是浪费。 */
557
573
  .tabbar::before{content:"";position:absolute;z-index:1;pointer-events:none;
558
574
  left:50%;top:0;transform:translateX(-50%);
559
- width:66px;height:33px;
560
- border-radius:0 0 33px 33px;
575
+ width:74px;height:37px;
576
+ border-radius:0 0 37px 37px;
561
577
  background:
562
- radial-gradient(circle 33px at 50% 0,
563
- var(--fab-notch) 29.5px,
564
- var(--fab-notch) 30.5px,
565
- color-mix(in srgb,var(--fab-notch) 35%,transparent) 32px,
566
- transparent 33px),
578
+ radial-gradient(circle 37px at 50% 0,
579
+ var(--fab-notch) 33.5px,
580
+ var(--fab-notch) 34.5px,
581
+ color-mix(in srgb,var(--fab-notch) 35%,transparent) 36px,
582
+ transparent 37px),
567
583
  var(--fab-well)}
568
584
  .tabfab__b{position:absolute;z-index:2;
569
- left:50%;bottom:calc(100% - 30px); /* 圆心钉在栏顶边线上:上露 30 / 下沉 30 */
585
+ left:50%;bottom:calc(100% - 34px); /* 圆心钉在栏顶边线上:上露 34 / 下沉 34 */
570
586
  transform:translateX(-50%);
571
- width:60px;height:60px;
587
+ width:68px;height:68px;
572
588
  display:flex;align-items:center;justify-content:center;
573
589
  color:var(--on-primary-action);
574
590
  background:linear-gradient(178deg,rgba(255,255,255,.14),rgba(255,255,255,0) 60%),var(--primary-action);
@@ -581,25 +597,14 @@ button.setrow{font-family:inherit}
581
597
  .tabfab__b:active{transform:translateX(-50%) translateY(2px);
582
598
  box-shadow:inset 0 -6px 12px -7px rgba(15,23,42,.56), inset 0 2px 3px -2px rgba(255,255,255,.26)}
583
599
  .tabfab__b svg{display:block;stroke-width:2.4;transform:translateY(-1px)}
584
- /* 会话页(2026-10-22 用户:「不要挤压对话框」「tab 跟会话列表一样是浮在上面」):
585
- 底栏浮起后不再占位,composer 只需让出「栏高 + 安全区 + 12px 呼吸」——
586
- 输入框贴着栏上沿 12px。原来 composer 下留 60(= +上探 30 + 呼吸 30),再叠上占位的 68 栏,
587
- 屏幕底到输入框一共 162;现在 74 + 34 + 12 = 120,对话区多出 42px,也再没有那段死区。
588
- 74 必须与 .tabbar 的 min-height 对齐(改栏高要一起改)。
600
+ /* 会话页(2026-10-22 用户:「不要挤压对话框」「两个效果要一致」「圆可以更大」):
601
+ 底栏浮起后不占位,「+」在**两页都凸出 34**(68 直径的一半),所以 composer 只需让出
602
+ 「上探 34 + 呼吸 12 + 栏高 56」—— 输入框落在凸出圆的头顶 12px,不遮不压。
603
+ 原来 composer 下留 60(= 上探 30 + 呼吸 30)再叠占位的 68 栏,屏幕底到输入框 162;
604
+ 现在 56 + 34 + 12 = 102,对话区多出 60px。
605
+ 56 必须与 .tabbar 的 min-height 对齐(改栏高要一起改)。
589
606
  **只叠一次**:safe-area 由这里一次性算进来,别再给 .composer 的基础 padding 加一遍。 */
590
- #conv .composer{padding-bottom:calc(12px + 74px + env(safe-area-inset-bottom))}
591
-
592
- /* 会话页的「+」收进槽里(不上浮):
593
- 输入框离栏只剩 12px,骑在边线上的 + 会顶到输入框下沿;而且这一屏已经有 composer
594
- 左侧的附件「+」,两个浮起的圆会让"哪个是新建"含混。
595
- 只改**高度关系**,不改颜色 / 水平位置:圆仍钉在正中,从"骑在边线上"变成"坐在栏里"。
596
- 直径 60 → 56 是跟着 74 的栏高走的(上下各留 9px),不是"缩小来躲"。 */
597
- body.is-conv .tabbar::before{display:none}
598
- body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-bottom) + 9px);
599
- box-shadow:
600
- inset 0 -6px 11px -6px rgba(15,23,42,.42),
601
- inset 0 2px 2px -2px rgba(255,255,255,.28),
602
- 0 6px 14px -8px rgba(15,23,42,.35)}
607
+ #conv .composer{padding-bottom:calc(12px + 34px + 56px + env(safe-area-inset-bottom))}
603
608
 
604
609
  /* 组件挂载点:占满剩余高度 */
605
610
  #conv{flex:1;min-height:0;display:flex}
@@ -906,6 +911,8 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
906
911
  <div class="history__list">
907
912
  <div class="setcard">
908
913
  <div class="setrow"><span class="setrow__l">设备</span><span class="setrow__v" id="mineDevice">—</span></div>
914
+ <!-- 环境(多连接):显式配置每台桌面的名字 / IP / 令牌,并在这里切换 -->
915
+ <button class="setrow" id="btnEnvPage"><span class="setrow__l">环境</span><span class="setrow__v" id="mineEnv">—</span></button>
909
916
  <div class="setrow"><span class="setrow__l">主题</span>
910
917
  <span class="seg" id="segTheme">
911
918
  <button class="seg__b" data-v="system">系统</button>
@@ -958,19 +965,41 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
958
965
  <!-- ══════════ 我的 → 团队模式(计划 + 7 类子任务)══════════
959
966
  从「我的」那一行点进来。这里配的全都是**设备级偏好**(开关本身按会话,在会话页运行设置里)。
960
967
  **只有一套划分**:计划 + 7 个角色(writer / researcher / reviewer / coder / designer /
961
- director / builder)共 8 行,行内直接写清「这类适合什么」—— 不再并列「6 个类别」那一层
962
- (用户 2026-10-22:「只需要计划+7类子任务即可 描述好每一类适合的规则 规则、引擎都可配置」)。
963
- 每行点开 = 引擎/模型 + 规则:计划配拆解粒度与两个开关,角色配「这类任务适合什么」
964
- (写进 core 的拆解提示词,--team-rule role:<name>=…)。 -->
968
+ director / builder),不再并列「6 个类别」那一层。
969
+ 2026-10-22 用户:「配引擎和配规则要分开配置 不要在 1 个页面」——
970
+ 于是**两张卡、两个入口**:上面「交给谁」点开只有引擎/模型;下面「规则」点开只有规则
971
+ (计划 = 拆解口径,角色 = 这类任务适合什么)。两者互不掺杂。 -->
965
972
  <div class="history" id="teamPage" hidden>
966
973
  <div class="history__top">
967
974
  <button class="topbar__icon" id="btnTeamBack" aria-label="返回"><svg width="20" height="20"><use href="#i-back"/></svg></button>
968
975
  <span class="history__title">团队模式</span>
969
976
  </div>
970
977
  <div class="history__list">
971
- <div class="history__group">计划与 7 类子任务</div>
978
+ <div class="history__group">交给谁(引擎 / 模型)</div>
972
979
  <div class="setcard" id="teamRouteCard"></div>
973
- <div class="history__hint" style="text-align:left">点一行配置引擎与规则。留空 = 跟随主引擎 / 使用内置描述。</div>
980
+ <div class="history__group">规则(只影响计划怎么拆)</div>
981
+ <div class="setcard" id="teamRuleCard"></div>
982
+ <div class="history__hint" style="text-align:left">两张卡分开配:上面只选引擎,下面只改规则。留空 = 跟随主引擎 / 使用内置规则。</div>
983
+ </div>
984
+ </div>
985
+
986
+ <!-- ══════════ 我的 → 环境(多连接:名字 / IP / 令牌)══════════
987
+ 2026-10-22 用户:「环境没有地方配置 ip 和名字啊」。
988
+ 环境不能只靠「连过才记得」:这里显式新建 / 改名 / 改地址 / 删,点一行就编辑。
989
+ 地址填 IP:端口即可(不带 http:// 自动补),令牌可以写在地址的 ?token= 里,
990
+ 也可以单独填下面那栏(显式令牌优先)。 -->
991
+ <div class="history" id="envPage" hidden>
992
+ <div class="history__top">
993
+ <button class="topbar__icon" id="btnEnvBack" aria-label="返回"><svg width="20" height="20"><use href="#i-back"/></svg></button>
994
+ <span class="history__title">环境</span>
995
+ </div>
996
+ <div class="history__list">
997
+ <div class="history__group">已配置的桌面</div>
998
+ <div class="setcard" id="envList"></div>
999
+ <div class="history__actions" style="padding:10px 0 0">
1000
+ <button class="newtask" id="btnEnvAdd" type="button"><svg width="18" height="18"><use href="#i-plus"/></svg><span>新建环境</span></button>
1001
+ </div>
1002
+ <div class="history__hint" style="text-align:left">环境 = 一台跑着 magic-client 的桌面。名字随你起(例如「公司 Mac」),地址填 IP:端口,例如 192.168.1.5:8791。保存后可在这里或任务页标题处切换。</div>
974
1003
  </div>
975
1004
  </div>
976
1005
 
@@ -1123,18 +1152,20 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1123
1152
  「定时任务」那个 tab 已删(2026-10-22 用户:「定时任务tab先删除」)——
1124
1153
  功能本身没删:会话菜单里的「定时任务」仍然能唤起,只是不再占一个底部入口
1125
1154
  (那一页本来也只有一句「还没有这个接口」的说明,占着导航反而误导)。
1126
- 中间那枚「+」不是 tab:它没有 data-tab、不参与 setActive,点了就是新建会话。 -->
1155
+ 中间那枚「+」不是 tab:它没有 data-tab、不参与 setActive,点了就是新建会话。
1156
+ **只有图标、没有文字**(2026-10-22 用户:「底栏只保留图标不需要文字了」):
1157
+ 语义靠 aria-label 承担;图标本身已用"清单 / 文档"的轮廓分家,缩到 25px 仍可辨。 -->
1127
1158
  <nav class="tabbar" id="tabbar" hidden>
1128
- <button class="tab" type="button" data-tab="tasks"><span class="icw"><svg width="23" height="23"><use href="#i-task"/></svg></span><span class="lb">任务</span></button>
1159
+ <button class="tab" type="button" data-tab="tasks" aria-label="任务"><span class="icw"><svg width="25" height="25"><use href="#i-task"/></svg></span></button>
1129
1160
  <!-- 需求 tab 常驻:不做能力位控制(用户 2026-10-22:「移动端永远显示」)。
1130
1161
  老 core 上点进来会由 loadRequirements 的 501 分支给出明确文案,
1131
1162
  不会是一片空白 —— 入口固定比入口忽隐忽现更好理解。 -->
1132
- <button class="tab" type="button" data-tab="reqs"><span class="icw"><svg width="23" height="23"><use href="#i-req"/></svg></span><span class="lb">需求</span></button>
1163
+ <button class="tab" type="button" data-tab="reqs" aria-label="需求"><span class="icw"><svg width="25" height="25"><use href="#i-req"/></svg></span></button>
1133
1164
  <span class="tabfab">
1134
- <button class="tabfab__b" type="button" id="btnFabNew" aria-label="新建会话"><svg width="26" height="26"><use href="#i-plus"/></svg></button>
1165
+ <button class="tabfab__b" type="button" id="btnFabNew" aria-label="新建会话"><svg width="30" height="30"><use href="#i-plus"/></svg></button>
1135
1166
  </span>
1136
- <button class="tab" type="button" data-tab="discover"><span class="icw"><svg width="23" height="23"><use href="#i-compass"/></svg></span><span class="lb">发现</span></button>
1137
- <button class="tab" type="button" data-tab="mine"><span class="icw"><svg width="23" height="23"><use href="#i-user"/></svg></span><span class="lb">我的</span></button>
1167
+ <button class="tab" type="button" data-tab="discover" aria-label="发现"><span class="icw"><svg width="25" height="25"><use href="#i-compass"/></svg></span></button>
1168
+ <button class="tab" type="button" data-tab="mine" aria-label="我的"><span class="icw"><svg width="25" height="25"><use href="#i-user"/></svg></span></button>
1138
1169
  </nav>
1139
1170
  <!-- 第三方库归口 shared/vendor(与内核共用一份):markdown 渲染栈要用 marked + DOMPurify;
1140
1171
  highlight.js 由内核按需加载,地址在下面给(内核自己不知道页面的相对根;
@@ -1185,6 +1216,9 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1185
1216
  var taskSearchInput = document.getElementById('taskSearchInput');
1186
1217
  var mineEl = document.getElementById('mine');
1187
1218
  var mineDevice = document.getElementById('mineDevice');
1219
+ var mineEnv = document.getElementById('mineEnv');
1220
+ var envPageEl = document.getElementById('envPage');
1221
+ var envListEl = document.getElementById('envList');
1188
1222
  var mineVersion = document.getElementById('mineVersion');
1189
1223
  var segTheme = document.getElementById('segTheme');
1190
1224
  var segFont = document.getElementById('segFont');
@@ -1250,7 +1284,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1250
1284
  老 core 没有这个字段 → 空 → 回落到下面这份内置表(与 internal/team 一致)。 */
1251
1285
  var TEAM_ROLE_LABELS = {
1252
1286
  writer: '写作', researcher: '调研', reviewer: '评审', coder: '编码',
1253
- designer: '视觉', director: '影像', builder: '工程'
1287
+ designer: '视觉', director: '影像', builder: '游戏'
1254
1288
  };
1255
1289
  var TEAM_ROLE_BUILTIN = [
1256
1290
  { name: 'writer', kind: 'text', duty: '把材料写成结构清晰、可直接交付的文档' },
@@ -1259,7 +1293,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1259
1293
  { name: 'coder', kind: 'code', duty: '把复杂代码改动落到可构建、可测试的文件上' },
1260
1294
  { name: 'designer', kind: 'image', duty: '产出可直接使用的画面资产' },
1261
1295
  { name: 'director', kind: 'video', duty: '把脚本 / 分镜变成可播放的影像' },
1262
- { name: 'builder', kind: 'game', duty: '交付能跑起来的工程与可复现的启动方式' }
1296
+ { name: 'builder', kind: 'game', duty: '游戏内玩法 / 系统开发,交付能跑起来、可复现启动的工程' }
1263
1297
  ];
1264
1298
  var TEAM_ROLES = []; // core 上报的角色清单;空 = 老 core(或还没连上)→ 用内置表
1265
1299
  function teamRoleList() { return TEAM_ROLES.length ? TEAM_ROLES : TEAM_ROLE_BUILTIN; }
@@ -1359,6 +1393,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1359
1393
  var CONN_KEY = 'magic.remote.conns.v1', CONN_MAX = 8;
1360
1394
  var LS_KEY = 'magic.remote.recent.v1'; // 旧版「最近连接」:只在首次升级时搬一次
1361
1395
  var ACTIVE_ID = '';
1396
+ var CONN_SEQ = 0; // 每连一次 +1:上一台桌面迟到的响应不许画到这一台上面
1362
1397
 
1363
1398
  function connHost(url) {
1364
1399
  try { return new URL(url).host; }
@@ -1380,6 +1415,8 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1380
1415
  host: connHost(c.url), app: '', at: c.at || 0 };
1381
1416
  });
1382
1417
  saveConns(list);
1418
+ /* 搬完就删旧键:否则用户「清空」后新列表为空,下次又会被旧记录搬回来。 */
1419
+ try { localStorage.removeItem(LS_KEY); } catch (e) {}
1383
1420
  }
1384
1421
  } catch (e) {}
1385
1422
  }
@@ -1408,7 +1445,10 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1408
1445
  return next;
1409
1446
  }
1410
1447
  function removeConn(id) { saveConns(loadConns().filter(function (c) { return c.id !== id; })); }
1411
- function clearConns() { saveConns([]); }
1448
+ function clearConns() {
1449
+ saveConns([]);
1450
+ try { localStorage.removeItem(LS_KEY); } catch (e) {}
1451
+ }
1412
1452
 
1413
1453
  /* 当前连接:先按地址 + 令牌找;App 壳注入的连接还没落盘时给一条临时记录,
1414
1454
  这样标题与菜单里仍然看得见「现在连的是谁」。 */
@@ -1467,7 +1507,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1467
1507
  del.type = 'button';
1468
1508
  del.className = 'recent__del';
1469
1509
  del.setAttribute('aria-label', '删除连接');
1470
- del.appendChild(svgUse('i-trash', 18));
1510
+ del.appendChild(svgUse('#i-trash', 18));
1471
1511
  del.onclick = function () { removeConn(item.id); renderRecent(); };
1472
1512
  row.appendChild(b); row.appendChild(del);
1473
1513
  recentListEl.appendChild(row);
@@ -1520,7 +1560,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1520
1560
  b.className = 'envmenu__item';
1521
1561
  var ico = document.createElement('span');
1522
1562
  ico.className = 'envmenu__ico';
1523
- ico.appendChild(svgUse('i-laptop', 20));
1563
+ ico.appendChild(svgUse('#i-laptop', 20));
1524
1564
  b.appendChild(ico);
1525
1565
  var col = document.createElement('span');
1526
1566
  col.className = 'envmenu__col';
@@ -1529,25 +1569,35 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1529
1569
  tl.appendChild(elSpan('envmenu__name', connLabel(c) || c.host || '未命名'));
1530
1570
  if (c.app) tl.appendChild(elSpan('envmenu__badge', c.app));
1531
1571
  col.appendChild(tl);
1532
- col.appendChild(elSpan('envmenu__s', (c.host || connHost(c.url)) + (active ? ' · 当前' : '')));
1572
+ /* 名字就是 host 时不重复写一遍:副标题只留「地址(有名字时)+ 状态」。 */
1573
+ var cname = connLabel(c) || c.host || '';
1574
+ var chost = c.host || connHost(c.url);
1575
+ var csub = (cname && cname !== chost) ? chost : '';
1576
+ col.appendChild(elSpan('envmenu__s', (csub ? csub + ' · ' : '') + (active ? '当前连接' : '点击切换')));
1533
1577
  b.appendChild(col);
1534
1578
  if (active) {
1535
1579
  var ck = document.createElement('span');
1536
1580
  ck.className = 'envmenu__check';
1537
- ck.appendChild(svgUse('i-check', 18));
1581
+ ck.appendChild(svgUse('#i-check', 18));
1538
1582
  b.appendChild(ck);
1539
1583
  }
1540
1584
  b.onclick = function () { closeEnvMenu(); switchEnv(c); };
1541
1585
  envMenuEl.appendChild(b);
1542
1586
  });
1543
1587
  envMenuEl.appendChild(elSpan('envmenu__sep', ''));
1544
- var add = document.createElement('button');
1545
- add.type = 'button';
1546
- add.className = 'envmenu__add';
1547
- add.appendChild(svgUse('i-plus', 18));
1548
- add.appendChild(elSpan('', '添加连接'));
1549
- add.onclick = function () { closeEnvMenu(); showLauncher(); };
1550
- envMenuEl.appendChild(add);
1588
+ /* 两个入口必须分开:新建环境 = 手填名字 + IP;扫码/粘贴 = 用配对页给的链接直接连。
1589
+ 以前只有后者,「环境没有地方配置 ip 和名字」(2026-10-22 用户)。 */
1590
+ function menuAct(label, fn) {
1591
+ var b = document.createElement('button');
1592
+ b.type = 'button';
1593
+ b.className = 'envmenu__add';
1594
+ b.appendChild(svgUse('#i-plus', 18));
1595
+ b.appendChild(elSpan('', label));
1596
+ b.onclick = function () { closeEnvMenu(); fn(); };
1597
+ envMenuEl.appendChild(b);
1598
+ }
1599
+ menuAct('环境设置(名字 / IP)', function () { showEnvPage('tasks'); });
1600
+ menuAct('扫码或粘贴连接地址', function () { showLauncher(); });
1551
1601
  document.body.appendChild(envScrimEl);
1552
1602
  document.body.appendChild(envMenuEl);
1553
1603
  var btn = document.getElementById('btnEnvSwitch');
@@ -1558,6 +1608,144 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1558
1608
  btn.classList.add('is-open');
1559
1609
  }
1560
1610
 
1611
+ /* ── 环境管理(我的 → 环境):显式配置名字 / IP / 令牌 ──
1612
+ 2026-10-22 用户:「环境没有地方配置 ip 和名字啊」——
1613
+ 扫码只是「连上并记住」,不是配置入口。这里补上增删改。 */
1614
+ function showEnvPage(from) {
1615
+ hideAll(); envPageEl.hidden = false; tabbarEl.hidden = true;
1616
+ ENV_BACK = from === 'tasks' ? 'tasks' : 'mine';
1617
+ renderEnvPage();
1618
+ }
1619
+ var ENV_BACK = 'mine'; // 环境页的返回目标:从任务页标题进来就回任务页,从「我的」进来就回「我的」
1620
+ function renderEnvPage() {
1621
+ if (!envListEl) return;
1622
+ envListEl.innerHTML = '';
1623
+ var list = loadConns();
1624
+ if (!list.length) {
1625
+ envListEl.appendChild(hEl('div', 'history__hint', '还没有配置环境。点下面的「新建环境」,填名字和 IP 即可。'));
1626
+ return;
1627
+ }
1628
+ var cur = activeConn();
1629
+ list.forEach(function (c) {
1630
+ var b = hEl('button', 'setrow');
1631
+ b.type = 'button';
1632
+ var main = hEl('span', 'setrow__main');
1633
+ main.appendChild(hEl('span', 'setrow__l', connLabel(c) || '未命名'));
1634
+ main.appendChild(hEl('span', 'setrow__sub',
1635
+ (c.host || connHost(c.url)) + (cur && c.id === cur.id ? ' · 当前连接' : '')));
1636
+ b.appendChild(main);
1637
+ b.appendChild(hEl('span', 'setrow__v', '编辑'));
1638
+ b.onclick = function () { openEnvEditor(c); };
1639
+ envListEl.appendChild(b);
1640
+ });
1641
+ }
1642
+ /* conn 为空 = 新建;否则编辑(名字、地址、令牌都能改,也能删)。 */
1643
+ function openEnvEditor(conn) {
1644
+ var isNew = !conn;
1645
+ openHostSheet(isNew ? '新建环境' : '编辑环境', function (body) {
1646
+ var nameEl = hEl('input', 'envfield');
1647
+ nameEl.type = 'text';
1648
+ nameEl.placeholder = '例如 公司 Mac';
1649
+ nameEl.value = (conn && conn.name) || '';
1650
+ var addrEl = hEl('input', 'envfield');
1651
+ addrEl.type = 'text';
1652
+ addrEl.placeholder = '192.168.1.5:8791';
1653
+ addrEl.setAttribute('autocapitalize', 'off');
1654
+ addrEl.setAttribute('autocomplete', 'off');
1655
+ addrEl.setAttribute('spellcheck', 'false');
1656
+ /* 地址栏显示不带 http:// 的短形态(用户脑子里就是 IP:端口);https 原样保留。 */
1657
+ addrEl.value = conn && conn.url ? String(conn.url).replace(/^http:\/\//i, '') : '';
1658
+ var tokEl = hEl('input', 'envfield');
1659
+ tokEl.type = 'text';
1660
+ tokEl.placeholder = '可选,配对令牌';
1661
+ tokEl.setAttribute('autocapitalize', 'off');
1662
+ tokEl.setAttribute('autocomplete', 'off');
1663
+ tokEl.setAttribute('spellcheck', 'false');
1664
+ tokEl.value = (conn && conn.token) || '';
1665
+ body.appendChild(hEl('label', 'envlabel', '名称'));
1666
+ body.appendChild(nameEl);
1667
+ body.appendChild(hEl('label', 'envlabel', '地址(IP:端口)'));
1668
+ body.appendChild(addrEl);
1669
+ body.appendChild(hEl('label', 'envlabel', '令牌'));
1670
+ body.appendChild(tokEl);
1671
+ var err = hEl('div', 'field__err');
1672
+ err.hidden = true;
1673
+ body.appendChild(err);
1674
+ body.appendChild(hEl('div', 'envhint',
1675
+ '地址也可以带 ?token=…;下面这栏单独填时优先。填好保存后可在这里或任务页标题处切换。'));
1676
+ var acts = hEl('div', 'envacts');
1677
+ var saveB = hEl('button', 'btn btn--filled', '保存');
1678
+ var goB = hEl('button', 'btn btn--outlined', '保存并连接');
1679
+ saveB.type = 'button'; goB.type = 'button';
1680
+ acts.appendChild(saveB); acts.appendChild(goB);
1681
+ body.appendChild(acts);
1682
+ var delB = null;
1683
+ if (!isNew) {
1684
+ delB = hEl('button', 'envdel', '删除这个环境');
1685
+ delB.type = 'button';
1686
+ body.appendChild(delB);
1687
+ }
1688
+
1689
+ function collect() {
1690
+ err.hidden = true;
1691
+ var raw = String(addrEl.value || '').trim();
1692
+ var t = parseTarget(raw);
1693
+ if (!t) { err.textContent = '请填地址,例如 192.168.1.5:8791'; err.hidden = false; return null; }
1694
+ var tok = String(tokEl.value || '').trim();
1695
+ var hadParam = /[?&]token=/.test(raw);
1696
+ if (tok) t.token = tok;
1697
+ else if (!hadParam) {
1698
+ /* 令牌栏没动过就沿用旧的;用户主动清空 = 就是要去掉令牌,不能又塞回去。 */
1699
+ t.token = (conn && tokEl.value === (conn.token || '')) ? (conn.token || '') : '';
1700
+ }
1701
+ t.name = String(nameEl.value || '').trim();
1702
+ return t;
1703
+ }
1704
+ function persist(t) {
1705
+ var wasActive = !!(conn && conn.url === BASE && String(conn.token || '') === String(TOKEN || ''));
1706
+ var next = upsertConn(t);
1707
+ /* 改了地址/令牌 = 换了一条记录:旧的那条要删掉,别留一条连不上的僵尸。 */
1708
+ if (conn && conn.id !== next.id) removeConn(conn.id);
1709
+ /* 名称允许清空,也允许改:upsertConn 会沿用旧名,这里按输入框的最终值覆盖。 */
1710
+ var list = loadConns();
1711
+ for (var i = 0; i < list.length; i++) {
1712
+ if (list[i].id !== next.id) continue;
1713
+ list[i].name = t.name;
1714
+ if (!list[i].app && conn && conn.app) list[i].app = conn.app;
1715
+ }
1716
+ saveConns(list);
1717
+ closeHostSheet();
1718
+ if (wasActive && (next.url !== BASE || String(next.token || '') !== String(TOKEN || ''))) {
1719
+ switchEnv(next); // 改的正是当前这台:按新地址/令牌重连
1720
+ return;
1721
+ }
1722
+ renderEnvPage();
1723
+ if (!historyEl.hidden) historyTitle.textContent = envLabel() || '任务';
1724
+ if (!mineEl.hidden && mineEnv) mineEnv.textContent = envLabel() || '—';
1725
+ toast(isNew ? ('已添加「' + (next.name || next.host) + '」') : '已保存');
1726
+ }
1727
+ saveB.onclick = function () { var t = collect(); if (t) persist(t); };
1728
+ goB.onclick = function () {
1729
+ var t = collect();
1730
+ if (!t) return;
1731
+ persist(t);
1732
+ /* persist 内部遇到「改的是当前连接」已经自己重连了;这里只处理新增/改别的环境。 */
1733
+ var c = findConn(connIdOf(t)) || findConn(connIdOf({ url: t.url, token: t.token }));
1734
+ if (c && !(c.url === BASE && String(c.token || '') === String(TOKEN || ''))) switchEnv(c);
1735
+ };
1736
+ if (delB) {
1737
+ delB.onclick = function () {
1738
+ if (!window.confirm('删除环境「' + (connLabel(conn) || conn.host || '') + '」?')) return;
1739
+ removeConn(conn.id);
1740
+ closeHostSheet();
1741
+ renderEnvPage();
1742
+ toast('已删除');
1743
+ };
1744
+ }
1745
+ setTimeout(function () { try { nameEl.focus(); } catch (e) {} }, 60);
1746
+ });
1747
+ }
1748
+
1561
1749
  /* ══════════ 解析 + 校验连接地址 ══════════
1562
1750
  与 ZCode RemoteControlUrlPolicy 同职责:只放行 http(s),解析出 base 与令牌。
1563
1751
  ZCode 生产包写死 zcode.z.ai;magic-agent 是用户自托管(LAN IP 各不相同),
@@ -1585,8 +1773,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1585
1773
  }
1586
1774
 
1587
1775
  /* ══════════ 视图:启动页 → 会话列表 → 会话 ══════════ */
1588
- /* 离开会话页要摘掉 is-conv,否则底栏的「+」会一直保持"收进槽里"的形态 */
1589
- function hideAll() { document.body.classList.remove('is-conv'); launcherEl.hidden = true; historyEl.hidden = true; mineEl.hidden = true; reqsEl.hidden = true; discoverEl.hidden = true; teamPageEl.hidden = true; runSettingsPageEl.hidden = true; pluginsEl.hidden = true; pluginDetailEl.hidden = true; sessionEl.hidden = true; }
1776
+ function hideAll() { launcherEl.hidden = true; historyEl.hidden = true; mineEl.hidden = true; reqsEl.hidden = true; discoverEl.hidden = true; teamPageEl.hidden = true; runSettingsPageEl.hidden = true; pluginsEl.hidden = true; pluginDetailEl.hidden = true; sessionEl.hidden = true; envPageEl.hidden = true; }
1590
1777
  function setActive(name) {
1591
1778
  Array.prototype.forEach.call(tabbarEl.querySelectorAll('.tab'), function (b) {
1592
1779
  b.classList.toggle('is-on', b.getAttribute('data-tab') === name);
@@ -1604,7 +1791,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1604
1791
  historyTitle.textContent = envLabel() || '任务';
1605
1792
  loadSessions();
1606
1793
  }
1607
- function showSession(sub) { hideAll(); document.body.classList.add('is-conv'); sessionEl.hidden = false; tabbarEl.hidden = false; setActive('tasks'); subtitleEl.textContent = (sub != null ? sub : histSubtitle) || ''; }
1794
+ function showSession(sub) { hideAll(); sessionEl.hidden = false; tabbarEl.hidden = false; setActive('tasks'); subtitleEl.textContent = (sub != null ? sub : histSubtitle) || ''; }
1608
1795
  /* 需求页:进页面即拉一次清单(GET /desk/requirements,只读、不调模型)。
1609
1796
  「整理」按钮才触发归纳(POST /desk/requirements/consolidate)——
1610
1797
  两者代价差三个数量级,绝不能混成一个动作。 */
@@ -1616,6 +1803,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1616
1803
  function showMine() {
1617
1804
  hideAll(); mineEl.hidden = false; tabbarEl.hidden = false; setActive('mine');
1618
1805
  mineDevice.textContent = histSubtitle || '本机';
1806
+ if (mineEnv) mineEnv.textContent = envLabel() || '—';
1619
1807
  var ns = document.getElementById('notifyState');
1620
1808
  if (ns) { var p = notifyPermState(); ns.textContent = p === 'plugin' ? '已接入' : (p === 'granted' ? '已开启' : '未开启'); }
1621
1809
  markSeg(segTheme, PREFS.theme); markSeg(segFont, PREFS.font); markSeg(segMode, TASK_MODE);
@@ -1643,9 +1831,12 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1643
1831
  function refreshMineVersion() {
1644
1832
  if (!TOKEN) { paintMineVersion(); return; }
1645
1833
  var hdr = { 'X-Magic-Token': TOKEN };
1834
+ var seq = CONN_SEQ; // 切环境后旧桌面的 health 不许写到新连接上
1646
1835
  fetch(BASE + '/desk/health', { headers: hdr })
1647
1836
  .then(function (r) { return r.ok ? r.json() : null; })
1648
1837
  .then(function (h) {
1838
+ if (seq !== CONN_SEQ) return;
1839
+ if (h && h.app) noteActiveMeta({ app: h.app });
1649
1840
  if (h && typeof h.version === 'string' && h.version) { SERVER_VERSION = h.version; return; }
1650
1841
  return fetch(BASE + '/desk/contract', { headers: hdr })
1651
1842
  .then(function (r) { return r.ok ? r.json() : null; })
@@ -2214,6 +2405,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
2214
2405
  function connect(target) {
2215
2406
  BASE = target.url || '';
2216
2407
  TOKEN = target.token || '';
2408
+ CONN_SEQ++; // 上一台桌面还在飞的请求:回来时直接丢弃(见 loadSessions)
2217
2409
  var c = upsertConn(target);
2218
2410
  ACTIVE_ID = c.id;
2219
2411
  showError('');
@@ -2224,6 +2416,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
2224
2416
  function disconnect() {
2225
2417
  stopPoll();
2226
2418
  closeEnvMenu();
2419
+ CONN_SEQ++;
2227
2420
  if (conv) { try { conv.destroy(); } catch (e) {} conv = null; }
2228
2421
  convEl.innerHTML = '';
2229
2422
  BASE = ''; TOKEN = '';
@@ -2273,9 +2466,11 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
2273
2466
  if (historyEl.hidden) return; // 只在这张页可见时拉
2274
2467
  if (!TOKEN) return; // 还没配对,拉也拉不到
2275
2468
  var hdr = TOKEN ? { 'X-Magic-Token': TOKEN } : {};
2469
+ var seq = CONN_SEQ;
2276
2470
  fetch(BASE + '/desk/sessions', { headers: hdr })
2277
2471
  .then(function (r) { return r.ok ? r.json() : null; })
2278
2472
  .then(function (list) {
2473
+ if (seq !== CONN_SEQ) return; // 响应回来时已经切到另一台桌面了
2279
2474
  if (!Array.isArray(list)) return;
2280
2475
  if (sessionSig(list) === sessionSig(LAST_SESSIONS)) return; // 没变 → 不动 DOM
2281
2476
  loadSessions(true); // 变了才走完整刷新(含标题归纳 / 健康),且不许闪
@@ -2284,6 +2479,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
2284
2479
  }
2285
2480
  function loadSessions(quiet) {
2286
2481
  var hdr = TOKEN ? { 'X-Magic-Token': TOKEN } : {};
2482
+ var seq = CONN_SEQ; // 切环境后旧请求可能才回来:序号对不上就不许改这一台的画面
2287
2483
  /* quiet(由 startListPoll 的轮询触发):不塞「读取中…」占位、失败也不覆盖现有列表 ——
2288
2484
  轮询是**后台行为**,不能因为一次网络抖动就把用户正看着的列表换成一行错误,
2289
2485
  也不该让他每次看到整页闪一下「读取中…」(这正是 deleteSessionRow 绕开它的原因)。 */
@@ -2299,6 +2495,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
2299
2495
  var pDirs = fetch(BASE + '/desk/dirs', { headers: hdr })
2300
2496
  .then(function (r) { return r.ok ? r.json() : {}; }).catch(function () { return {}; });
2301
2497
  Promise.all([pList, pTitles, pHealth, pDirs]).then(function (a) {
2498
+ if (seq !== CONN_SEQ) return;
2302
2499
  TITLES = a[1] || {};
2303
2500
  DW = (a[2] && a[2].defaultWorkspace) || '';
2304
2501
  DW_READY = true;
@@ -2332,6 +2529,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
2332
2529
  var pending = LAST_SESSIONS.filter(function (s) { return !s.title_summarized; });
2333
2530
  pending.slice(0, TITLE_SUMMARIZE_CAP).forEach(function (s) { setTimeout(function () { ensureTitle(s); }, 0); });
2334
2531
  }).catch(function (e) {
2532
+ if (seq !== CONN_SEQ) return;
2335
2533
  if (quiet) return; // 后台轮询失败:保留现有列表(见上面的说明)
2336
2534
  historyList.innerHTML = '<div class="history__hint">读取会话失败:' + (e && e.message ? e.message : e) + '</div>';
2337
2535
  });
@@ -4059,13 +4257,14 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
4059
4257
  var sum = document.getElementById('teamSummary');
4060
4258
  if (sum) sum.textContent = custom ? (custom + ' 项已单独指定') : '跟随主引擎';
4061
4259
  }
4062
- /* 团队模式子页面 = 计划行 + 7 个角色行,全部进同一个 #teamRouteCard(只有一套路由配置)。
4063
- 每行常显「这类适合什么」(角色 = 规则 / 计划 = 拆解口径),右侧是引擎 ——
4064
- 用户要的是「描述好每一类适合的规则」,所以描述不藏进详情页。
4065
- 签名含行文案与引擎:变了才重建,避免 5s 轮询时闪。 */
4260
+ /* 团队模式子页面 = **两张卡**(用户 2026-10-22:「配引擎和配规则要分开配置 不要在 1 个页面」):
4261
+ #teamRouteCard「交给谁」= 计划 + 7 个角色的引擎/模型,点开只有引擎;
4262
+ #teamRuleCard「规则」= 计划的拆解口径 + 7 个角色的「适合什么」,点开只有规则。
4263
+ 签名含两侧文案与引擎:变了才重建,避免 5s 轮询时闪。 */
4066
4264
  function paintTeamRoles(roles) {
4067
- var card = document.getElementById('teamRouteCard');
4068
- if (!card) return;
4265
+ var routeCard = document.getElementById('teamRouteCard');
4266
+ var ruleCard = document.getElementById('teamRuleCard');
4267
+ if (!routeCard || !ruleCard) return;
4069
4268
  var show = teamSupported();
4070
4269
  var plan = (TEAM.routes || {}).plan || {};
4071
4270
  var planSub = '拆解:' + ({ coarse: '少', standard: '标准', fine: '细' }[TEAM.split] || '标准')
@@ -4078,32 +4277,51 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
4078
4277
  }).join(',');
4079
4278
  if (sig === TEAM_ROLE_SIG) return;
4080
4279
  TEAM_ROLE_SIG = sig;
4081
- card.innerHTML = '';
4280
+ routeCard.innerHTML = '';
4281
+ ruleCard.innerHTML = '';
4082
4282
  if (!show) return;
4083
- addTeamRow(card, 'plan', '计划任务', planSub);
4283
+ /* 上卡:只讲「交给谁」,右侧是引擎;不展示规则描述。 */
4284
+ addTeamRouteRow(routeCard, 'plan', '计划任务');
4285
+ roles.forEach(function (r) {
4286
+ addTeamRouteRow(routeCard, 'role:' + r.name, TEAM_ROLE_LABELS[r.name] || r.name);
4287
+ });
4288
+ /* 下卡:只讲「规则」——计划是拆解口径,角色是「这类适合什么」。 */
4289
+ addTeamRuleRow(ruleCard, 'plan', '计划任务规则', planSub);
4084
4290
  roles.forEach(function (r) {
4085
- addTeamRow(card, 'role:' + r.name, TEAM_ROLE_LABELS[r.name] || r.name, teamRoleDuty(r));
4291
+ var label = (TEAM_ROLE_LABELS[r.name] || r.name) + '规则';
4292
+ addTeamRuleRow(ruleCard, 'role:' + r.name, label, teamRoleDuty(r));
4086
4293
  });
4087
4294
  }
4088
- /* 一行:名字 + 规则描述 + 右侧引擎。点开 = 引擎与规则(见 openTeamRoutePicker)。 */
4089
- function addTeamRow(card, key, label, sub) {
4295
+ /* 上卡的一行:名字 + 右侧引擎。点开 = 只选引擎/模型。 */
4296
+ function addTeamRouteRow(card, key, label) {
4090
4297
  var rt = (TEAM.routes || {})[key] || {};
4091
4298
  var b = hEl('button', 'setrow');
4092
4299
  b.type = 'button';
4093
4300
  var main = hEl('span', 'setrow__main');
4094
4301
  main.appendChild(hEl('span', 'setrow__l', label));
4095
- if (sub) main.appendChild(hEl('span', 'setrow__sub', sub));
4096
4302
  b.appendChild(main);
4097
4303
  b.appendChild(hEl('span', 'setrow__v',
4098
4304
  rt.engine ? (rt.engine + (rt.model ? ' · ' + qaModelText(rt.model) : '')) : '跟随主引擎'));
4099
4305
  b.appendChild(hEl('span', 'setrow__chev', '›'));
4100
4306
  b.onclick = function () {
4101
- openTeamRoutePicker(key,
4102
- String(key).indexOf('role:') === 0 ? (label + ':适合做什么、交给谁') : '计划任务:怎么拆、交给谁',
4307
+ openTeamEnginePicker(key,
4308
+ String(key).indexOf('role:') === 0 ? (label + '交给谁') : '计划任务交给谁',
4103
4309
  teamRoleKindOf(key));
4104
4310
  };
4105
4311
  card.appendChild(b);
4106
4312
  }
4313
+ /* 下卡的一行:名字 + 当前规则。点开 = 只改规则(不含引擎)。 */
4314
+ function addTeamRuleRow(card, key, label, sub) {
4315
+ var b = hEl('button', 'setrow');
4316
+ b.type = 'button';
4317
+ var main = hEl('span', 'setrow__main');
4318
+ main.appendChild(hEl('span', 'setrow__l', label));
4319
+ if (sub) main.appendChild(hEl('span', 'setrow__sub', sub));
4320
+ b.appendChild(main);
4321
+ b.appendChild(hEl('span', 'setrow__chev', '›'));
4322
+ b.onclick = function () { openTeamRuleSheet(key, label); };
4323
+ card.appendChild(b);
4324
+ }
4107
4325
  function teamRoleKindOf(key) {
4108
4326
  if (String(key).indexOf('role:') !== 0) return 'plan';
4109
4327
  var n = String(key).slice(5);
@@ -4115,26 +4333,19 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
4115
4333
  var ov = (TEAM.rules || {})['role:' + r.name];
4116
4334
  return (ov && String(ov).trim()) || r.duty || '';
4117
4335
  }
4118
- /* 点一行 → 抽屉:先「规则」,再「引擎 · 模型」,与「默认引擎」那套同一个交互。
4119
- 计划行:拆解粒度 + 简单代码 / 自动补任务(--team-split / --team-split-code /
4120
- --team-adaptive);角色行:「这类任务适合什么」(--team-rule role:<name>=…)。
4121
- 首项 = 不单独指定 / 跟随主引擎。
4336
+ /* 上卡点一行 → 抽屉:**只选引擎 · 模型**(与「默认引擎」那套同一个交互)。
4337
+ 规则不在这里 —— 见下卡与 openTeamRuleSheet(用户 2026-10-22:「配引擎和配规则要分开配置」)。
4338
+ 首项 = 跟随主引擎。
4122
4339
  2026-10-21 口径(用户:「图像视频不是说需要生成 是擅长做 比如有一个带了视频 skill 的引擎」):
4123
4340
  这里问的是「这类活谁擅长」,所以不写「交给 core 自动路由」—— 说「自动」会让人以为
4124
4341
  不配也能跑,而实际语义是「跟主引擎一致」。 */
4125
- function openTeamRoutePicker(key, title, kind) {
4342
+ function openTeamEnginePicker(key, title, kind) {
4126
4343
  var isRole = String(key).indexOf('role:') === 0;
4127
4344
  if (!kind) kind = key; // 计划键时 kind 就是键本身
4128
4345
  openHostSheet(title, function (body) {
4129
4346
  var hint = hEl('div', 'history__hint', '正在探测引擎与模型…');
4130
4347
  hint.style.textAlign = 'left';
4131
4348
  body.appendChild(hint);
4132
- /* 规则控件放在引擎清单**之前**:规则是这一行的主内容,引擎是「交给谁」。
4133
- 它们与探测无关,引擎清单拉失败也照样能改。分区标题必须显式给出 ——
4134
- 用户 2026-10-22:「入口在哪里 不能配置规则」:控件在,但没标题就读不出这里是配置区。 */
4135
- body.appendChild(hEl('div', 'pick__cap pick__cap--team', '规则'));
4136
- if (isRole) body.appendChild(buildTeamRuleEditor(key));
4137
- else if (kind === 'plan') body.appendChild(buildTeamSplitControls());
4138
4349
  fetchEngines().then(function (list) {
4139
4350
  if (hint.parentNode) hint.remove();
4140
4351
  var avail = list.filter(function (x) { return x.ok; });
@@ -4152,19 +4363,13 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
4152
4363
  不是给 core 的配置(core 只认 engine / model)。
4153
4364
  ⚠️ 必须建在 paint() **外面**:paint 每次切引擎都会重画两列,放里面会一行行堆起来
4154
4365
  (2026-10-21 实测:点两次引擎,抽屉底部就多出三段同样的说明)。 */
4155
- var tipText;
4156
- if (isRole) {
4157
- var info = teamRoleList().filter(function (r) { return 'role:' + r.name === key; })[0];
4158
- tipText = '这个角色内置的职责:' + ((info && info.duty) || '—')
4159
- + '。上面写的规则会替掉它,进计划提示词;这里留空 = 跟随主引擎。';
4160
- } else {
4161
- tipText = '计划阶段负责把任务拆成子任务,交给更擅长规划、上下文更长的引擎更稳。';
4162
- }
4163
- body.appendChild(hEl('div', 'pick__cap pick__cap--team', '交给谁'));
4366
+ var tipText = isRole
4367
+ ? '只选这一类活交给谁;「这类任务适合什么」在团队模式页的「规则」卡里改。'
4368
+ : '计划阶段负责把任务拆成子任务,交给更擅长规划、上下文更长的引擎更稳;拆解规则在「规则」卡里改。';
4164
4369
  var tip = hEl('div', 'history__hint', tipText);
4165
4370
  tip.style.textAlign = 'left';
4166
4371
  body.appendChild(tip);
4167
- var wrap = hEl('div', 'pick__cols pick__cols--team');
4372
+ var wrap = hEl('div', 'pick__cols');
4168
4373
  var left = hEl('div', 'pick__col');
4169
4374
  var right = hEl('div', 'pick__col pick__col--r');
4170
4375
  wrap.appendChild(left); wrap.appendChild(right); body.appendChild(wrap);
@@ -4209,6 +4414,15 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
4209
4414
  });
4210
4415
  });
4211
4416
  }
4417
+ /* 下卡点一行 → 抽屉:**只改规则**(不含引擎)。计划 = 拆解粒度与两个开关;
4418
+ 角色 = 「这类任务适合什么」。与上卡的引擎选择互不掺杂(用户 2026-10-22)。 */
4419
+ function openTeamRuleSheet(key, label) {
4420
+ var isRole = String(key).indexOf('role:') === 0;
4421
+ openHostSheet(label, function (body) {
4422
+ if (isRole) body.appendChild(buildTeamRuleEditor(key));
4423
+ else body.appendChild(buildTeamSplitControls());
4424
+ });
4425
+ }
4212
4426
  /* 角色行的「规则」:一句话写清这类任务适合什么。**默认值直接填内置职责**(用户
4213
4427
  2026-10-22:「也没看到默认规则啊」—— placeholder 在手机上几乎看不见),保存后覆盖它
4214
4428
  (core 的 --team-rule role:<name>=…);改回内置值等于没改,不落覆盖。 */
@@ -4440,7 +4654,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
4440
4654
 
4441
4655
  avail.forEach(function (en) {
4442
4656
  var n = (en.models || []).length;
4443
- /* is-on = 已保存的默认(浅蓝实底);is-cur = 右列此刻在浏览谁(淡底)——
4657
+ /* is-on = 已保存的默认(浅蓝实底);is-cur = 右列此刻在浏览谁(左侧细条,无底色)——
4444
4658
  两者分开,默认是「自动」时不会出现两个一样的选中块。 */
4445
4659
  var b = hEl('button', 'pick__citem'
4446
4660
  + (RUN_PREF.engine === en.engine ? ' is-on' : '')
@@ -4578,6 +4792,10 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
4578
4792
  归属只在用户显式选空间时确定,不能让上一次的 WS 悄悄粘过来。 */
4579
4793
  on('btnFabNew', function () { newConversation(); });
4580
4794
  on('btnMineBack', function () { showHistory(); });
4795
+ /* 我的 → 环境(多连接):列表(点行编辑)+ 新建;返回回「我的」。 */
4796
+ on('btnEnvPage', function () { showEnvPage('mine'); });
4797
+ on('btnEnvBack', function () { if (ENV_BACK === 'tasks') showHistory(); else showMine(); });
4798
+ on('btnEnvAdd', function () { openEnvEditor(null); });
4581
4799
  on('dscBrowser', function () { openBrowser(HOME_URL); });
4582
4800
  on('dscApps', function () { openComingSheet('应用', '暂无可用应用。桌面端发布的应用会出现在这里。'); });
4583
4801
  on('dscPlugins', function () { showPlugins(); });
@@ -4596,7 +4814,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
4596
4814
  on('btnDefaultEngine', openDefaultEnginePicker);
4597
4815
  on('btnDefaultModel', openDefaultEnginePicker);
4598
4816
  /* 团队模式配置(「我的」→ 子页面):计划 + 7 个角色,行由 paintTeamRoles 动态渲染,
4599
- 行的点击在渲染时绑定(见 addTeamRow / openTeamRoutePicker),这里只留入口与返回。 */
4817
+ 行的点击在渲染时绑定(见 addTeamRouteRow / addTeamRuleRow),这里只留入口与返回。 */
4600
4818
  on('btnTeamPage', showTeamPage);
4601
4819
  on('btnTeamBack', showMine);
4602
4820
  on('btnLogout', disconnect);
@@ -103,7 +103,8 @@ const plannerPromptTemplate = `你是任务规划器。把用户的诉求拆成
103
103
  - code:代码产出(脚本、程序、构建 / 测试相关的改动)→ coder。
104
104
  - image:画面类产出(位图 / 矢量:封面、插画、海报、图标)→ designer。
105
105
  - video:影像类产出(分镜、剪辑、成片)→ director。
106
- - game:可运行的游戏工程(代码 + 素材 + 启动方式)→ builder。
106
+ - game:游戏相关产出 —— 游戏内玩法 / 系统开发(在现有工程里改代码、加功能),以及从零交付
107
+ 可运行的游戏工程(代码 + 素材 + 启动方式)→ builder。
107
108
  - {{CODE_GATE}}
108
109
  - **需求里出现了对应类别的产物,就必须单独拆出该类任务**,哪怕用户没明说「配图 / 做成视频」。
109
110
  例:「写 100 字游戏介绍并配 1 个封面」→ 1 个 writer + 1 个 designer;
@@ -87,8 +87,8 @@ var builtinRoles = []Role{
87
87
  },
88
88
  {
89
89
  Name: "builder",
90
- Duty: "交付能跑起来的工程与可复现的启动方式",
91
- SystemPrompt: "你的角色是游戏 / 工程构建者。优先让产物可运行:先搭最小可运行骨架,再补功能;所有命令都要在工作目录内可复现,依赖与启动方式写进交付物。",
90
+ Duty: "游戏内玩法 / 系统开发,交付能跑起来、可复现启动的工程",
91
+ SystemPrompt: "你的角色是游戏开发者。在现有工程里做玩法 / 系统开发时,先读懂现有结构与约定,只改相关范围;从零交付工程时,先搭最小可运行骨架再补功能。所有命令都要在工作目录内可复现,依赖与启动方式写进交付物。",
92
92
  Kind: KindGame,
93
93
  },
94
94
  }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magic-agent",
3
- "version": "1.0.109",
3
+ "version": "1.0.111",
4
4
  "description": "专业的 agent CLI 代理工具 - 统一 claude / codebuddy / trae / llm / codex / openclaw 引擎",
5
5
  "license": "MIT",
6
6
  "bin": {