cs-bots 0.7.5 → 0.9.0

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/src/client.js CHANGED
@@ -493,14 +493,14 @@ window.__ModuleLoader__.load({
493
493
  */
494
494
  const ERROR_MAP = {
495
495
  BOARD_WIP_EXCEEDED: {
496
- actionLabel: `查看${HUMAN_LABEL}的任务`,
497
- text: '你已经在管 N 个任务了(上限 M),先完成或释放一些再接新的',
496
+ actionLabel: '查看任务栏',
497
+ text: `${HUMAN_LABEL}已经在管 N 个任务了(上限 M),先完成或释放一些再接新的`,
498
498
  hint: 'WIP 超限:同一 Bot 名下进行中的任务数达到上限',
499
499
  },
500
500
  BOARD_LEASE_HELD: {
501
501
  actionLabel: '查看持有者',
502
- text: '这个任务正由另一个 Bot 在做,等它完成或租约过期',
503
- hint: '租约被占用:任务上有未过期的租约,持有者不是当前操作者',
502
+ text: '这个任务正被另一个 Bot 占着,等它完成或把任务放回待办',
503
+ hint: '任务上有未过期的占用(这是防抢机制,不在页面展示),持有者不是当前操作者',
504
504
  },
505
505
  BOARD_NOT_READY: {
506
506
  actionLabel: '查看前置任务',
@@ -519,12 +519,12 @@ window.__ModuleLoader__.load({
519
519
  },
520
520
  BOARD_REV_CONFLICT: {
521
521
  actionLabel: '刷新',
522
- text: '这个任务刚被别人改过,刷新一下再操作',
522
+ text: '这个任务刚被别的 Bot 改过,刷新一下再操作',
523
523
  hint: 'rev 不匹配:expectedRev 与当前 rev 不一致(CAS 拒绝静默覆盖)',
524
524
  },
525
525
  CAS_MISMATCH: {
526
526
  actionLabel: '刷新',
527
- text: '这个任务刚被别人改过,刷新一下再操作',
527
+ text: '这个任务刚被别的 Bot 改过,刷新一下再操作',
528
528
  hint: 'rev 不匹配:expectedRev 与当前 rev 不一致(CAS 拒绝静默覆盖)',
529
529
  },
530
530
  BOARD_TASK_DELETED: {
@@ -537,13 +537,54 @@ window.__ModuleLoader__.load({
537
537
  text: '写入失败了,可能是磁盘或权限问题,请检查状态目录',
538
538
  hint: '事件追加失败:拒绝「只改内存不落盘」',
539
539
  },
540
+ // 契约 第2批 ①:跨团队路由错误(对齐 backend-b 的实际枚举,见 src/teams.js resolveRoute)
541
+ ROUTE_TASK_ID_REQUIRED: {
542
+ actionLabel: null,
543
+ text: '缺少关联任务,无法发协作请求',
544
+ hint: 'taskId 必填:路由靠 taskId 做环路检测与去重',
545
+ },
546
+ ROUTE_TARGET_REQUIRED: {
547
+ actionLabel: null,
548
+ text: '没选目标团队',
549
+ hint: 'toTeam 或 toBot 必须给一个',
550
+ },
551
+ ROUTE_TARGET_AMBIGUOUS: {
552
+ actionLabel: null,
553
+ text: '目标不明确(团队和 Bot 只能选一个)',
554
+ hint: 'toTeam 与 toBot 同时给出会被拒',
555
+ },
556
+ ROUTE_TARGET_NOT_FOUND: {
557
+ actionLabel: '刷新团队列表',
558
+ text: '目标团队不存在了,刷新一下',
559
+ hint: 'route.toTeam 不在团队列表中',
560
+ },
561
+ ROUTE_HOP_EXCEEDED: {
562
+ actionLabel: null,
563
+ text: '跳数超限,已阻止循环转发',
564
+ hint: '路由 hop 超过上限(3),防止 A→B→A→B 死循环',
565
+ },
566
+ ROUTE_NO_LEAD: {
567
+ actionLabel: null,
568
+ text: '目标团队没有负责人,无法送达',
569
+ hint: '目标团队 leadId 为空,没人可唤醒',
570
+ },
571
+ ROUTE_NO_DISPATCHER: {
572
+ actionLabel: null,
573
+ text: '唤醒通道不可用,这次请求没送出去',
574
+ hint: 'dispatcher 未装配(宿主缺组件),路由决定不做',
575
+ },
576
+ ROUTE_RECORD_FAILED: {
577
+ actionLabel: null,
578
+ text: '留痕失败,请求已中止(不会偷偷投递)',
579
+ hint: 'route 事件追加失败(磁盘/权限),intent-first 原则拒绝「投了没记」',
580
+ },
540
581
  }
541
582
 
542
583
  /**
543
584
  * 错误可读映射:后端 code → 人话 + 下一步建议。
544
585
  *
545
586
  * 为什么:后端 message 只描述「发生了什么」(还夹着 id/rev),
546
- * 用户看完仍然不知道**接下来该做什么**。高频冲突(WIP 超限、租约占用、
587
+ * 用户看完仍然不知道**接下来该做什么**。高频冲突(WIP 超限、占用冲突、
547
588
  * rev 冲突)恰恰是最需要下一步的,所以按 code 映射。
548
589
  *
549
590
  * 后端 message 仍保留在 hint 里 —— 定位信息(哪个任务 / 哪个 Bot)不能丢。
@@ -958,6 +999,12 @@ window.__ModuleLoader__.load({
958
999
  const [graphError, setGraphError] = useState(null)
959
1000
  // 交付物文件预览弹窗
960
1001
  const [previewPath, setPreviewPath] = useState(null)
1002
+ // 换负责人弹窗(契约 §4.3(c):Boss 直接换负责人 / 重新派发)
1003
+ const [reassignOpen, setReassignOpen] = useState(false)
1004
+ // 契约 §5.3:投递失败展示 + 「诊断这个任务」一次性排查(需求6)
1005
+ const [diagnose, setDiagnose] = useState(null)
1006
+ const [diagnoseLoading, setDiagnoseLoading] = useState(false)
1007
+ const [diagnoseErr, setDiagnoseErr] = useState(null)
961
1008
  // 预览内容在这里统一取(需求3):弹窗只是壳,由父组件拉数据,
962
1009
  // 否则弹窗自己的 effect 只在真实渲染器挂载子组件时才跑,测试桩与边缘场景都取不到内容。
963
1010
  const [preview, setPreview] = useState(null)
@@ -1010,6 +1057,13 @@ window.__ModuleLoader__.load({
1010
1057
  return () => { alive = false }
1011
1058
  }, [task?.id])
1012
1059
 
1060
+ // 进度来源(契约 §6):board 侧新增只读派生字段 progressBy/progressAt。
1061
+ // `progressBy === 'ui'` ⇒ 最后一次写进度的是 Boss(前端提交);
1062
+ // 旧数据没有这个字段 ⇒ 视为「队长估算」(Boss 从没写过 progress)。
1063
+ // ⚠️ 必须在组件体顶层用 `task?.` 取值:首渲染时 `data` 还没到(loading),
1064
+ // `task` 是 undefined —— 写 `task.progressBy` 会让整个抽屉白屏(已实测复现)。
1065
+ const progressSource = task?.progressBy === 'ui' ? `${HUMAN_LABEL}设定` : '队长估算'
1066
+
1013
1067
  const saveProgress = async () => {
1014
1068
  const v = Number(progressVal)
1015
1069
  if (Number.isNaN(v) || v < 0 || v > 100) {
@@ -1060,7 +1114,7 @@ window.__ModuleLoader__.load({
1060
1114
  }, '📦 看产出物')),
1061
1115
  h('div', { className: 'csb-kv' },
1062
1116
  // ── 5W:谁建的 / 是什么 / 何时 / 在哪做 / 为什么 ──
1063
- // 需求5:表述以「人」为主体——人建的任务就说「用户」,不暴露 Bot 会话 id
1117
+ // 需求5:表述以「Boss」为主体——Boss 建的任务就说「Boss」,不暴露 Bot 会话 id
1064
1118
  h('span', { className: 'csb-k' }, '谁建的'),
1065
1119
  h('span', { className: 'csb-v' }, task.source === 'human' ? HUMAN_LABEL : (task.createdBy ? botName(bots, task.createdBy) : '智能体')),
1066
1120
  h('span', { className: 'csb-k' }, '来源'),
@@ -1074,7 +1128,7 @@ window.__ModuleLoader__.load({
1074
1128
  h('span', { key: 'se', className: 'csb-k' }, '执行阶段'),
1075
1129
  h('span', { key: 'sv', className: 'csb-v' },
1076
1130
  h('span', { className: stageBadgeCls(data.stageInfo.group), title: data.stageInfo.reason }, data.stageInfo.label),
1077
- data.stageInfo.needsPerson ? h('span', { className: 'csb-b csb-b-run', style: { marginLeft: 8 } }, '需要你处理') : null),
1131
+ data.stageInfo.needsPerson ? h('span', { className: 'csb-b csb-b-run', style: { marginLeft: 8 } }, '需要 Boss 处理') : null),
1078
1132
  ]
1079
1133
  : null,
1080
1134
  h('span', { className: 'csb-k' }, '任务编号'), h('span', { className: 'csb-v csb-mono' }, task.id),
@@ -1084,7 +1138,7 @@ window.__ModuleLoader__.load({
1084
1138
  // 人会在「等你回答」里看到一句「端口被占用」而不知道该回什么。
1085
1139
  data?.stageInfo?.stage === 'person/answer' && task.latestReport?.attention?.question
1086
1140
  ? [
1087
- h('span', { key: 'ae', className: 'csb-k' }, '要你回答'),
1141
+ h('span', { key: 'ae', className: 'csb-k' }, '要 Boss 回答'),
1088
1142
  h('span', { key: 'av', className: 'csb-v', style: { color: 'var(--dsw-alias-text-warn, #b45309)', fontWeight: 600 } },
1089
1143
  h(CollapsibleText, { text: String(task.latestReport.attention.question), limit: 300 })),
1090
1144
  ]
@@ -1111,7 +1165,7 @@ window.__ModuleLoader__.load({
1111
1165
  : '可以做什么',
1112
1166
  desc: task.status === 'completed'
1113
1167
  ? '已完成不是终点:下面的「变更」区可以重开接着做 / 派生优化单 / 转成长任务。'
1114
- : '指派、完成、验收是 Bot 的职责,不由你操作。这里只有需要你拍板的动作。',
1168
+ : '下面这些动作 Boss 可以直接做:改优先级/截止/复杂度/付费、改目标与边界、换负责人、阻塞与解阻塞、编辑长任务的阶段清单。每个操作都会如实提示结果。',
1115
1169
  }),
1116
1170
  h('div', { className: 'csb-bar', style: { marginTop: 10 } },
1117
1171
  task.status === 'pending'
@@ -1125,16 +1179,97 @@ window.__ModuleLoader__.load({
1125
1179
  // 改造06(CONTRACT-bots06 §3.1 / §3.3):变更中心 —— 放在「可以做什么」之后
1126
1180
  h(TaskEvolvePanel, { taskId: task.id, task, onChange }),
1127
1181
 
1128
- // 高级:租约(原「续租」按钮,普通人看不懂,收进折叠并解释)
1129
- h(Section, { title: '高级:租约', desc: '租约是「我先占着,别人别抢」的凭据。领取任务默认占 5 分钟,长任务需要隔一会儿点一次「延长」,否则会被自动释放回待办。' }),
1130
- task.status === 'in_progress'
1131
- ? h('div', { className: 'csb-bar', style: { marginTop: 10 } },
1132
- h('span', { className: 'csb-set-d' }, task.lease ? `当前占用到 ${fmt(task.lease.expiresAt)}` : '当前没有租约'),
1133
- h('button', { className: 'csb-btn csb-btn-s', onClick: () => act(() => upd('heartbeat'), '延长') }, '延长 5 分钟'))
1134
- : h('div', { className: 'csb-set-d', style: { marginTop: 10 } }, '任务不在进行中,没有租约。'),
1182
+ // 契约 §4.3:Boss 直接动手(改排期/路由、改目标与边界、换负责人、
1183
+ // 阻塞/解除阻塞/归档、编辑长任务阶段清单)——全部内联,不跳页、不靠 prompt。
1184
+ h(TaskBossActions, {
1185
+ task,
1186
+ bots,
1187
+ onChange,
1188
+ onReassign: () => setReassignOpen(true),
1189
+ }),
1190
+
1191
+ // 需求2:任务对话(进度 + 对话内容 + 去对话跳转),直接放在产出物之前
1192
+ h(TaskConversationPanel, { taskId: task.id, task, bots, onGotoSource }),
1193
+
1194
+ // 契约 §2.5(需求3):阶段产出物 —— 必须在总产出物证据之前,
1195
+ // 让 Boss 「点击去看产出物」时先看到过程产物,不用去翻对话。
1196
+ h(TaskPhasePanel, { taskId: task.id, onPreview: (p) => setPreviewPath(p) }),
1197
+
1198
+ // ── 契约 §5(需求6):投递失败必须可见,且能一键诊断 ────────────────
1199
+ // 「armDeliveryWatch 超时未兑现」的落点就在 `task.delivery.error`,
1200
+ // 后端把它留在任务上(一直显示到被解决),前端必须把它显示出来 ——
1201
+ // 否则 Boss 看到的是「任务一直没动静」,没有一句人话告诉他卡在哪。
1202
+ task.delivery?.error
1203
+ ? h('div', { className: 'csb-err', style: { marginTop: 12 } },
1204
+ `投递失败:${task.delivery.error}`
1205
+ + (task.delivery.at ? `(${fmt(task.delivery.at)})` : '')
1206
+ + ' —— 先点「诊断这个任务」看执行者能不能干活,再决定重试还是换人。')
1207
+ : null,
1208
+ h('div', { className: 'csb-bar', style: { marginTop: 10, gap: 6 } },
1209
+ h('button', {
1210
+ className: 'csb-btn', disabled: diagnoseLoading,
1211
+ onClick: async () => {
1212
+ setDiagnoseLoading(true)
1213
+ setDiagnoseErr(null)
1214
+ setDiagnose(null)
1215
+ try {
1216
+ const d = await call(`/tasks/diagnose?id=${encodeURIComponent(task.id)}`, { method: 'GET' })
1217
+ setDiagnose(d)
1218
+ } catch (e) {
1219
+ setDiagnoseErr(humanizeError(e, {}, '诊断失败:'))
1220
+ } finally {
1221
+ setDiagnoseLoading(false)
1222
+ }
1223
+ },
1224
+ }, diagnoseLoading ? '诊断中…' : '诊断这个任务')),
1225
+ // 诊断结果:一段人话结论 + 各项状态(Boss 不用自己翻日志)
1226
+ diagnoseErr
1227
+ ? h('div', { className: 'csb-err' }, diagnoseErr.text)
1228
+ : null,
1229
+ diagnose
1230
+ ? h('div', { className: 'csb-card', style: { marginTop: 10, padding: '10px 12px' } },
1231
+ h('div', { className: 'csb-card-main' },
1232
+ h('div', { className: 'csb-card-t' },
1233
+ diagnose.verdict === 'blocked'
1234
+ ? '执行者暂时不能干活'
1235
+ : diagnose.verdict === 'ok'
1236
+ ? '执行者可以干活'
1237
+ : '诊断结果'),
1238
+ diagnose.advice
1239
+ ? h('div', { className: 'csb-card-m', style: { marginTop: 6 } }, diagnose.advice)
1240
+ : null,
1241
+ // 每一项状态都显示(ok / 被谁卡住),不用猜
1242
+ h('div', { className: 'csb-card-m', style: { marginTop: 6, flexWrap: 'wrap' } },
1243
+ h('span', { className: 'csb-b' }, `执行者:${diagnose.bot?.name ?? '(未知)'}`),
1244
+ h('span', { className: 'csb-b' },
1245
+ diagnose.bot?.hasSession ? '会话在线' : '会话不在线'),
1246
+ diagnose.bot?.preset
1247
+ ? h('span', { className: 'csb-b' }, `预制体:${diagnose.bot.preset}`)
1248
+ : null,
1249
+ diagnose.live
1250
+ ? h('span', { className: 'csb-b' }, diagnose.live.ok ? '能拉起' : `拉不起来:${diagnose.live.reason ?? '未知'}`)
1251
+ : null),
1252
+ (diagnose.wakes ?? []).length > 0
1253
+ ? h('div', { className: 'csb-card-m', style: { marginTop: 6, flexWrap: 'wrap' } },
1254
+ '最近唤醒:',
1255
+ diagnose.wakes.slice(0, 5).map((w, i) =>
1256
+ h('span', { key: i, className: 'csb-b csb-b-err' },
1257
+ `${w.result ?? '未知'}${w.reason ? `(${w.reason})` : ''}`)))
1258
+ : null))
1259
+ : null,
1135
1260
 
1136
1261
  // 进度编辑
1137
- h(Section, { title: '更新进度', desc: `当前 ${task.progress ?? 0}%。填入 0-100 的数字并保存(走 CAS,别人刚改过会提示刷新)。` }),
1262
+ // 契约 §6:`estimate`(预估工作量,队长承诺的投入)与 `progress`(实际完成度)
1263
+ // 是**两个不同的数**,必须分两行显示 —— 混成一个数会让人以为「估 8 小时、做了 8」
1264
+ // 就是「100% 做完」。
1265
+ h(Section, {
1266
+ title: '进度',
1267
+ desc: `当前完成度 ${task.progress ?? 0}%(${progressSource})。`
1268
+ + (task.estimate !== undefined && task.estimate !== null
1269
+ ? `预估工作量 ${task.estimate}(队长估的投入,跟完成度不是一回事)。`
1270
+ : '')
1271
+ + '填入 0-100 的数字并保存(保存走 CAS,任务刚被改过会提示刷新)。',
1272
+ }),
1138
1273
  h('div', { className: 'csb-bar', style: { marginTop: 10 } },
1139
1274
  h('input', {
1140
1275
  className: 'csb-in', style: { width: 100 }, type: 'number', min: 0, max: 100,
@@ -1223,7 +1358,11 @@ window.__ModuleLoader__.load({
1223
1358
  error: preview?.error ?? null,
1224
1359
  data: preview?.data ?? null,
1225
1360
  onClose: () => setPreviewPath(null),
1226
- }) : null)
1361
+ }) : null,
1362
+ // 契约 §4.3(c):Boss 直接换负责人 / 重新派发(内联弹窗)
1363
+ reassignOpen
1364
+ ? h(TaskReassignModal, { task, bots, onClose: () => setReassignOpen(false), onChange })
1365
+ : null)
1227
1366
  }
1228
1367
 
1229
1368
  /**
@@ -1511,7 +1650,7 @@ window.__ModuleLoader__.load({
1511
1650
  * 把任务的「负责人相关字段」解析成一个真正可跳转的 Bot。
1512
1651
  *
1513
1652
  * 为什么需要它(2026-11-06 用户实测报错「该负责人不是本平台的 Bot,无法跳转…定位 ui」):
1514
- * - `ownerId` 是**租约持有者**:人会自己 claim(`ui`),或验收时被覆盖;
1653
+ * - `ownerId` 是**占用持有者**:Boss 会自己 claim(`ui`),或验收时被覆盖;
1515
1654
  * - `assignee` 才是「这活原本派给谁」;
1516
1655
  * - 任务可能没有 owner 但创建时填了 assignee。
1517
1656
  * 所以按 **assignee → ownerId → task.ownerId** 顺序找,命中即用;
@@ -1602,7 +1741,7 @@ window.__ModuleLoader__.load({
1602
1741
  * 去对话:把「负责人」解析成真正的 Bot 会话,再调宿主 openSession。
1603
1742
  *
1604
1743
  * 为什么不能只看 ownerId(2026-11-06 用户实测报错):
1605
- * `ownerId` 是**租约持有者**,人会自己 claim(`ui`)或验收时被覆盖;
1744
+ * `ownerId` 是**占用持有者**,Boss 会自己 claim(`ui`)或验收时被覆盖;
1606
1745
  * 而「这活原本派给谁」记在 `assignee` 上。只看 ownerId 就会出现
1607
1746
  * 「任务明明派给了 bot-xxx,点去对话却说『该负责人不是本平台的 Bot』,定位 ui」。
1608
1747
  * ⇒ 按 **assignee → ownerId → anchor.ownerId** 顺序逐个尝试,
@@ -1620,7 +1759,7 @@ window.__ModuleLoader__.load({
1620
1759
  setJump({
1621
1760
  id: candidates[0],
1622
1761
  hint: onlyPlatform
1623
- ? '这个任务的负责人是平台操作者(你),没有可跳转的 Bot 会话。'
1762
+ ? '这个任务的负责人是平台自身的 Boss 身份,没有可跳转的 Bot 会话。'
1624
1763
  + '如果它是派给某个 Bot 的,请确认「负责人」字段填的是 Bot 的会话 ID(可在 Bot 页复制)。'
1625
1764
  : '这个任务的负责人不是本平台注册的 Bot(可能已被退役,或是外部会话),无法自动跳转。'
1626
1765
  + '可以复制 ID 到左侧会话列表里手动定位。',
@@ -1804,7 +1943,7 @@ window.__ModuleLoader__.load({
1804
1943
  h('div', { style: { fontSize: 12.5, lineHeight: 1.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } },
1805
1944
  h('span', { className: 'csb-mono', style: { marginRight: 6 } }, `[${fmt(n.at)}]`),
1806
1945
  n.text ?? '',
1807
- h('span', { className: 'csb-set-d' }, `(${n.by ?? 'ui'})`)))))),
1946
+ h('span', { className: 'csb-set-d' }, `(${humanizeBy(n.by)})`)))))),
1808
1947
 
1809
1948
  // 改造06:换负责人弹窗(Bot 下拉 → POST /tasks/reassign)
1810
1949
  reassignOpen
@@ -1838,7 +1977,7 @@ window.__ModuleLoader__.load({
1838
1977
  // 规则(契约原文,逐条实现,不自作主张):
1839
1978
  // room + exists !== false → 按钮「回到消息出处(房间名 #N)」
1840
1979
  // room + exists === false → 按钮禁用 + 提示「来源房间已删除」
1841
- // none → **不渲染任何来源区块**(人建的任务不占位)
1980
+ // none → **不渲染任何来源区块**(Boss 建的任务不占位)
1842
1981
  // todo/evolve → 只读一行「来源:<sourceSeq>」
1843
1982
  // unparsed → 只读一行 + 后端 reason(绝不渲染跳不动的按钮)
1844
1983
  sourceQ.error
@@ -1909,7 +2048,7 @@ window.__ModuleLoader__.load({
1909
2048
  h('span', { className: 'csb-v' },
1910
2049
  h('span', { className: stageBadgeCls(si.group), title: si.reason }, si.label),
1911
2050
  si.needsPerson
1912
- ? h('span', { className: 'csb-b csb-b-run', style: { marginLeft: 8 } }, '需要你处理')
2051
+ ? h('span', { className: 'csb-b csb-b-run', style: { marginLeft: 8 } }, '需要 Boss 处理')
1913
2052
  : null)))
1914
2053
  }
1915
2054
  // ── 执行者状态(2026-11-09:「显示正在执行中,但我看不到过程」)────
@@ -1973,6 +2112,327 @@ window.__ModuleLoader__.load({
1973
2112
  })())
1974
2113
  }
1975
2114
 
2115
+ /**
2116
+ * 任务详情 ·「Boss 直接动手」区块(契约 §4.3,需求5)。
2117
+ *
2118
+ * Boss 原话:「我要能直接动手,不要只给我看」。五类动作全部做成**内联表单**,
2119
+ * 不跳页、不弹浏览器 prompt;每个写操作都 toast 出真实结果(成功 / 失败理由),
2120
+ * 绝不允许「点了没反应」。
2121
+ *
2122
+ * 五条路径(契约 §4.1 的端点,A 直接用):
2123
+ * (a) 改优先级 / 截止 / 复杂度档位 T0–T3 / 是否付费 → `POST /tasks/update` action:'edit'
2124
+ * (b) 改目标 / 边界(description、writeScopes) → 同上
2125
+ * (c) 换负责人 / 收回 / 重新派发 → `POST /tasks/reassign` / action:'release'
2126
+ * (d) 阻塞 / 解除阻塞 / 关闭归档 → action:'block'/'unblock'/'delete'
2127
+ * (e) 编辑长任务阶段清单 → `POST /tasks/phases`
2128
+ *
2129
+ * CAS:所有写都带 `expectedRev: task.rev`;冲突时后端 409,这里显示人话并给刷新入口。
2130
+ */
2131
+ function TaskBossActions({ task, bots, onChange, onReassign }) {
2132
+ // 当前展开的表单(同时只开一个,避免整页都是输入框)
2133
+ const [form, setForm] = useState(null) // 'basic' | 'scope' | 'block' | 'phases'
2134
+ const [busy, setBusy] = useState(null)
2135
+ const [err, setErr] = useState(null)
2136
+ const [done, setDone] = useState(null)
2137
+
2138
+ // (a) 排期/路由字段
2139
+ const [priority, setPriority] = useState(task?.priority !== undefined ? String(task.priority) : '')
2140
+ const [dueAt, setDueAt] = useState(task?.dueAt ? toDateInput(task.dueAt) : '')
2141
+ const [complexity, setComplexity] = useState(task?.expectedComplexity ?? '')
2142
+ const [paid, setPaid] = useState(task?.requiresPaid === true ? 'yes' : task?.requiresPaid === false ? 'no' : '')
2143
+ // (b) 目标与边界
2144
+ const [subject, setSubject] = useState(task?.subject ?? '')
2145
+ const [description, setDescription] = useState(task?.description ?? '')
2146
+ const [scopes, setScopes] = useState((task?.writeScopes ?? []).join('\n'))
2147
+ // (d) 阻塞理由
2148
+ const [blockReason, setBlockReason] = useState(task?.blockedReason ?? '')
2149
+ // (e) 阶段清单
2150
+ const [phaseText, setPhaseText] = useState((task?.longrun?.phases ?? []).join('\n'))
2151
+
2152
+ if (!task) return null
2153
+
2154
+ const run = async (label, fn) => {
2155
+ setBusy(label)
2156
+ setErr(null)
2157
+ setDone(null)
2158
+ try {
2159
+ const d = await fn()
2160
+ setDone(d?.note ?? `${label}成功`)
2161
+ setForm(null)
2162
+ onChange?.()
2163
+ } catch (e) {
2164
+ setErr(humanizeError(e, {}, `${label}失败:`))
2165
+ } finally {
2166
+ setBusy(null)
2167
+ }
2168
+ }
2169
+ const edit = (body) => call('/tasks/update', {
2170
+ method: 'POST',
2171
+ body: { id: task.id, expectedRev: task.rev, action: 'edit', ...body },
2172
+ })
2173
+
2174
+ const saveBasic = () => run('保存', () => edit({
2175
+ // 空字符串 ⇒ 不传(不改);优先级用 null 表示清除
2176
+ ...(priority === '' ? {} : { priority: Number(priority) }),
2177
+ ...(dueAt === '' ? { dueAt: null } : { dueAt: dateInputToMs(dueAt) }),
2178
+ ...(complexity === '' ? { expectedComplexity: null } : { expectedComplexity: complexity }),
2179
+ ...(paid === '' ? { requiresPaid: null } : { requiresPaid: paid === 'yes' }),
2180
+ }))
2181
+ const saveScope = () => run('保存目标与边界', () => edit({
2182
+ ...(subject.trim() ? { subject: subject.trim() } : {}),
2183
+ description,
2184
+ writeScopes: scopes.split('\n').map((s) => s.trim()).filter(Boolean),
2185
+ }))
2186
+ const savePhases = () => run('保存阶段清单', () => call('/tasks/phases', {
2187
+ method: 'POST',
2188
+ body: {
2189
+ id: task.id,
2190
+ expectedRev: task.rev,
2191
+ phases: phaseText.split('\n').map((s) => s.trim()).filter(Boolean),
2192
+ },
2193
+ }))
2194
+ const doBlock = () => run('阻塞', () => call('/tasks/update', {
2195
+ method: 'POST',
2196
+ body: { id: task.id, expectedRev: task.rev, action: 'block', reason: blockReason.trim() || 'Boss 阻塞' },
2197
+ }))
2198
+ const doUnblock = () => run('解除阻塞', () => call('/tasks/update', {
2199
+ method: 'POST', body: { id: task.id, expectedRev: task.rev, action: 'unblock' },
2200
+ }))
2201
+ const doRelease = () => run('收回', () => call('/tasks/update', {
2202
+ method: 'POST', body: { id: task.id, expectedRev: task.rev, action: 'release' },
2203
+ }))
2204
+ const doArchive = () => run('归档', () => call('/tasks/update', {
2205
+ method: 'POST', body: { id: task.id, expectedRev: task.rev, action: 'delete' },
2206
+ }))
2207
+
2208
+ const toggle = (name) => setForm((v) => (v === name ? null : name))
2209
+ const btn = (label, name, extra) => h('button', {
2210
+ className: `csb-btn${name === form ? ' csb-btn-p' : ''}`,
2211
+ onClick: () => toggle(name), ...(extra ?? {}),
2212
+ }, label)
2213
+
2214
+ return h('div', { style: { marginTop: 10 } },
2215
+ h('div', { className: 'csb-bar', style: { flexWrap: 'wrap', gap: 6 } },
2216
+ btn('改优先级 / 截止 / 复杂度 / 付费', 'basic'),
2217
+ btn('改目标与边界', 'scope'),
2218
+ h('button', { className: 'csb-btn', disabled: Boolean(busy), onClick: () => onReassign?.() },
2219
+ busy === '换负责人' ? '换负责人中…' : '换负责人 / 重新派发'),
2220
+ h('button', { className: 'csb-btn', disabled: Boolean(busy), onClick: doRelease }, busy === '收回' ? '收回中…' : '收回任务'),
2221
+ btn(task.status === 'blocked' ? '解除阻塞' : '阻塞任务', 'block'),
2222
+ h('button', { className: 'csb-btn csb-btn-d', disabled: Boolean(busy), onClick: doArchive, title: '归档=从任务栏移除(可恢复)' }, '归档'),
2223
+ btn('编辑长任务阶段清单', 'phases')),
2224
+
2225
+ err ? h('div', { className: 'csb-err' }, err.text) : null,
2226
+ done ? h(Notice, { kind: 'ok', onClose: () => setDone(null) }, done) : null,
2227
+
2228
+ // (a) 排期与路由
2229
+ form === 'basic'
2230
+ ? h('div', { className: 'csb-fld', style: { marginTop: 10 } },
2231
+ h('div', { className: 'csb-bar', style: { gap: 10, flexWrap: 'wrap' } },
2232
+ h('label', null, '优先级',
2233
+ h('input', {
2234
+ className: 'csb-in', type: 'number', min: 1, max: 10, style: { width: 80, marginLeft: 6 },
2235
+ value: priority, onChange: (e) => setPriority(e.target.value),
2236
+ })),
2237
+ h('label', null, '截止',
2238
+ h('input', {
2239
+ className: 'csb-in', type: 'date', style: { marginLeft: 6 },
2240
+ value: dueAt, onChange: (e) => setDueAt(e.target.value),
2241
+ })),
2242
+ h('label', null, '复杂度',
2243
+ h('select', {
2244
+ className: 'csb-in', style: { marginLeft: 6 },
2245
+ value: complexity, onChange: (e) => setComplexity(e.target.value),
2246
+ },
2247
+ h('option', { value: '' }, '不设'),
2248
+ ['T0', 'T1', 'T2', 'T3'].map((t) => h('option', { key: t, value: t }, t)))),
2249
+ h('label', null, '付费渠道',
2250
+ h('select', {
2251
+ className: 'csb-in', style: { marginLeft: 6 },
2252
+ value: paid, onChange: (e) => setPaid(e.target.value),
2253
+ },
2254
+ h('option', { value: '' }, '不设'),
2255
+ h('option', { value: 'no' }, '不用(免费)'),
2256
+ h('option', { value: 'yes' }, '用(花 Boss 的钱)')))),
2257
+ h('div', { className: 'csb-fld-h' },
2258
+ '优先级 1-10(≥8 算紧急);截止留空=清除;复杂度 T0 简单 / T1 常规 / T2 较难 / T3 很难;付费勾了就走付费渠道。'),
2259
+ h('button', {
2260
+ className: 'csb-btn csb-btn-p', disabled: Boolean(busy), onClick: saveBasic,
2261
+ }, busy === '保存' ? '保存中…' : '保存'))
2262
+ : null,
2263
+
2264
+ // (b) 目标与边界
2265
+ form === 'scope'
2266
+ ? h('div', { className: 'csb-fld', style: { marginTop: 10 } },
2267
+ h('label', null, '任务标题'),
2268
+ h('input', { className: 'csb-in', value: subject, onChange: (e) => setSubject(e.target.value) }),
2269
+ h('label', null, '目标与细节'),
2270
+ h('textarea', {
2271
+ className: 'csb-ta', value: description,
2272
+ onChange: (e) => setDescription(e.target.value),
2273
+ placeholder: '写清做完是什么样、验收口径是什么',
2274
+ }),
2275
+ h('label', null, '写入边界(每行一个)'),
2276
+ h('textarea', {
2277
+ className: 'csb-ta', value: scopes,
2278
+ onChange: (e) => setScopes(e.target.value),
2279
+ placeholder: 'src/api.js\nsrc/client.js',
2280
+ }),
2281
+ h('div', { className: 'csb-fld-h' },
2282
+ '写入边界=这个任务允许改的文件前缀,每行一个。留空=不限。'),
2283
+ h('button', {
2284
+ className: 'csb-btn csb-btn-p', disabled: Boolean(busy), onClick: saveScope,
2285
+ }, busy === '保存目标与边界' ? '保存中…' : '保存'))
2286
+ : null,
2287
+
2288
+ // (d) 阻塞
2289
+ form === 'block'
2290
+ ? h('div', { className: 'csb-fld', style: { marginTop: 10 } },
2291
+ task.status === 'blocked'
2292
+ ? h('div', null,
2293
+ h('div', { className: 'csb-fld-h' }, '这个任务现在是阻塞状态。解除后执行者可以继续做。'),
2294
+ h('button', { className: 'csb-btn csb-btn-p', disabled: Boolean(busy), onClick: doUnblock },
2295
+ busy === '解除阻塞' ? '处理中…' : '解除阻塞'))
2296
+ : h('div', null,
2297
+ h('label', null, '阻塞原因'),
2298
+ h('input', {
2299
+ className: 'csb-in', value: blockReason,
2300
+ onChange: (e) => setBlockReason(e.target.value),
2301
+ placeholder: '例如:等 Boss 确认接口口径',
2302
+ }),
2303
+ h('div', { className: 'csb-fld-h' }, '阻塞后会记下原因,解阻塞时执行者能看到。'),
2304
+ h('button', { className: 'csb-btn csb-btn-d', disabled: Boolean(busy), onClick: doBlock },
2305
+ busy === '阻塞' ? '处理中…' : '确认阻塞')))
2306
+ : null,
2307
+
2308
+ // (e) 阶段清单
2309
+ form === 'phases'
2310
+ ? h('div', { className: 'csb-fld', style: { marginTop: 10 } },
2311
+ h('label', null, '阶段清单(每行一个阶段,最多 12 项)'),
2312
+ h('textarea', {
2313
+ className: 'csb-ta', value: phaseText,
2314
+ onChange: (e) => setPhaseText(e.target.value),
2315
+ placeholder: '范围与验收标准确认\n执行与自测\n交付与复盘',
2316
+ }),
2317
+ h('div', { className: 'csb-fld-h' },
2318
+ '已经汇报过的阶段产物不会丢;清单变短时旧产物保留但不再展示。'),
2319
+ h('button', {
2320
+ className: 'csb-btn csb-btn-p', disabled: Boolean(busy), onClick: savePhases,
2321
+ }, busy === '保存阶段清单' ? '保存中…' : '保存阶段清单'))
2322
+ : null)
2323
+ }
2324
+
2325
+ /** epoch ms → `YYYY-MM-DD`(`<input type=date>` 的取值格式,本地时区)。 */
2326
+ function toDateInput(ms) {
2327
+ if (!ms) return ''
2328
+ const d = new Date(ms)
2329
+ if (Number.isNaN(d.getTime())) return ''
2330
+ const p = (n) => String(n).padStart(2, '0')
2331
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
2332
+ }
2333
+
2334
+ /** `YYYY-MM-DD` → epoch ms(当天 23:59:59 本地时区:截止=那天结束前都算没超)。 */
2335
+ function dateInputToMs(text) {
2336
+ if (!text) return null
2337
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(text).trim())
2338
+ if (!m) return null
2339
+ const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), 23, 59, 59, 999)
2340
+ return Number.isNaN(d.getTime()) ? null : d.getTime()
2341
+ }
2342
+
2343
+ /**
2344
+ * 任务详情 ·「阶段产出物」区块(契约 §2.5,需求3)。
2345
+ *
2346
+ * Boss 的原话:「点击去看产出物,看不到过程产物」⇒ 必须能按阶段看到每阶段交了
2347
+ * 什么东西,**不需要去翻对话**。
2348
+ *
2349
+ * 数据来自 `GET /tasks/phases?id=`;后端未就绪 / 读取失败都要降级成**人话提示**
2350
+ * (带重试),不许白屏、不许静默。非长任务是**正常空态**,不是错误。
2351
+ *
2352
+ * 产出物复用「产出物证据」那套渲染:路径可预览(`onPreview`)→ `FilePreviewModal`,
2353
+ * URL 走 `window.open`。
2354
+ */
2355
+ function TaskPhasePanel({ taskId, onPreview }) {
2356
+ const phasesQ = useApi(taskId ? `/tasks/phases?id=${encodeURIComponent(taskId)}` : null, [taskId])
2357
+ const data = phasesQ.data
2358
+ const phases = Array.isArray(data?.phases) ? data.phases : []
2359
+ const isLongrun = data?.isLongrun === true
2360
+ const phaseLabel = (st) => (st === 'done' ? '已完成' : st === 'blocked' ? '卡住' : st === 'doing' ? '进行中' : '未开始')
2361
+ const phaseBadgeCls = (st) => (st === 'done' ? 'csb-b csb-b-ok' : st === 'blocked' ? 'csb-b csb-b-err'
2362
+ : st === 'doing' ? 'csb-b csb-b-run' : 'csb-b csb-b-idle')
2363
+ const ordinal = (i) => '\u2460\u2461\u2462\u2463\u2464\u2465\u2466\u2467\u2468\u2469\u246a\u246b'[i] ?? `${i + 1}.`
2364
+
2365
+ // 非长任务:契约 §2.5 明说这不是错误 —— 空态 + 一句人话指向总产出物。
2366
+ if (!phasesQ.loading && !phasesQ.error && data && !isLongrun) {
2367
+ return h(Section, { title: '阶段产出物' },
2368
+ h(Empty, {
2369
+ icon: '\u25dc',
2370
+ title: '这不是长任务,没有分阶段产物',
2371
+ hint: '总产出物见下方「产出物证据」。',
2372
+ }))
2373
+ }
2374
+
2375
+ // 每阶段的产出物行:路径可预览、URL 可打开,绝不给不可点的死文本。
2376
+ const artifactRow = (a, key) => h('div', { key, className: 'csb-bar', style: { gap: 6, alignItems: 'center' } },
2377
+ h('span', { className: 'csb-card-t csb-mono', style: { fontSize: 12 } },
2378
+ typeof a === 'string' ? a : JSON.stringify(a)),
2379
+ isUrl(a)
2380
+ ? h('button', {
2381
+ className: 'csb-btn csb-btn-s', style: { height: 22, fontSize: 11 },
2382
+ onClick: () => window.open(a, '_blank', 'noopener'),
2383
+ }, '打开链接')
2384
+ : h('button', {
2385
+ className: 'csb-btn csb-btn-s', style: { height: 22, fontSize: 11 },
2386
+ onClick: () => onPreview?.(a),
2387
+ }, '预览文件'))
2388
+
2389
+ const phaseCard = (p, i) => {
2390
+ const idx = p.index ?? i
2391
+ const arts = p.artifacts ?? []
2392
+ return h('div', { key: idx, className: 'csb-card', style: { padding: '9px 12px' } },
2393
+ h('div', { className: 'csb-card-main' },
2394
+ h('div', { className: 'csb-bar', style: { gap: 8 } },
2395
+ h('span', { className: 'csb-card-t' }, `${ordinal(idx)} ${p.title ?? `阶段 ${idx + 1}`}`),
2396
+ h('span', {
2397
+ className: phaseBadgeCls(p.state),
2398
+ title: p.state === 'blocked' ? '执行者报告这一阶段卡住了' : '',
2399
+ }, phaseLabel(p.state))),
2400
+ p.note ? h('div', { className: 'csb-card-m' }, p.note) : null,
2401
+ arts.length === 0
2402
+ ? h('div', { className: 'csb-set-d' }, '这一阶段没交产出物。')
2403
+ : h('div', { className: 'csb-card-m' }, arts.map((a, j) => artifactRow(a, j))),
2404
+ p.by ? h('div', { className: 'csb-set-d' }, `汇报人:${humanizeBy(p.by)}`) : null))
2405
+ }
2406
+
2407
+ return h('div', null,
2408
+ h(Section, {
2409
+ title: '阶段产出物',
2410
+ desc: isLongrun
2411
+ ? `共 ${phases.length} 个阶段。每阶段交了什么直接在这里看,不用去翻对话。`
2412
+ : '执行者按阶段汇报产出物,交一次记一次;没有阶段记录时这里显示空态。',
2413
+ }),
2414
+ // 后端未就绪 / 读取失败:可读提示 + 重试(绝不静默)
2415
+ phasesQ.error
2416
+ ? h('div', { className: 'csb-note' },
2417
+ `阶段产出物读不到:${phasesQ.error}`,
2418
+ h('button', {
2419
+ className: 'csb-btn csb-btn-s', style: { marginLeft: 8 },
2420
+ onClick: () => phasesQ.reload(),
2421
+ }, '重试'))
2422
+ : null,
2423
+ phasesQ.loading ? h(Skeleton, { n: 1 }) : null,
2424
+ (!phasesQ.loading && !phasesQ.error && phases.length === 0)
2425
+ ? h(Empty, {
2426
+ icon: '\u25dc',
2427
+ title: '执行者还没汇报这一阶段。',
2428
+ hint: '执行者每完成一个阶段会汇报一次产出物;目前还没有任何阶段记录。',
2429
+ })
2430
+ : null,
2431
+ phases.length > 0
2432
+ ? h('div', { className: 'csb-list', style: { marginTop: 10 } }, phases.map(phaseCard))
2433
+ : null)
2434
+ }
2435
+
1976
2436
  /**
1977
2437
  * 任务对话(需求2,2026-11-09)。
1978
2438
  *
@@ -2123,6 +2583,19 @@ window.__ModuleLoader__.load({
2123
2583
  }
2124
2584
  }
2125
2585
 
2586
+ /**
2587
+ * 机器标识 → 面向 Boss 的展示文案(契约 §0)。
2588
+ *
2589
+ * `(ui)` / `by: 'ui'` 这类机器标识可以留在技术字段里,但**面向 Boss 的展示文案**
2590
+ * 要写 `Boss`(例:补充说明历史里的 `(ui)` → `(Boss)`)。
2591
+ * 非 Boss 机器身份(Bot / 其他会话)保持原文 —— 不猜、不美化。
2592
+ */
2593
+ function humanizeBy(raw) {
2594
+ const v = raw ?? 'ui'
2595
+ if (v === 'ui' || v === 'user' || v === 'human' || v === 'boss' || v === 'anyone') return HUMAN_LABEL
2596
+ return String(v)
2597
+ }
2598
+
2126
2599
  /**
2127
2600
  * 一条变更记录 → `[时间] 动作 — 说明(by)`(契约 §3.1)。
2128
2601
  * 快照记录没有 note:用「上一轮交付物 N 项」兜底,避免历史里出现空动作。
@@ -2135,8 +2608,7 @@ window.__ModuleLoader__.load({
2135
2608
  note = `上一轮交付物 ${arts.length} 项`
2136
2609
  }
2137
2610
  if (!note && ch?.kind === 'reassign') note = `${ch?.from ?? '?'} → ${ch?.to ?? '?'}`
2138
- const by = ch?.by ?? ch?.byId ?? 'ui'
2139
- return `[${fmt(ch?.at)}] ${label}${note ? ` — ${note}` : ''}(${by})`
2611
+ return `[${fmt(ch?.at)}] ${label}${note ? ` — ${note}` : ''}(${humanizeBy(ch?.by ?? ch?.byId)})`
2140
2612
  }
2141
2613
 
2142
2614
  /** `POST /tasks/evolve`:三种 mode 的唯一入口(note 必填,前端先拦一次)。 */
@@ -2400,7 +2872,7 @@ window.__ModuleLoader__.load({
2400
2872
  liveBots.map((b) => h('option', { key: b.id ?? b.sessionId, value: b.sessionId ?? b.id },
2401
2873
  `${b.name ?? '(无名)'}${b.state ? ` · ${b.state}` : ''}`))),
2402
2874
  h('div', { className: 'csb-fld-h' },
2403
- '换人会先把原负责人的租约交还,再以新负责人身份领取,并唤醒新负责人。'
2875
+ '换人会先把原负责人的占用交还,再以新负责人身份领取,并唤醒新负责人。'
2404
2876
  + '已完成的任务会被拦 —— 先重开或派生优化单。')),
2405
2877
  err ? h('div', { className: 'csb-err', style: { marginTop: 10 } }, err) : null,
2406
2878
  done ? h(Notice, { kind: 'ok' }, done) : null)
@@ -2570,7 +3042,7 @@ window.__ModuleLoader__.load({
2570
3042
  value: text, onChange: (e) => setText(e.target.value),
2571
3043
  }),
2572
3044
  h('div', { className: 'csb-fld-h' },
2573
- `等价于你在这个 Bot 的会话里发一条消息(会话 ${short(bot?.sessionId ?? '—', 18)})。`)),
3045
+ `等价于 Boss 在这个 Bot 的会话里发一条消息(会话 ${short(bot?.sessionId ?? '—', 18)})。`)),
2574
3046
  err ? h('div', { className: 'csb-err' }, err) : null,
