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/CHANGELOG.md +294 -0
- package/README.md +47 -174
- package/docs/01-/346/236/266/346/236/204/346/226/207/346/241/243.md +14 -13
- package/docs/02-/351/234/200/346/261/202/350/247/204/346/240/274/344/271/246.md +3 -3
- package/docs/03-/350/257/246/347/273/206/350/256/276/350/256/241.md +3 -3
- package/docs/04-/346/223/215/344/275/234/346/211/213/345/206/214.md +2 -2
- package/docs/CONTRACT-/345/215/207/347/272/24709-Boss/344/275/223/351/252/214.md +280 -0
- package/docs//345/215/207/347/272/24708-/346/216/245/347/273/255/344/270/216/345/233/236/346/273/232.md +298 -0
- package/package.json +2 -2
- package/src/api.js +1105 -8
- package/src/board.js +104 -11
- package/src/client.js +731 -96
- package/src/dispatcher.js +70 -11
- package/src/index.js +8 -1
- package/src/kernel.js +15 -0
- package/src/room.js +160 -9
- package/src/roster.js +54 -0
- package/src/settings.js +2 -2
- package/src/teams.js +386 -4
- package/src/tools.js +124 -3
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:
|
|
497
|
-
text:
|
|
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: '
|
|
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
|
|
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 } }, '
|
|
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
|
-
: '
|
|
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
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
:
|
|
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
|
-
|
|
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`
|
|
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`
|
|
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
|
-
? '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
2718
|
-
//
|
|
2719
|
-
q.set('source',
|
|
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,
|
|
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('');
|
|
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
|
-
//
|
|
2844
|
-
title: isAi ? '智能体任务' :
|
|
3314
|
+
// 契约 §1:顶层保留两个子页签(任务栏 / 智能体任务),页内不再有「看全部 / 只看需要管」切换。
|
|
3315
|
+
title: isAi ? '智能体任务' : '任务栏',
|
|
2845
3316
|
desc: isAi
|
|
2846
|
-
?
|
|
2847
|
-
:
|
|
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' }, '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
'
|
|
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: '
|
|
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' }, '
|
|
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' }, '
|
|
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:
|
|
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
|