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.
- package/client-ui-mobile/index.html +343 -125
- package/internal/team/planner.go +2 -1
- package/internal/team/role.go +2 -2
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/package.json +1 -1
|
@@ -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 =
|
|
420
|
-
|
|
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
|
-
**
|
|
500
|
-
|
|
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:
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
/* 选中态不只换颜色,还给一块
|
|
513
|
-
|
|
514
|
-
.tab .icw{display:grid;place-items:center;width:
|
|
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` 是一块悬在栏顶边界内的半圆(宽
|
|
551
|
+
① **洞**:`::before` 是一块悬在栏顶边界内的半圆(宽 74 / 高 37,下两角半径 37),
|
|
537
552
|
填槽底 `--fab-well`,在底栏上边缘切出一个半圆缺口。
|
|
538
|
-
高度只到
|
|
539
|
-
② **嵌**:按钮直径
|
|
553
|
+
高度只到 37,**不侵入上方内容**(试过整圆深度,会盖住列表底部)。
|
|
554
|
+
② **嵌**:按钮直径 68,圆心落在栏顶边线上(上露 34 / 下沉 34),
|
|
540
555
|
于是下半实打实坐进洞里。圆心压边线而不是抬高,是「嵌」与「放」的分界。
|
|
541
|
-
③ **接触影**:`radial-gradient` 把暗环**锚定在 r=
|
|
542
|
-
从
|
|
556
|
+
③ **接触影**:`radial-gradient` 把暗环**锚定在 r=34(按钮半径)**上,
|
|
557
|
+
从 33.5px 起最暗、到 37px 淡完。这是唯一能精确控制「暗在哪一圈」的写法 ——
|
|
543
558
|
`box-shadow: inset` 的暗边永远从元素边界向内,管不到元素外的那道槽。
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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**(洞半径
|
|
554
|
-
|
|
555
|
-
|
|
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:
|
|
560
|
-
border-radius:0 0
|
|
575
|
+
width:74px;height:37px;
|
|
576
|
+
border-radius:0 0 37px 37px;
|
|
561
577
|
background:
|
|
562
|
-
radial-gradient(circle
|
|
563
|
-
var(--fab-notch)
|
|
564
|
-
var(--fab-notch)
|
|
565
|
-
color-mix(in srgb,var(--fab-notch) 35%,transparent)
|
|
566
|
-
transparent
|
|
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% -
|
|
585
|
+
left:50%;bottom:calc(100% - 34px); /* 圆心钉在栏顶边线上:上露 34 / 下沉 34 */
|
|
570
586
|
transform:translateX(-50%);
|
|
571
|
-
width:
|
|
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
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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 +
|
|
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
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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"
|
|
978
|
+
<div class="history__group">交给谁(引擎 / 模型)</div>
|
|
972
979
|
<div class="setcard" id="teamRouteCard"></div>
|
|
973
|
-
<div class="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
1137
|
-
<button class="tab" type="button" data-tab="mine"><span class="icw"><svg width="
|
|
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() {
|
|
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
|
-
|
|
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
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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
|
-
|
|
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();
|
|
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
|
-
/* 团队模式子页面 =
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4260
|
+
/* 团队模式子页面 = **两张卡**(用户 2026-10-22:「配引擎和配规则要分开配置 不要在 1 个页面」):
|
|
4261
|
+
#teamRouteCard「交给谁」= 计划 + 7 个角色的引擎/模型,点开只有引擎;
|
|
4262
|
+
#teamRuleCard「规则」= 计划的拆解口径 + 7 个角色的「适合什么」,点开只有规则。
|
|
4263
|
+
签名含两侧文案与引擎:变了才重建,避免 5s 轮询时闪。 */
|
|
4066
4264
|
function paintTeamRoles(roles) {
|
|
4067
|
-
var
|
|
4068
|
-
|
|
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
|
-
|
|
4280
|
+
routeCard.innerHTML = '';
|
|
4281
|
+
ruleCard.innerHTML = '';
|
|
4082
4282
|
if (!show) return;
|
|
4083
|
-
|
|
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
|
-
|
|
4291
|
+
var label = (TEAM_ROLE_LABELS[r.name] || r.name) + '规则';
|
|
4292
|
+
addTeamRuleRow(ruleCard, 'role:' + r.name, label, teamRoleDuty(r));
|
|
4086
4293
|
});
|
|
4087
4294
|
}
|
|
4088
|
-
/*
|
|
4089
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
4120
|
-
|
|
4121
|
-
首项 = 不单独指定 / 跟随主引擎。
|
|
4336
|
+
/* 上卡点一行 → 抽屉:**只选引擎 · 模型**(与「默认引擎」那套同一个交互)。
|
|
4337
|
+
规则不在这里 —— 见下卡与 openTeamRuleSheet(用户 2026-10-22:「配引擎和配规则要分开配置」)。
|
|
4338
|
+
首项 = 跟随主引擎。
|
|
4122
4339
|
2026-10-21 口径(用户:「图像视频不是说需要生成 是擅长做 比如有一个带了视频 skill 的引擎」):
|
|
4123
4340
|
这里问的是「这类活谁擅长」,所以不写「交给 core 自动路由」—— 说「自动」会让人以为
|
|
4124
4341
|
不配也能跑,而实际语义是「跟主引擎一致」。 */
|
|
4125
|
-
function
|
|
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
|
-
|
|
4157
|
-
|
|
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
|
|
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
|
-
行的点击在渲染时绑定(见
|
|
4817
|
+
行的点击在渲染时绑定(见 addTeamRouteRow / addTeamRuleRow),这里只留入口与返回。 */
|
|
4600
4818
|
on('btnTeamPage', showTeamPage);
|
|
4601
4819
|
on('btnTeamBack', showMine);
|
|
4602
4820
|
on('btnLogout', disconnect);
|
package/internal/team/planner.go
CHANGED
|
@@ -103,7 +103,8 @@ const plannerPromptTemplate = `你是任务规划器。把用户的诉求拆成
|
|
|
103
103
|
- code:代码产出(脚本、程序、构建 / 测试相关的改动)→ coder。
|
|
104
104
|
- image:画面类产出(位图 / 矢量:封面、插画、海报、图标)→ designer。
|
|
105
105
|
- video:影像类产出(分镜、剪辑、成片)→ director。
|
|
106
|
-
- game
|
|
106
|
+
- game:游戏相关产出 —— 游戏内玩法 / 系统开发(在现有工程里改代码、加功能),以及从零交付
|
|
107
|
+
可运行的游戏工程(代码 + 素材 + 启动方式)→ builder。
|
|
107
108
|
- {{CODE_GATE}}
|
|
108
109
|
- **需求里出现了对应类别的产物,就必须单独拆出该类任务**,哪怕用户没明说「配图 / 做成视频」。
|
|
109
110
|
例:「写 100 字游戏介绍并配 1 个封面」→ 1 个 writer + 1 个 designer;
|
package/internal/team/role.go
CHANGED
|
@@ -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
|