2575
3047
  done && !done.wake
2576
3048
  ? h('div', { className: 'csb-ok' }, `已送达(会话 ${short(done.sessionId ?? '—', 18)})`)
@@ -2665,7 +3137,7 @@ window.__ModuleLoader__.load({
2665
3137
  /**
2666
3138
  * 任务看板。
2667
3139
  *
2668
- * 需求1(2026-11-08):任务页**默认只展示人建任务**;智能体自建/分发的任务
3140
+ * 需求1(2026-11-08):任务页**默认只展示 Boss 建任务**;智能体自建/分发的任务
2669
3141
  * 走独立子页签(`sourceFilter='ai'`),按状态分类展示。
2670
3142
  *
2671
3143
  * 为什么这样分:人关心的是「我要盯的活」,智能体之间互相派的活是执行细节——
@@ -2704,8 +3176,7 @@ window.__ModuleLoader__.load({
2704
3176
  const [err, setErr] = useState(null)
2705
3177
  // 创建/派活的成功提示(改造04:创建时填了负责人 = 一步派活,wake 结果要 toast 出来)
2706
3178
  const [ok, setOk] = useState(null)
2707
- // 默认只看「需要我管的」;点「看全部」才显示 AI 派发的和已完成的
2708
- const [attentionOnly, setAttentionOnly] = useState(true)
3179
+ // 契约 §1:任务栏默认全量 —— 不再区分「需要管 / 全部」,也不带 attention 过滤。
2709
3180
  // 双视图:看板 / 列表
2710
3181
  const [view, setView] = useState('list')
2711
3182
  // 本地过滤:负责人 / 关键词(不改后端 query)
@@ -2714,11 +3185,11 @@ window.__ModuleLoader__.load({
2714
3185
  const subjectRef = useRef(null)
2715
3186
  const q = new URLSearchParams()
2716
3187
  if (filter) q.set('status', filter)
2717
- if (attentionOnly && sourceFilter === 'human') q.set('attention', '1')
2718
- // 需求1:按来源分离——human=用户自建,ai=智能体自建/分发
2719
- q.set('source', sourceFilter)
3188
+ // 契约 §1:任务栏默认全量 ⇒ 不带 attention、不带 source;
3189
+ // 只有「智能体任务」子页签显式要求 source=ai。
3190
+ if (sourceFilter === 'ai') q.set('source', 'ai')
2720
3191
  const qs = q.toString()
2721
- const { data, loading, error, reload } = useApi(`/tasks${qs ? `?${qs}` : ''}`, [filter, attentionOnly, sourceFilter])
3192
+ const { data, loading, error, reload } = useApi(`/tasks${qs ? `?${qs}` : ''}`, [filter, sourceFilter])
2722
3193
  const { data: rosterData } = useApi('/roster')
2723
3194
  const { data: wfData } = useApi('/library/workflows')
2724
3195
  const bots = rosterData?.bots ?? []
@@ -2738,7 +3209,7 @@ window.__ModuleLoader__.load({
2738
3209
 
2739
3210
  // 「下一步」按钮的落点(spec §8):按错误码给可执行动作
2740
3211
  const errActions = {
2741
- BOARD_WIP_EXCEEDED: () => { setFilter(''); setAttentionOnly(true) },
3212
+ BOARD_WIP_EXCEEDED: () => { setFilter(''); setOwnerFilter(''); setKeyword('') },
2742
3213
  BOARD_REV_CONFLICT: reload,
2743
3214
  CAS_MISMATCH: reload,
2744
3215
  // 查看持有者 / 前置任务 / 依赖图 → 打开任务详情抽屉
@@ -2840,18 +3311,12 @@ window.__ModuleLoader__.load({
2840
3311
  const isAi = sourceFilter === 'ai'
2841
3312
  return h('div', { className: 'csb-wrap' },
2842
3313
  h(Section, {
2843
- // 需求1:按来源给不同标题与说明——人只看自己建的,AI 的任务走独立子页签
2844
- title: isAi ? '智能体任务' : `${HUMAN_LABEL}的任务`,
3314
+ // 契约 §1:顶层保留两个子页签(任务栏 / 智能体任务),页内不再有「看全部 / 只看需要管」切换。
3315
+ title: isAi ? '智能体任务' : '任务栏',
2845
3316
  desc: isAi
2846
- ? `Bot 自行创建或分发的任务,按状态分类。这些是执行细节,平时不用盯;出问题(受阻/失败)时会出现在「${HUMAN_LABEL}的任务」的待处理里。`
2847
- : (attentionOnly
2848
- ? '只看需要你管的(卡住 / 高优先 / 逾期 / 待验收)。智能体自己派发的任务在「智能体任务」子页签。'
2849
- : '全部你建的任务。点下方回到「只看需要我管的」。'),
3317
+ ? 'Bot 自行创建或分发的任务,按状态分类。这些是执行细节,平时不用盯;出问题(受阻/失败)时会出现在「任务栏」里。'
3318
+ : '全部任务都在这里:Boss 建的、智能体派发的、以及已经完成的。',
2850
3319
  }),
2851
- isAi ? null : h('div', { className: 'csb-bar', style: { marginTop: 8 } },
2852
- attentionOnly
2853
- ? h('button', { className: 'csb-btn csb-btn-s', onClick: () => setAttentionOnly(false) }, `看全部${HUMAN_LABEL}的任务`)
2854
- : h('button', { className: 'csb-btn csb-btn-s', onClick: () => setAttentionOnly(true) }, '只看需要我管的')),
2855
3320
  h('div', { className: 'csb-bar', style: { marginTop: 12 } },
2856
3321
  h('input', {
2857
3322
  className: 'csb-in csb-grow', placeholder: isLong
@@ -2980,7 +3445,7 @@ window.__ModuleLoader__.load({
2980
3445
  onChange: (e) => setMandateBasis(e.target.value),
2981
3446
  },
2982
3447
  h('option', { value: '' }, '不记录(UI 建的,不算授权)'),
2983
- h('option', { value: 'explicit' }, '人明确点名要做(可以直接动手)'),
3448
+ h('option', { value: 'explicit' }, 'Boss 明确点名要做(可以直接动手)'),
2984
3449
  h('option', { value: 'inferred' }, '按规则推断(动手前先确认)'),
2985
3450
  h('option', { value: 'task-board' }, '任务板派发(管理面正式派活)')),
2986
3451
  h('div', { className: 'csb-fld-h' },
@@ -2993,7 +3458,7 @@ window.__ModuleLoader__.load({
2993
3458
  onChange: (e) => setRequiresPaid(e.target.checked),
2994
3459
  }),
2995
3460
  h('span', null, '允许付费(默认只走免费渠道)')),
2996
- h('div', { className: 'csb-fld-h' }, '只有确实需要强模型时才勾;勾了就是花你的钱。')))
3461
+ h('div', { className: 'csb-fld-h' }, '只有确实需要强模型时才勾;勾了就是花 Boss 的钱。')))
2997
3462
  : null,
2998
3463
  h('div', { className: 'csb-bar', style: { marginTop: 8, flexWrap: 'wrap' } },
2999
3464
  h('button', {
@@ -3076,7 +3541,7 @@ window.__ModuleLoader__.load({
3076
3541
  si ? h('div', { style: { marginTop: 4, display: 'flex', gap: 6, flexWrap: 'wrap' } },
3077
3542
  h('span', { className: stageBadgeCls(si.group) }, si.label),
3078
3543
  si.needsPerson
3079
- ? h('span', { className: 'csb-b csb-b-run', title: si.reason }, '需你处理')
3544
+ ? h('span', { className: 'csb-b csb-b-run', title: si.reason }, '需 Boss 处理')
3080
3545
  : null) : null,
3081
3546
  h('div', { className: 'csb-progress' },
3082
3547
  h('div', { className: 'csb-progress-fill', style: { width: `${Math.max(0, Math.min(100, t.progress ?? 0))}%` } })),
@@ -3100,7 +3565,7 @@ window.__ModuleLoader__.load({
3100
3565
 
3101
3566
  const SKILL_SOURCES = [
3102
3567
  { id: 'custom', label: '自建' },
3103
- { id: 'user', label: '用户级' },
3568
+ { id: 'user', label: 'Boss 级' },
3104
3569
  { id: 'project', label: '项目级' },
3105
3570
  { id: 'bundled', label: '系统自带' },
3106
3571
  ]
@@ -3120,7 +3585,7 @@ window.__ModuleLoader__.load({
3120
3585
  skill ? h('div', null,
3121
3586
  h('div', { className: 'csb-kv', style: { marginBottom: 10 } },
3122
3587
  h('span', { className: 'csb-k' }, '描述'), h('span', { className: 'csb-v' }, skill.description || '—'),
3123
- h('span', { className: 'csb-k' }, '来源'), h('span', { className: 'csb-v' }, skill.source === 'bundled' ? '系统自带' : skill.source === 'custom' ? '自建' : skill.source === 'user' ? '用户级' : '项目级')),
3588
+ h('span', { className: 'csb-k' }, '来源'), h('span', { className: 'csb-v' }, skill.source === 'bundled' ? '系统自带' : skill.source === 'custom' ? '自建' : skill.source === 'user' ? 'Boss 级' : '项目级')),
3124
3589
  h('div', { className: 'csb-pre', style: { maxHeight: 400, overflow: 'auto' } }, skill.body || '(空)'))
3125
3590
  : null,
3126
3591
  ],
@@ -3899,7 +4364,7 @@ window.__ModuleLoader__.load({
3899
4364
  error ? h('div', { className: 'csb-err' }, error) : null,
3900
4365
  !loading && bots.length === 0
3901
4366
  ? h(Empty, {
3902
- icon: '⬡', title: '还没有 Bot', hint: '先在上面建一个。如果只是临时分工,不用建 Bot —— 直接建任务指派给人就行。',
4367
+ icon: '⬡', title: '还没有 Bot', hint: '先在上面建一个。如果只是临时分工,不用建 Bot —— 直接建任务指派给 Bot 就行。',
3903
4368
  action: { label: '新建 Bot', icon: '✎', onClick: () => nameRef.current?.focus() },
3904
4369
  })
3905
4370
  : null,
@@ -4052,7 +4517,7 @@ window.__ModuleLoader__.load({
4052
4517
  h('span', { className: 'csb-k' }, '专家身份'), h('span', { className: 'csb-v' }, bot.preset ?? '默认'),
4053
4518
  h('span', { className: 'csb-k' }, '档案文件'), h('span', { className: 'csb-v csb-mono' }, data.profilePath ?? '—')),
4054
4519
  data.hasOutside
4055
- ? h('div', { className: 'csb-note' }, '你的 AGENTS.md 里还有手写内容 —— 保存档案不会覆盖它,只改 cs-bots 托管的那一段。')
4520
+ ? h('div', { className: 'csb-note' }, 'Boss 的 AGENTS.md 里还有手写内容 —— 保存档案不会覆盖它,只改 cs-bots 托管的那一段。')
4056
4521
  : null,
4057
4522
 
4058
4523
  h(Section, { title: '① 岗位名', desc: '一句话说清它是什么角色。会作为档案里的小标题显示。' }),
@@ -4062,7 +4527,7 @@ window.__ModuleLoader__.load({
4062
4527
  value: draft.title, onChange: (e) => setDraft({ ...draft, title: e.target.value }),
4063
4528
  }),
4064
4529
 
4065
- h(Section, { title: '② 人格与说话方式', desc: '它该用什么性格、什么口吻跟你说话。不填就用默认的。' }),
4530
+ h(Section, { title: '② 人格与说话方式', desc: '它该用什么性格、什么口吻跟 Boss 说话。不填就用默认的。' }),
4066
4531
  h('textarea', {
4067
4532
  className: 'csb-ta', style: { marginTop: 10 },
4068
4533
  placeholder: '例如:说话先给结论再列证据;不确定就明说不知道,不许编;不要用"作为一个 AI"这类腔调。',
@@ -4079,11 +4544,11 @@ window.__ModuleLoader__.load({
4079
4544
  h(Section, { title: '④ 身份', desc: '它是谁、负责什么。这段话会在每次对话开始前进到它的上下文里。' }),
4080
4545
  h('textarea', {
4081
4546
  className: 'csb-ta', style: { marginTop: 10 },
4082
- placeholder: '例如:你是本项目的后端工程师,负责 API 设计、数据模型与数据库迁移;对 SQL 性能负责;拿不准时先给方案再动手。',
4547
+ placeholder: '例如:它是本项目的后端工程师,负责 API 设计、数据模型与数据库迁移;对 SQL 性能负责;拿不准时先给方案再动手。',
4083
4548
  value: draft.identity, onChange: (e) => setDraft({ ...draft, identity: e.target.value }),
4084
4549
  }),
4085
4550
 
4086
- h(Section, { title: '⑤ 经验与规矩', desc: '它踩过的坑、你定下的规矩。写在这里的每一条,它每次都会看到。' }),
4551
+ h(Section, { title: '⑤ 经验与规矩', desc: '它踩过的坑、Boss 定下的规矩。写在这里的每一条,它每次都会看到。' }),
4087
4552
  h('textarea', {
4088
4553
  className: 'csb-ta', style: { marginTop: 10, minHeight: 100 },
4089
4554
  placeholder: '例如:改数据库结构必须先写迁移;禁止直接 force push;测试必须全绿才算完成。',
@@ -4403,7 +4868,7 @@ window.__ModuleLoader__.load({
4403
4868
  h(Section, {
4404
4869
  title: '房间 = 群聊',
4405
4870
  desc: '房间就是一群成员共用的消息流。成员是「会话」,消息可以直接转发去唤醒某个 Bot。'
4406
- + '你是超级管理员:所有房间都能进、都能看全文、都能改设置。',
4871
+ + 'Boss 是超级管理员:所有房间都能进、都能看全文、都能改设置。',
4407
4872
  }),
4408
4873
  h('div', { className: 'csb-bar', style: { marginTop: 12 } },
4409
4874
  h('input', {
@@ -4474,7 +4939,7 @@ window.__ModuleLoader__.load({
4474
4939
  h('div', { className: 'csb-room-chat' },
4475
4940
  h(Section, {
4476
4941
  title: `消息 · ${current.name}`,
4477
- desc: `共 ${msgs.length} 条,当前显示最近 ${Math.min(shownCount, msgs.length)} 条。发帖者必须是成员;你是超级管理员,可以直接看全部内容。`,
4942
+ desc: `共 ${msgs.length} 条,当前显示最近 ${Math.min(shownCount, msgs.length)} 条。发帖者必须是成员;Boss 是超级管理员,可以直接看全部内容。`,
4478
4943
  }),
4479
4944
  current.purpose ? h('div', { className: 'csb-note', style: { marginTop: 10 } }, `用途:${current.purpose}`) : null,
4480
4945
  current.notice ? h('div', { className: 'csb-note', style: { marginTop: 10 } }, current.notice) : null,
@@ -4527,6 +4992,11 @@ window.__ModuleLoader__.load({
4527
4992
  onKeyDown: (e) => e.key === 'Enter' && post(),
4528
4993
  }),
4529
4994
  h('button', { className: 'csb-btn csb-btn-p', disabled: !text.trim(), onClick: post }, '发送')),
4995
+ // 契约 t-38 ③:mentionOnly 缺省 true 时提示「@ 才唤醒」
4996
+ current.settings?.mentionOnly !== false
4997
+ ? h('div', { className: 'csb-set-d', style: { fontSize: 12, marginTop: 4 } },
4998
+ '只有被 @ 的人会被唤醒;不 @ 就只记录不打扰。')
4999
+ : null,
4530
5000
  // ── 右栏:管理 ──
4531
5001
  h('div', { className: 'csb-room-side' },
4532
5002
  h(Section, { title: '房间信息', desc: '基本信息和设置' }),
@@ -4624,6 +5094,8 @@ window.__ModuleLoader__.load({
4624
5094
  allowBotAutoPost: room.settings?.allowBotAutoPost !== false,
4625
5095
  maxBotsPerRound: room.settings?.maxBotsPerRound ?? 3,
4626
5096
  relayEnabled: room.settings?.relayEnabled !== false,
5097
+ // 契约 t-38 ③:缺省 true —— 不 @ 就只落消息、不唤醒任何人
5098
+ mentionOnly: room.settings?.mentionOnly !== false,
4627
5099
  })
4628
5100
  const save = async () => {
4629
5101
  try {
@@ -4641,6 +5113,7 @@ window.__ModuleLoader__.load({
4641
5113
  allowBotAutoPost: settings.allowBotAutoPost,
4642
5114
  maxBotsPerRound: Number(settings.maxBotsPerRound) || 3,
4643
5115
  relayEnabled: settings.relayEnabled,
5116
+ mentionOnly: settings.mentionOnly,
4644
5117
  },
4645
5118
  byId: 'user',
4646
5119
  },
@@ -4683,14 +5156,14 @@ window.__ModuleLoader__.load({
4683
5156
  onChange: (e) => setForm({ ...form, adminId: e.target.value }),
4684
5157
  }))
4685
5158
  : null,
4686
- h(Field, { label: '房间公告', hint: '会显示在消息列表上方,所有进这个房间的人都能看到' },
5159
+ h(Field, { label: '房间公告', hint: '会显示在消息列表上方,所有进这个房间的成员都能看到' },
4687
5160
  h('textarea', {
4688
5161
  className: 'csb-ta', value: form.notice,
4689
5162
  placeholder: '例如:本房间只讨论发布相关事项;发版前 24 小时冻结代码。',
4690
5163
  onChange: (e) => setForm({ ...form, notice: e.target.value }),
4691
5164
  })),
4692
5165
  h('h4', { style: { marginTop: 18 } }, '房间规则'),
4693
- h(Field, { label: '允许 Bot 自己发言', hint: '关掉后只有人能主动发消息,Bot 只能被动回复' },
5166
+ h(Field, { label: '允许 Bot 自己发言', hint: '关掉后只有 Boss 能主动发消息,Bot 只能被动回复' },
4694
5167
  h('label', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
4695
5168
  h('input', {
4696
5169
  type: 'checkbox', checked: settings.allowBotAutoPost,
@@ -4706,7 +5179,14 @@ window.__ModuleLoader__.load({
4706
5179
  h('input', {
4707
5180
  type: 'checkbox', checked: settings.relayEnabled,
4708
5181
  onChange: (e) => setSettings({ ...settings, relayEnabled: e.target.checked }),
4709
- }), settings.relayEnabled ? '允许' : '不允许')))
5182
+ }), settings.relayEnabled ? '允许' : '不允许')),
5183
+ // 契约 t-38 ③:只有被 @ 的人才被唤醒;不 @ 就只记录不打扰(缺省开)
5184
+ h(Field, { label: '只有被 @ 的人才被唤醒', hint: '关掉后房间每条消息都会唤醒所有 Bot(不推荐,实测会烧 token)' },
5185
+ h('label', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
5186
+ h('input', {
5187
+ type: 'checkbox', checked: settings.mentionOnly,
5188
+ onChange: (e) => setSettings({ ...settings, mentionOnly: e.target.checked }),
5189
+ }), settings.mentionOnly ? '开启' : '关闭')))
4710
5190
  }
4711
5191
 
4712
5192
  // ── 成果(artifacts 聚合)────────────────────────────────────────────────
@@ -4993,12 +5473,12 @@ window.__ModuleLoader__.load({
4993
5473
  return h('div', { className: 'csb-wrap' },
4994
5474
  h(Section, {
4995
5475
  title: 'PDCA 复盘周期',
4996
- desc: 'PDCA 是给人的长任务做复盘跟进的,人类主导——五步不可跳,这正是「不自欺」的纪律。智能体有自己的归纳周期(周期驱动、自动闭环),折叠在下面。',
5476
+ desc: 'PDCA 是给 Boss 的长任务做复盘跟进的,Boss 主导——五步不可跳,这正是「不自欺」的纪律。智能体有自己的归纳周期(周期驱动、自动闭环),折叠在下面。',
4997
5477
  }),
4998
5478
 
4999
5479
  // ── 我的长任务周期(human)────────────────────────────────────────────
5000
5480
  h('div', { className: 'csb-sec-t' }, `📋 ${HUMAN_LABEL} 的任务周期`),
5001
- h('div', { className: 'csb-set-d' }, '为你的长任务开一个周期。目标原文是「防漂移锚」——复盘时拿它对照,看有没有走偏。'),
5481
+ h('div', { className: 'csb-set-d' }, '为长任务开一个周期。目标原文是「防漂移锚」——复盘时拿它对照,看有没有走偏。'),
5002
5482
  h('div', { className: 'csb-bar', style: { marginTop: 8, flexWrap: 'wrap', gap: 6 } },
5003
5483
  h('input', { className: 'csb-in', style: { width: 120 }, placeholder: '任务 id(t-x)', value: taskId, onChange: (e) => setTaskId(e.target.value) }),
5004
5484
  h('input', { className: 'csb-in', style: { width: 260 }, placeholder: '目标原文(防漂移锚)', value: objective, onChange: (e) => setObjective(e.target.value) }),
@@ -5012,7 +5492,7 @@ window.__ModuleLoader__.load({
5012
5492
 
5013
5493
  // ── 智能体归纳周期(agent)────────────────────────────────────────────
5014
5494
  h('div', { className: 'csb-sec-t', style: { marginTop: 18 } }, '🤖 智能体归纳周期'),
5015
- h('div', { className: 'csb-set-d' }, '智能体用来做定期归纳提升的周期。与人的周期不同:归纳是批量总结,可以一次性提交,提交后自动完成——不需要人点「完成」,也不需要假装走完五步。'),
5495
+ h('div', { className: 'csb-set-d' }, '智能体用来做定期归纳提升的周期。与 Boss 的周期不同:归纳是批量总结,可以一次性提交,提交后自动完成——不需要 Boss 点「完成」,也不需要假装走完五步。'),
5016
5496
  // 到期未归纳的会话:这是「定期归纳」真正被驱动的入口
5017
5497
  dueSessions.length > 0
5018
5498
  ? h('div', { className: 'csb-note', style: { marginTop: 8 } },
@@ -5029,7 +5509,7 @@ window.__ModuleLoader__.load({
5029
5509
  : null,
5030
5510
  urgeMsg ? h('div', { className: 'csb-notice csb-notice-ok', style: { marginTop: 8 } }, h('span', { className: 'csb-notice-t' }, urgeMsg)) : null,
5031
5511
  agentCycles.length === 0
5032
- ? h(Empty, { icon: '🧠', title: '智能体尚未开归纳周期', hint: '智能体通常自动创建归纳周期;如果你要手动触发,填任务 id 和归纳目标。' })
5512
+ ? h(Empty, { icon: '🧠', title: '智能体尚未开归纳周期', hint: '智能体通常自动创建归纳周期;Boss 要手动触发时,填任务 id 和归纳目标。' })
5033
5513
  : h('div', { className: 'csb-list' }, agentCycles.map((c) => renderCycle(c, true))),
5034
5514
  agentMsg ? h('div', { className: 'csb-notice csb-notice-ok', style: { marginTop: 8 } }, h('span', { className: 'csb-notice-t' }, agentMsg)) : null)
5035
5515
  }
@@ -5043,7 +5523,7 @@ window.__ModuleLoader__.load({
5043
5523
  }
5044
5524
  /** 下拉里的合法取值(顺序即页面顺序:安全 → 宽松 → 逃生阀)。 */
5045
5525
  const ADMIN_POLICY_KEYS = ['lead-only', 'any-member', 'open']
5046
- /** 团队默认策略:人建的团队一律默认「仅负责人可管理」。 */
5526
+ /** 团队默认策略:Boss 建的团队一律默认「仅负责人可管理」。 */
5047
5527
  const DEFAULT_ADMIN_POLICY = 'lead-only'
5048
5528
  /**
5049
5529
  * 团队页(需求3,2026-11-08 重做)。
@@ -5056,7 +5536,7 @@ window.__ModuleLoader__.load({
5056
5536
  *
5057
5537
  * ## 由此确定的三条设计
5058
5538
  *
5059
- * 1. **上层只显示「用户建的团队」**。智能体自建的小团队不再占用上层版面——
5539
+ * 1. **上层只显示「Boss 建的团队」**。智能体自建的小团队不再占用上层版面——
5060
5540
  * 它们是执行细节,要看请走「穿透」。所以本页 `list({ type:'big' })` 过滤,
5061
5541
  * 且 UI 上**完全不出现「大/小」字样**(用户明确说不需要这个概念)。
5062
5542
  *
@@ -5082,9 +5562,12 @@ window.__ModuleLoader__.load({
5082
5562
  * 房间页本身已支持列出全部房间 + 消息内容查看,超级管理员不受限制。
5083
5563
  */
5084
5564
  function TeamsTab({ onGoto }) {
5085
- // 只取用户建的团队(big);智能体自建的 small 不上上层
5565
+ // 只取 Boss 建的团队(big);智能体自建的 small 不上上层
5086
5566
  const listQ = useApi('/teams?type=big', [])
5087
5567
  const rosterQ = useApi('/roster')
5568
+ // 契约 §3(UI):团队卡片要显示 `房间:<名>` —— 房间名不在团队记录里,
5569
+ // 要拿 roomId 从 /rooms 列表里查出名字(没有房间就只有 roomId 显示)。
5570
+ const roomsQ = useApi('/rooms', [])
5088
5571
  const [name, setName] = useState('')
5089
5572
  const [leadId, setLeadId] = useState('')
5090
5573
  const [purpose, setPurpose] = useState('')
@@ -5109,6 +5592,13 @@ window.__ModuleLoader__.load({
5109
5592
  }
5110
5593
  const act = (p, body) => call(p, { method: 'POST', body })
5111
5594
 
5595
+ // 契约 第2批 ①:跨团队协作请求弹窗状态
5596
+ const [routeOpen, setRouteOpen] = useState(null)
5597
+ const [routeTarget, setRouteTarget] = useState('')
5598
+ const [routeText, setRouteText] = useState('')
5599
+ const [routeBusy, setRouteBusy] = useState(false)
5600
+ const [routeMsg, setRouteMsg] = useState(null)
5601
+
5112
5602
  const teams = listQ.data?.items ?? []
5113
5603
  // 后端下发的合法策略(/teams 返回 policies);万一旧后端没这个字段,就用本地常量兜底
5114
5604
  const policyKeys = listQ.data?.policies?.length ? listQ.data.policies : ADMIN_POLICY_KEYS
@@ -5118,6 +5608,14 @@ window.__ModuleLoader__.load({
5118
5608
  // 可用的 Bot(不含已退役)——成员与队长都从这里选
5119
5609
  const bots = (rosterQ.data?.bots ?? []).filter((b) => b.state !== 'retired')
5120
5610
  const botBySession = new Map(bots.map((b) => [b.sessionId, b]))
5611
+ // 契约 §3(UI):roomId → 房间名(团队卡片要显示 `房间:<名>`)。
5612
+ // 房间列表可能还没回来 / 房间被归档 —— 查不到就退回 roomId,绝不显示 "undefined"。
5613
+ const roomNameById = new Map((roomsQ.data?.rooms ?? []).map((r) => [r.id, r.name]))
5614
+ const roomLabelOf = (t) => {
5615
+ const id = typeof t?.roomId === 'string' ? t.roomId : ''
5616
+ if (!id) return ''
5617
+ return roomNameById.get(id) ?? id
5618
+ }
5121
5619
 
5122
5620
  const createTeam = async () => {
5123
5621
  if (!name.trim()) { setMsg('团队名必填'); return }
@@ -5132,12 +5630,13 @@ window.__ModuleLoader__.load({
5132
5630
  leadId,
5133
5631
  purpose: purpose.trim() || undefined,
5134
5632
  memberIds: selected,
5135
- // 人建的团队默认「仅负责人可管理」(服务端缺省是 open,为了不破坏既有调用面;
5633
+ // Boss 建的团队默认「仅负责人可管理」(服务端缺省是 open,为了不破坏既有调用面;
5136
5634
  // 页面是新入口,没有历史包袱,所以在这里给安全默认值)
5137
5635
  adminPolicy: DEFAULT_ADMIN_POLICY,
5138
5636
  by: 'ui',
5139
5637
  })
5140
- setMsg(`团队「${r.team.name}」已创建(负责人:${botBySession.get(leadId)?.name ?? leadId})`)
5638
+ setMsg(`团队「${r.team.name}」已创建(负责人:${botBySession.get(leadId)?.name ?? leadId})`
5639
+ + (r.room?.name ? ` · 议事厅:${r.room.name}` : (r.roomError ? ` · 议事厅未建好(${r.roomError})` : '')))
5141
5640
  setName(''); setLeadId(''); setPurpose(''); setSelected([])
5142
5641
  listQ.reload()
5143
5642
  } catch (e) { setMsg(String(e.message ?? e)) } finally { setBusy(false) }
@@ -5177,6 +5676,40 @@ window.__ModuleLoader__.load({
5177
5676
  } catch (e) { setMsg(String(e.message ?? e)) } finally { setBusy(false) }
5178
5677
  }
5179
5678
 
5679
+ // 契约 第2批 ①:跨团队发协作请求
5680
+ const sendRoute = async () => {
5681
+ if (!routeTarget) { setRouteMsg('请选择目标团队'); return }
5682
+ if (!routeText.trim()) { setRouteMsg('请填写请求内容'); return }
5683
+ const team = teams.find((t) => t.id === routeOpen)
5684
+ if (!team) { setRouteMsg('源团队不存在'); return }
5685
+ setRouteBusy(true); setRouteMsg(null)
5686
+ try {
5687
+ const r = await act('/teams/route', {
5688
+ fromTeam: routeOpen,
5689
+ toTeam: routeTarget,
5690
+ // taskId:服务层 `bot_team_route` 用它做环路去重键(`route:<from>:<to>:<taskId>`)。
5691
+ // 页面上的「发协作请求」不强制挂任务,所以这里不带 taskId,
5692
+ // 由服务层生成一个会话级临时 id(不挂具体任务 = 每一轮都是独立请求,不会被误去重)。
5693
+ text: routeText.trim(),
5694
+ hop: 1,
5695
+ by: 'ui',
5696
+ })
5697
+ if (r.ok && r.decision === 'delivered') {
5698
+ const toTeam = teams.find((t) => t.id === routeTarget)
5699
+ setRouteMsg(`已送达「${toTeam?.name ?? routeTarget}」的负责人(唤醒 1 次)`)
5700
+ setRouteText('')
5701
+ } else {
5702
+ const he = humanizeError({ code: r.decision, reason: r.reason, details: r.details })
5703
+ setRouteMsg(he.text + (he.hint ? ` —— ${he.hint}` : ''))
5704
+ }
5705
+ } catch (e) {
5706
+ const he = humanizeError(e)
5707
+ setRouteMsg(he.text + (he.hint ? ` —— ${he.hint}` : ''))
5708
+ } finally {
5709
+ setRouteBusy(false)
5710
+ }
5711
+ }
5712
+
5180
5713
  /**
5181
5714
  * 切换「谁能管理这个团队」(需求:管理员权限边界从页面控制)。
5182
5715
  *
@@ -5239,7 +5772,7 @@ window.__ModuleLoader__.load({
5239
5772
  return h('div', { className: 'csb-wrap' },
5240
5773
  h(Section, {
5241
5774
  title: '团队',
5242
- desc: '你创建的团队。每个团队有负责人(队长)、成员和讨论房间。'
5775
+ desc: 'Boss 创建的团队。每个团队有负责人(队长)、成员和讨论房间。'
5243
5776
  + '智能体自己组织的临时小组不在这里(点右上角「⚙ 穿透」可查)。',
5244
5777
  }),
5245
5778
 
@@ -5282,12 +5815,19 @@ window.__ModuleLoader__.load({
5282
5815
  const leadName = botBySession.get(t.leadId)?.name ?? (t.leadId ? short(t.leadId, 12) : '(未指定)')
5283
5816
  const pending = pendingMembers[t.id] ?? []
5284
5817
  const addable = bots.filter((b) => !members.includes(b.sessionId))
5818
+ // 契约 t-38 ①:徽标改文案。kind: 'human' | 'agent'(后端在加);
5819
+ // 旧数据无 kind ⇒ 按 createdBy 兜底(ui/user/person ⇒ Boss建设)。
5820
+ const teamKind = t.kind ?? (['ui','user','person'].includes(t.createdBy) ? 'human' : 'agent')
5821
+ const kindBadge = teamKind === 'human'
5822
+ ? h('span', { className: 'csb-b csb-b-run', title: 'Boss 建设的团队' }, '👤 Boss建设')
5823
+ : h('span', { className: 'csb-b csb-b-idle', title: '智能体建设的团队' }, '🤖 智能体建设')
5285
5824
  return h('div', { key: t.id, className: 'csb-card', style: { flexDirection: 'column', alignItems: 'stretch', marginBottom: 8 } },
5286
5825
  h('div', { className: 'csb-card-main' },
5287
5826
  h('div', { className: 'csb-card-t' },
5288
5827
  t.name,
5289
5828
  t.status === 'closed' ? h('span', { className: 'csb-b csb-b-idle', style: { marginLeft: 8 } }, '已关闭') : null,
5290
- t.purpose ? h('span', { className: 'csb-set-d' }, ` · ${t.purpose.slice(0, 40)}`) : null),
5829
+ t.purpose ? h('span', { className: 'csb-set-d' }, ` · ${t.purpose.slice(0, 40)}`) : null,
5830
+ h('span', { style: { marginLeft: 8 } }, kindBadge)),
5291
5831
 
5292
5832
  // 管理员权限边界(需求:管理员权限边界从页面控制)
5293
5833
  // 徽章:人要能一眼看出「谁能动这个团队」,所以常驻显示当前策略的中文名。
@@ -5371,27 +5911,104 @@ window.__ModuleLoader__.load({
5371
5911
  }))
5372
5912
  : null,
5373
5913
 
5374
- // 跳转房间(用户补充:可以穿透房间看子团队工作内容 / 定向搜索)
5914
+ // 契约 §3(UI):团队卡片显示 `房间:<名>` + 「去房间」按钮。
5915
+ // 房间名要从 /rooms 列表里查(团队记录只有 roomId);
5916
+ // 没有房间就直说,不能假装有。
5917
+ h('div', { className: 'csb-card-m' },
5918
+ h('span', { className: 'csb-b csb-b-run', title: '团队的议事厅(契约 §3)' }, '🏛 房间:'),
5919
+ h('span', null, roomLabelOf(t) || '(还没有房间)')),
5920
+
5921
+ // 跳转房间(契约 §3:选中该团队自己的房间,而不是按名字筛)
5375
5922
  h('div', { className: 'csb-bar', style: { marginTop: 8 } },
5376
5923
  h('button', {
5377
5924
  className: 'csb-btn csb-btn-s',
5378
- title: '查看这个团队的讨论房间与成员工作内容',
5925
+ title: '去这个团队的议事厅(选中该房间)',
5379
5926
  // 必须带 sub:'rooms' —— safeGoTab 会把 'rooms' 映射成主页签 'team',
5380
5927
  // 不带 sub 就落到该页签的第一个子视图(Bot 档案),这就是
5381
5928
  // 「点去房间却跳到 Bot 创建页」的真正原因。
5382
- onClick: () => onGoto?.('team', { sub: 'rooms', filter: t.name }),
5383
- }, '💬 看房间'),
5929
+ // 带 roomId ⇒ 房间页直接选中这个房间;没有 roomId 就退回按名字筛。
5930
+ onClick: () => onGoto?.(
5931
+ 'team',
5932
+ t.roomId
5933
+ ? { sub: 'rooms', roomId: t.roomId }
5934
+ : { sub: 'rooms', filter: t.name },
5935
+ ),
5936
+ }, '💬 去房间'),
5384
5937
  h('button', {
5385
5938
  className: 'csb-btn csb-btn-s',
5386
5939
  onClick: () => onGoto?.('bots'),
5387
5940
  }, '🤖 看 Bot'),
5941
+ // 契约 t-38 ②:AI 团队不置灰,正常可进入查看
5942
+ h('button', {
5943
+ className: 'csb-btn csb-btn-s',
5944
+ onClick: () => onGoto?.('team', { sub: 'teams' }),
5945
+ }, '🏠 进入团队'),
5388
5946
  h('div', { className: 'csb-spacer' }),
5947
+
5948
+ // 契约 t-38 ②:AI 团队额外提示
5949
+ teamKind === 'agent'
5950
+ ? h('div', { className: 'csb-set-d', style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-text-sub)' } },
5951
+ '这是智能体自己建的团队,你可以查看和发言')
5952
+ : null,
5389
5953
  h('button', {
5390
5954
  className: 'csb-btn csb-btn-s csb-btn-d',
5391
5955
  disabled: busy, onClick: () => closeTeam(t),
5392
- }, '关闭团队')))
5956
+ }, '关闭团队')),
5957
+ // 契约 第2批 ③:团队卡片显示今日唤醒数
5958
+ t.wakeCountToday !== undefined
5959
+ ? h('div', { className: 'csb-set-d', style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-text-sub)' } },
5960
+ `今日唤醒 ${t.wakeCountToday} 次`)
5961
+ : null,
5962
+ // 契约 第2批 ①:跨团队发请求按钮
5963
+ h('button', {
5964
+ className: 'csb-btn csb-btn-s',
5965
+ disabled: busy,
5966
+ onClick: () => setRouteOpen(t.id),
5967
+ }, '📤 发协作请求'))
5393
5968
  : null)
5394
- })))
5969
+ })),
5970
+
5971
+ // 契约 第2批 ①:跨团队协作请求弹窗。
5972
+ // 后端端点按 /teams/route(B 在加 bot_team_route 工具,端点未定则按此先写)。
5973
+ routeOpen
5974
+ ? h(Modal, {
5975
+ title: '发协作请求给另一个团队',
5976
+ onClose: () => { setRouteOpen(null); setRouteMsg(null); setRouteText(''); setRouteTarget('') },
5977
+ footer: [
5978
+ h('button', {
5979
+ key: 'c', className: 'csb-btn',
5980
+ onClick: () => { setRouteOpen(null); setRouteMsg(null); setRouteText(''); setRouteTarget('') },
5981
+ }, '取消'),
5982
+ h('button', {
5983
+ key: 's', className: 'csb-btn csb-btn-p',
5984
+ disabled: routeBusy || !routeTarget || !routeText.trim(),
5985
+ onClick: sendRoute,
5986
+ }, routeBusy ? '发送中…' : '发送请求'),
5987
+ ],
5988
+ },
5989
+ h('div', null,
5990
+ h('div', { className: 'csb-fld' },
5991
+ h('div', { className: 'csb-fld-l' }, `从「${teams.find((x) => x.id === routeOpen)?.name ?? routeOpen}」发给`),
5992
+ h('select', {
5993
+ className: 'csb-sel', style: { width: '100%' },
5994
+ value: routeTarget,
5995
+ onChange: (e) => { setRouteTarget(e.target.value); setRouteMsg(null) },
5996
+ },
5997
+ h('option', { value: '' }, '选择目标团队…'),
5998
+ teams.filter((x) => x.id !== routeOpen && x.status !== 'closed')
5999
+ .map((x) => h('option', { key: x.id, value: x.id }, x.name)))),
6000
+ h('div', { className: 'csb-fld' },
6001
+ h('div', { className: 'csb-fld-l' }, '请求内容(会唤醒对方负责人一次)'),
6002
+ h('textarea', {
6003
+ className: 'csb-in', rows: 4, style: { width: '100%', resize: 'vertical' },
6004
+ placeholder: '一句话说清要对方做什么…',
6005
+ value: routeText, onChange: (e) => setRouteText(e.target.value),
6006
+ })),
6007
+ routeMsg
6008
+ ? h('div', { className: 'csb-notice csb-notice-ok', style: { marginTop: 8 } },
6009
+ h('span', { className: 'csb-notice-t' }, routeMsg))
6010
+ : null))
6011
+ : null)
5395
6012
  }
5396
6013
 
5397
6014
  // ══════════════════════════════════════════════════════════════════════════
@@ -5464,12 +6081,18 @@ window.__ModuleLoader__.load({
5464
6081
  const open = expanded[t.id] === true
5465
6082
  const lead = botBySession.get(t.leadId)
5466
6083
  const memberBots = (t.members ?? []).map((m) => botBySession.get(m)).filter(Boolean)
6084
+ // 契约 t-38 ①:同 TeamsTab 的 kindBadge 逻辑
6085
+ const teamKind = t.kind ?? (['ui','user','person'].includes(t.createdBy) ? 'human' : 'agent')
6086
+ const kindBadge = teamKind === 'human'
6087
+ ? h('span', { className: 'csb-b csb-b-run', title: 'Boss 建设的团队' }, '👤 Boss建设')
6088
+ : h('span', { className: 'csb-b csb-b-idle', title: '智能体建设的团队' }, '🤖 智能体建设')
5467
6089
  return h('div', { key: t.id, className: 'csb-set-c' },
5468
6090
  h('div', { className: 'csb-row', style: { alignItems: 'center' } },
5469
6091
  h('div', { style: { flex: 1, minWidth: 0 } },
5470
6092
  h('div', { style: { fontWeight: 600 } },
5471
6093
  t.name,
5472
- t.status === 'closed' ? h('span', { className: 'csb-set-d' }, ' (已关闭)') : null),
6094
+ t.status === 'closed' ? h('span', { className: 'csb-set-d' }, ' (已关闭)') : null,
6095
+ h('span', { style: { marginLeft: 8 } }, kindBadge)),
5473
6096
  h('div', { className: 'csb-set-d' },
5474
6097
  `负责人:${lead?.name ?? t.leadId ?? '(无)'}`,
5475
6098
  ` 成员 ${memberBots.length} 个`)),
@@ -5507,7 +6130,7 @@ window.__ModuleLoader__.load({
5507
6130
  }
5508
6131
  if (rows.length === 0) {
5509
6132
  rows.push(h('div', { key: 'empty', className: 'csb-set-d', style: { paddingTop: 6 } },
5510
- '这个团队还没有成员。去「团队」页加人,或到「Bot 档案」新建一个 Bot。'))
6133
+ '这个团队还没有成员。去「团队」页加成员,或到「Bot 档案」新建一个 Bot。'))
5511
6134
  }
5512
6135
 
5513
6136
  return h('div', { style: { paddingTop: 8 } },
@@ -5520,7 +6143,13 @@ window.__ModuleLoader__.load({
5520
6143
  h('button', {
5521
6144
  type: 'button', className: 'csb-btn csb-btn-s',
5522
6145
  onClick: () => onGoto?.('team', { sub: 'teams' }),
5523
- }, '→ 管理团队')))
6146
+ }, '→ 管理团队')),
6147
+ // 契约 t-38 ②:AI 团队额外提示
6148
+ teamKind === 'agent'
6149
+ ? h('div', { className: 'csb-set-d', style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-text-sub)' } },
6150
+ '这是智能体自己建的团队,你可以查看和发言')
6151
+ : null
6152
+ )
5524
6153
  }
5525
6154
 
5526
6155
  return h('div', { className: 'csb-wrap' },
@@ -5684,7 +6313,7 @@ window.__ModuleLoader__.load({
5684
6313
  // ── 记忆 ────────────────────────────────────────────────────────────────
5685
6314
 
5686
6315
  const LAYERS = ['L0', 'L1', 'L2', 'L3', 'L4', 'L5']
5687
- const LAYER_HINT = { L0: '人格(仅人可改)', L1: '用户偏好', L2: '团队规矩', L3: '分支专属', L4: '任务笔记', L5: '归档' }
6316
+ const LAYER_HINT = { L0: '人格(仅 Boss 可改)', L1: 'Boss 偏好', L2: '团队规矩', L3: '分支专属', L4: '任务笔记', L5: '归档' }
5688
6317
 
5689
6318
  function MemoryTab() {
5690
6319
  const [layer, setLayer] = useState('')
@@ -5789,7 +6418,7 @@ window.__ModuleLoader__.load({
5789
6418
  await call('/suggestions/accept', { method: 'POST', body: { id: sg.id } })
5790
6419
  setOk('已采纳,内容已写入记忆')
5791
6420
  } else {
5792
- await call('/suggestions/reject', { method: 'POST', body: { id: sg.id, reason: '人工驳回' } })
6421
+ await call('/suggestions/reject', { method: 'POST', body: { id: sg.id, reason: 'Boss 驳回' } })
5793
6422
  setOk('已驳回')
5794
6423
  }
5795
6424
  reload()
@@ -5813,7 +6442,7 @@ window.__ModuleLoader__.load({
5813
6442
  onClick: () => setShowSug(true),
5814
6443
  },
5815
6444
  h('span', { className: 'csb-notice-t' },
5816
- `有 ${suggestions.length} 条「自动整理建议」等你确认 —— 模型不能直接改你的长期记忆,只能提议。点这里看。`),
6445
+ `有 ${suggestions.length} 条「自动整理建议」等 Boss 确认 —— 模型不能直接改 Boss 的长期记忆,只能提议。点这里看。`),
5817
6446
  h('span', { className: 'csb-b' }, '去确认'))
5818
6447
  : null,
5819
6448
 
@@ -5896,7 +6525,7 @@ window.__ModuleLoader__.load({
5896
6525
  h('div', { className: 'csb-card-m' },
5897
6526
  h('span', { className: 'csb-b' }, `${e.layer} · ${LAYER_HINT[e.layer] ?? ''}`),
5898
6527
  e.botId ? h('span', { className: 'csb-b' }, `Bot ${short(e.botId, 10)}`) : h('span', { className: 'csb-b' }, '全局'),
5899
- e.explicit ? h('span', { className: 'csb-b csb-b-ok' }, '你说过的') : null,
6528
+ e.explicit ? h('span', { className: 'csb-b csb-b-ok' }, 'Boss 明确说的') : null,
5900
6529
  e.conflict ? h('span', { className: 'csb-b csb-b-err' }, '与另一条冲突') : null,
5901
6530
  e.archived
5902
6531
  ? h('span', { className: 'csb-b csb-b-idle' },
@@ -5951,7 +6580,7 @@ window.__ModuleLoader__.load({
5951
6580
  onChange: (e) => setNewTags(e.target.value),
5952
6581
  })),
5953
6582
  h('div', { className: 'csb-field-h' },
5954
- '你手动写的内容会标记为「你说过的」——以后自动整理只能建议、不能改它。'))
6583
+ 'Boss 手动写的内容会标记为「Boss 明确说的」——以后自动整理只能建议、不能改它。'))
5955
6584
  : null,
5956
6585
 
5957
6586
  // ── 编辑 ──
@@ -5975,7 +6604,7 @@ window.__ModuleLoader__.load({
5975
6604
  className: 'csb-in', value: editing.tagsText ?? '',
5976
6605
  onChange: (e) => setEditing({ ...editing, tagsText: e.target.value }),
5977
6606
  })),
5978
- h(Field, { label: '保护级别', hint: '勾上=你说过的话,自动整理不能改;不勾=允许模型提议修订' },
6607
+ h(Field, { label: '保护级别', hint: '勾上=Boss 明确说过的话,自动整理不能改;不勾=允许模型提议修订' },
5979
6608
  h('label', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
5980
6609
  h('input', {
5981
6610
  type: 'checkbox', checked: editing.explicit === true,
@@ -5993,7 +6622,7 @@ window.__ModuleLoader__.load({
5993
6622
  h('div', { className: 'csb-card-m', style: { marginBottom: 10 } },
5994
6623
  h('span', { className: 'csb-b' }, `${detail.entry?.layer} · ${LAYER_HINT[detail.entry?.layer] ?? ''}`),
5995
6624
  h('span', { className: 'csb-b' }, `第 ${detail.entry?.rev} 版`),
5996
- detail.entry?.explicit ? h('span', { className: 'csb-b csb-b-ok' }, '你说过的') : null),
6625
+ detail.entry?.explicit ? h('span', { className: 'csb-b csb-b-ok' }, 'Boss 明确说的') : null),
5997
6626
  h('div', { className: 'csb-pre', style: { whiteSpace: 'pre-wrap' } }, detail.entry?.content ?? ''),
5998
6627
  h('h4', { style: { marginTop: 18 } }, '修改历史'),
5999
6628
  h('div', { className: 'csb-list' }, (detail.history ?? []).slice(-10).reverse().map((ev, i) =>
@@ -6014,7 +6643,7 @@ window.__ModuleLoader__.load({
6014
6643
  footer: [h('button', { key: 'c', className: 'csb-btn', onClick: () => setShowSug(false) }, '关闭')],
6015
6644
  },
6016
6645
  h('p', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } },
6017
- '这些是模型整理的「它认为该记住/该改」的条目。你不点采纳,它就不会生效。'),
6646
+ '这些是模型整理的「它认为该记住/该改」的条目。Boss 不点采纳,它就不会生效。'),
6018
6647
  suggestions.length === 0
6019
6648
  ? h(Empty, { icon: '✓', title: '没有待确认的建议', hint: '有建议时这里会出现。' })
6020
6649
  : h('div', { className: 'csb-list' }, suggestions.map((sg) => h('div', { key: sg.id, className: 'csb-card' },
@@ -6061,7 +6690,7 @@ window.__ModuleLoader__.load({
6061
6690
  title: '彻底删除,无法恢复',
6062
6691
  message: `内容:${confirmPurge.content.slice(0, 120)}\n\n`
6063
6692
  + '这一步会把这条件目从记忆库里抹掉,模型以后永远看不到它,也不能恢复。\n'
6064
- + '如果里面有你以后还想查的内容,请先用「恢复」拿回来备份。',
6693
+ + '如果里面有 Boss 以后还想查的内容,请先用「恢复」拿回来备份。',
6065
6694
  danger: true, confirmText: '我确认,彻底删除',
6066
6695
  onCancel: () => setConfirmPurge(null),
6067
6696
  onConfirm: async () => {
@@ -6272,7 +6901,7 @@ window.__ModuleLoader__.load({
6272
6901
  h(Section, {
6273
6902
  title: '记忆自审',
6274
6903
  desc: '每聊若干回合,让模型停下来检查一次「有没有该记住的、有没有记错的」。'
6275
- + '它只会**提议**,你在「记忆」页点采纳才真正写进去。',
6904
+ + '它只会**提议**,Boss 在「记忆」页点采纳才真正写进去。',
6276
6905
  }),
6277
6906
  h(ReviewSettings, {}),
6278
6907
 
@@ -6839,7 +7468,7 @@ window.__ModuleLoader__.load({
6839
7468
  ? h(Empty, {
6840
7469
  icon: '⌘',
6841
7470
  title: '还没有提示词',
6842
- hint: '把「你每次都要说一遍的要求」存成提示词,以后一键注入,不用重复打字。',
7471
+ hint: '把「Boss 每次都要说一遍的要求」存成提示词,以后一键注入,不用重复打字。',
6843
7472
  action: { label: '新建提示词', icon: '✎', onClick: () => setEditing({ ...blank }) },
6844
7473
  })
6845
7474
  : null,
@@ -7218,7 +7847,7 @@ window.__ModuleLoader__.load({
7218
7847
  {
7219
7848
  id: 'workspace', label: '工作台',
7220
7849
  subs: [
7221
- { id: 'board', label: `${HUMAN_LABEL}的任务`, Comp: UserTasksTab },
7850
+ { id: 'board', label: '任务栏', Comp: UserTasksTab },
7222
7851
  { id: 'ai-tasks', label: '智能体任务', Comp: AiTasksTab },
7223
7852
  { id: 'cycles', label: '复盘周期', Comp: CyclesTab },
7224
7853
  { id: 'todo', label: '我的待办', Comp: TodoTab },
@@ -7563,6 +8192,12 @@ window.__ModuleLoader__.load({
7563
8192
  exports.ConfirmModal = ConfirmModal
7564
8193
  // 改造04(CONTRACT-bots04 §3):派活 / 补充说明 / 目标锚 / 房间建任务
7565
8194
  exports.TaskAnchorPanel = TaskAnchorPanel
8195
+ // 契约 §2.5 / §4.3:阶段产出物面板 + Boss 直接动手面板
8196
+ exports.TaskPhasePanel = TaskPhasePanel
8197
+ exports.TaskBossActions = TaskBossActions
8198
+ exports.toDateInput = toDateInput
8199
+ exports.dateInputToMs = dateInputToMs
8200
+ exports.humanizeBy = humanizeBy
7566
8201
  exports.BotAssignModal = BotAssignModal
7567
8202
  exports.BotMessageModal = BotMessageModal
7568
8203
  exports.RoomTaskCreateModal = RoomTaskCreateModal
@@ -7593,7 +8228,7 @@ window.__ModuleLoader__.load({
7593
8228
  exports.TaskConversationPanel = TaskConversationPanel
7594
8229
  // 需求4:PDCA 周期页(human / agent 两类周期)
7595
8230
  exports.CyclesTab = CyclesTab
7596
- // 需求3:团队页(只显示用户建的大团队)
8231
+ // 需求3:团队页(只显示 Boss 建的大团队)
7597
8232
  exports.TeamsTab = TeamsTab
7598
8233
  exports.PANEL_ID = PANEL_ID
7599
8234
  exports.API = API