cs-bots 0.8.0 → 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 +186 -0
- package/docs/01-/346/236/266/346/236/204/346/226/207/346/241/243.md +5 -5
- 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/package.json +2 -2
- package/src/api.js +100 -1
- package/src/board.js +61 -0
- package/src/client.js +194 -14
- 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/teams.js +353 -4
- package/src/tools.js +48 -2
package/src/client.js
CHANGED
|
@@ -537,6 +537,47 @@ 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
|
/**
|
|
@@ -1073,7 +1114,7 @@ window.__ModuleLoader__.load({
|
|
|
1073
1114
|
}, '📦 看产出物')),
|
|
1074
1115
|
h('div', { className: 'csb-kv' },
|
|
1075
1116
|
// ── 5W:谁建的 / 是什么 / 何时 / 在哪做 / 为什么 ──
|
|
1076
|
-
// 需求5
|
|
1117
|
+
// 需求5:表述以「Boss」为主体——Boss 建的任务就说「Boss」,不暴露 Bot 会话 id
|
|
1077
1118
|
h('span', { className: 'csb-k' }, '谁建的'),
|
|
1078
1119
|
h('span', { className: 'csb-v' }, task.source === 'human' ? HUMAN_LABEL : (task.createdBy ? botName(bots, task.createdBy) : '智能体')),
|
|
1079
1120
|
h('span', { className: 'csb-k' }, '来源'),
|
|
@@ -1936,7 +1977,7 @@ window.__ModuleLoader__.load({
|
|
|
1936
1977
|
// 规则(契约原文,逐条实现,不自作主张):
|
|
1937
1978
|
// room + exists !== false → 按钮「回到消息出处(房间名 #N)」
|
|
1938
1979
|
// room + exists === false → 按钮禁用 + 提示「来源房间已删除」
|
|
1939
|
-
// none →
|
|
1980
|
+
// none → **不渲染任何来源区块**(Boss 建的任务不占位)
|
|
1940
1981
|
// todo/evolve → 只读一行「来源:<sourceSeq>」
|
|
1941
1982
|
// unparsed → 只读一行 + 后端 reason(绝不渲染跳不动的按钮)
|
|
1942
1983
|
sourceQ.error
|
|
@@ -3096,7 +3137,7 @@ window.__ModuleLoader__.load({
|
|
|
3096
3137
|
/**
|
|
3097
3138
|
* 任务看板。
|
|
3098
3139
|
*
|
|
3099
|
-
* 需求1(2026-11-08
|
|
3140
|
+
* 需求1(2026-11-08):任务页**默认只展示 Boss 建任务**;智能体自建/分发的任务
|
|
3100
3141
|
* 走独立子页签(`sourceFilter='ai'`),按状态分类展示。
|
|
3101
3142
|
*
|
|
3102
3143
|
* 为什么这样分:人关心的是「我要盯的活」,智能体之间互相派的活是执行细节——
|
|
@@ -4951,6 +4992,11 @@ window.__ModuleLoader__.load({
|
|
|
4951
4992
|
onKeyDown: (e) => e.key === 'Enter' && post(),
|
|
4952
4993
|
}),
|
|
4953
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,
|
|
4954
5000
|
// ── 右栏:管理 ──
|
|
4955
5001
|
h('div', { className: 'csb-room-side' },
|
|
4956
5002
|
h(Section, { title: '房间信息', desc: '基本信息和设置' }),
|
|
@@ -5048,6 +5094,8 @@ window.__ModuleLoader__.load({
|
|
|
5048
5094
|
allowBotAutoPost: room.settings?.allowBotAutoPost !== false,
|
|
5049
5095
|
maxBotsPerRound: room.settings?.maxBotsPerRound ?? 3,
|
|
5050
5096
|
relayEnabled: room.settings?.relayEnabled !== false,
|
|
5097
|
+
// 契约 t-38 ③:缺省 true —— 不 @ 就只落消息、不唤醒任何人
|
|
5098
|
+
mentionOnly: room.settings?.mentionOnly !== false,
|
|
5051
5099
|
})
|
|
5052
5100
|
const save = async () => {
|
|
5053
5101
|
try {
|
|
@@ -5065,6 +5113,7 @@ window.__ModuleLoader__.load({
|
|
|
5065
5113
|
allowBotAutoPost: settings.allowBotAutoPost,
|
|
5066
5114
|
maxBotsPerRound: Number(settings.maxBotsPerRound) || 3,
|
|
5067
5115
|
relayEnabled: settings.relayEnabled,
|
|
5116
|
+
mentionOnly: settings.mentionOnly,
|
|
5068
5117
|
},
|
|
5069
5118
|
byId: 'user',
|
|
5070
5119
|
},
|
|
@@ -5130,7 +5179,14 @@ window.__ModuleLoader__.load({
|
|
|
5130
5179
|
h('input', {
|
|
5131
5180
|
type: 'checkbox', checked: settings.relayEnabled,
|
|
5132
5181
|
onChange: (e) => setSettings({ ...settings, relayEnabled: e.target.checked }),
|
|
5133
|
-
}), 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 ? '开启' : '关闭')))
|
|
5134
5190
|
}
|
|
5135
5191
|
|
|
5136
5192
|
// ── 成果(artifacts 聚合)────────────────────────────────────────────────
|
|
@@ -5467,7 +5523,7 @@ window.__ModuleLoader__.load({
|
|
|
5467
5523
|
}
|
|
5468
5524
|
/** 下拉里的合法取值(顺序即页面顺序:安全 → 宽松 → 逃生阀)。 */
|
|
5469
5525
|
const ADMIN_POLICY_KEYS = ['lead-only', 'any-member', 'open']
|
|
5470
|
-
/**
|
|
5526
|
+
/** 团队默认策略:Boss 建的团队一律默认「仅负责人可管理」。 */
|
|
5471
5527
|
const DEFAULT_ADMIN_POLICY = 'lead-only'
|
|
5472
5528
|
/**
|
|
5473
5529
|
* 团队页(需求3,2026-11-08 重做)。
|
|
@@ -5480,7 +5536,7 @@ window.__ModuleLoader__.load({
|
|
|
5480
5536
|
*
|
|
5481
5537
|
* ## 由此确定的三条设计
|
|
5482
5538
|
*
|
|
5483
|
-
* 1.
|
|
5539
|
+
* 1. **上层只显示「Boss 建的团队」**。智能体自建的小团队不再占用上层版面——
|
|
5484
5540
|
* 它们是执行细节,要看请走「穿透」。所以本页 `list({ type:'big' })` 过滤,
|
|
5485
5541
|
* 且 UI 上**完全不出现「大/小」字样**(用户明确说不需要这个概念)。
|
|
5486
5542
|
*
|
|
@@ -5506,7 +5562,7 @@ window.__ModuleLoader__.load({
|
|
|
5506
5562
|
* 房间页本身已支持列出全部房间 + 消息内容查看,超级管理员不受限制。
|
|
5507
5563
|
*/
|
|
5508
5564
|
function TeamsTab({ onGoto }) {
|
|
5509
|
-
//
|
|
5565
|
+
// 只取 Boss 建的团队(big);智能体自建的 small 不上上层
|
|
5510
5566
|
const listQ = useApi('/teams?type=big', [])
|
|
5511
5567
|
const rosterQ = useApi('/roster')
|
|
5512
5568
|
// 契约 §3(UI):团队卡片要显示 `房间:<名>` —— 房间名不在团队记录里,
|
|
@@ -5536,6 +5592,13 @@ window.__ModuleLoader__.load({
|
|
|
5536
5592
|
}
|
|
5537
5593
|
const act = (p, body) => call(p, { method: 'POST', body })
|
|
5538
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
|
+
|
|
5539
5602
|
const teams = listQ.data?.items ?? []
|
|
5540
5603
|
// 后端下发的合法策略(/teams 返回 policies);万一旧后端没这个字段,就用本地常量兜底
|
|
5541
5604
|
const policyKeys = listQ.data?.policies?.length ? listQ.data.policies : ADMIN_POLICY_KEYS
|
|
@@ -5567,7 +5630,7 @@ window.__ModuleLoader__.load({
|
|
|
5567
5630
|
leadId,
|
|
5568
5631
|
purpose: purpose.trim() || undefined,
|
|
5569
5632
|
memberIds: selected,
|
|
5570
|
-
//
|
|
5633
|
+
// Boss 建的团队默认「仅负责人可管理」(服务端缺省是 open,为了不破坏既有调用面;
|
|
5571
5634
|
// 页面是新入口,没有历史包袱,所以在这里给安全默认值)
|
|
5572
5635
|
adminPolicy: DEFAULT_ADMIN_POLICY,
|
|
5573
5636
|
by: 'ui',
|
|
@@ -5613,6 +5676,40 @@ window.__ModuleLoader__.load({
|
|
|
5613
5676
|
} catch (e) { setMsg(String(e.message ?? e)) } finally { setBusy(false) }
|
|
5614
5677
|
}
|
|
5615
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
|
+
|
|
5616
5713
|
/**
|
|
5617
5714
|
* 切换「谁能管理这个团队」(需求:管理员权限边界从页面控制)。
|
|
5618
5715
|
*
|
|
@@ -5718,12 +5815,19 @@ window.__ModuleLoader__.load({
|
|
|
5718
5815
|
const leadName = botBySession.get(t.leadId)?.name ?? (t.leadId ? short(t.leadId, 12) : '(未指定)')
|
|
5719
5816
|
const pending = pendingMembers[t.id] ?? []
|
|
5720
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: '智能体建设的团队' }, '🤖 智能体建设')
|
|
5721
5824
|
return h('div', { key: t.id, className: 'csb-card', style: { flexDirection: 'column', alignItems: 'stretch', marginBottom: 8 } },
|
|
5722
5825
|
h('div', { className: 'csb-card-main' },
|
|
5723
5826
|
h('div', { className: 'csb-card-t' },
|
|
5724
5827
|
t.name,
|
|
5725
5828
|
t.status === 'closed' ? h('span', { className: 'csb-b csb-b-idle', style: { marginLeft: 8 } }, '已关闭') : null,
|
|
5726
|
-
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)),
|
|
5727
5831
|
|
|
5728
5832
|
// 管理员权限边界(需求:管理员权限边界从页面控制)
|
|
5729
5833
|
// 徽章:人要能一眼看出「谁能动这个团队」,所以常驻显示当前策略的中文名。
|
|
@@ -5834,13 +5938,77 @@ window.__ModuleLoader__.load({
|
|
|
5834
5938
|
className: 'csb-btn csb-btn-s',
|
|
5835
5939
|
onClick: () => onGoto?.('bots'),
|
|
5836
5940
|
}, '🤖 看 Bot'),
|
|
5941
|
+
// 契约 t-38 ②:AI 团队不置灰,正常可进入查看
|
|
5942
|
+
h('button', {
|
|
5943
|
+
className: 'csb-btn csb-btn-s',
|
|
5944
|
+
onClick: () => onGoto?.('team', { sub: 'teams' }),
|
|
5945
|
+
}, '🏠 进入团队'),
|
|
5837
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,
|
|
5838
5953
|
h('button', {
|
|
5839
5954
|
className: 'csb-btn csb-btn-s csb-btn-d',
|
|
5840
5955
|
disabled: busy, onClick: () => closeTeam(t),
|
|
5841
|
-
}, '关闭团队'))
|
|
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
|
+
}, '📤 发协作请求'))
|
|
5842
5968
|
: null)
|
|
5843
|
-
}))
|
|
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)
|
|
5844
6012
|
}
|
|
5845
6013
|
|
|
5846
6014
|
// ══════════════════════════════════════════════════════════════════════════
|
|
@@ -5913,12 +6081,18 @@ window.__ModuleLoader__.load({
|
|
|
5913
6081
|
const open = expanded[t.id] === true
|
|
5914
6082
|
const lead = botBySession.get(t.leadId)
|
|
5915
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: '智能体建设的团队' }, '🤖 智能体建设')
|
|
5916
6089
|
return h('div', { key: t.id, className: 'csb-set-c' },
|
|
5917
6090
|
h('div', { className: 'csb-row', style: { alignItems: 'center' } },
|
|
5918
6091
|
h('div', { style: { flex: 1, minWidth: 0 } },
|
|
5919
6092
|
h('div', { style: { fontWeight: 600 } },
|
|
5920
6093
|
t.name,
|
|
5921
|
-
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)),
|
|
5922
6096
|
h('div', { className: 'csb-set-d' },
|
|
5923
6097
|
`负责人:${lead?.name ?? t.leadId ?? '(无)'}`,
|
|
5924
6098
|
` 成员 ${memberBots.length} 个`)),
|
|
@@ -5969,7 +6143,13 @@ window.__ModuleLoader__.load({
|
|
|
5969
6143
|
h('button', {
|
|
5970
6144
|
type: 'button', className: 'csb-btn csb-btn-s',
|
|
5971
6145
|
onClick: () => onGoto?.('team', { sub: 'teams' }),
|
|
5972
|
-
}, '→ 管理团队'))
|
|
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
|
+
)
|
|
5973
6153
|
}
|
|
5974
6154
|
|
|
5975
6155
|
return h('div', { className: 'csb-wrap' },
|
|
@@ -8048,7 +8228,7 @@ window.__ModuleLoader__.load({
|
|
|
8048
8228
|
exports.TaskConversationPanel = TaskConversationPanel
|
|
8049
8229
|
// 需求4:PDCA 周期页(human / agent 两类周期)
|
|
8050
8230
|
exports.CyclesTab = CyclesTab
|
|
8051
|
-
// 需求3
|
|
8231
|
+
// 需求3:团队页(只显示 Boss 建的大团队)
|
|
8052
8232
|
exports.TeamsTab = TeamsTab
|
|
8053
8233
|
exports.PANEL_ID = PANEL_ID
|
|
8054
8234
|
exports.API = API
|
package/src/dispatcher.js
CHANGED
|
@@ -336,6 +336,8 @@ export function createDispatcher(ctx, config = {}) {
|
|
|
336
336
|
result,
|
|
337
337
|
// ── 平台扩展字段(§3.5 未列,但对「可读原因」与可审计是必需的)──
|
|
338
338
|
text: draft.text,
|
|
339
|
+
// t-39 第2批 D:唤醒成本可见。无团队归属时是 null(不猜)。
|
|
340
|
+
teamId: draft.teamId ?? null,
|
|
339
341
|
updatedAt: at,
|
|
340
342
|
seq: takeSeq(),
|
|
341
343
|
}
|
|
@@ -345,14 +347,22 @@ export function createDispatcher(ctx, config = {}) {
|
|
|
345
347
|
return { wake, durable }
|
|
346
348
|
}
|
|
347
349
|
|
|
348
|
-
/**
|
|
349
|
-
|
|
350
|
+
/**
|
|
351
|
+
* 构造 wake 草稿(此刻尚无终态;`result` 只在 `commit` 时赋值)。
|
|
352
|
+
*
|
|
353
|
+
* `teamId`:唤醒成本可见(t-39 第2批 D)。**不是所有唤醒都属于某个团队**
|
|
354
|
+
* (例如任务派活、房间广播都可能与团队无关)—— 那种情况一律写 `null`,
|
|
355
|
+
* 绝不按 target 猜一个团队塞进去:猜出来的数字比没有更糟,它会让
|
|
356
|
+
* 「每团队唤醒成本」的统计凭空多出一份不属于它的开销。
|
|
357
|
+
*/
|
|
358
|
+
function draftWake({ target, reason, dedupKey, text, at, teamId }) {
|
|
350
359
|
return {
|
|
351
360
|
id: `wake-${at}-${process.pid.toString(36)}-${takeSeq().toString(36)}`,
|
|
352
361
|
target,
|
|
353
362
|
reason,
|
|
354
363
|
dedupKey,
|
|
355
364
|
text,
|
|
365
|
+
teamId: teamId ?? null,
|
|
356
366
|
scheduledAt: at,
|
|
357
367
|
}
|
|
358
368
|
}
|
|
@@ -449,7 +459,7 @@ export function createDispatcher(ctx, config = {}) {
|
|
|
449
459
|
*/
|
|
450
460
|
function notify(request = {}) {
|
|
451
461
|
const at = now()
|
|
452
|
-
const { target, reason, dedupKey, text, delayMs } = request
|
|
462
|
+
const { target, reason, dedupKey, text, delayMs, teamId } = request
|
|
453
463
|
const wakeText = text ?? `[cs-bots 唤醒] ${REASON_TEXT[reason] ?? String(reason)}`
|
|
454
464
|
|
|
455
465
|
// 每调用一次 = 一次决策,先计数(恒等式 total === sent + skipped + failed + pending 由此成立)。
|
|
@@ -462,7 +472,7 @@ export function createDispatcher(ctx, config = {}) {
|
|
|
462
472
|
}
|
|
463
473
|
if (!WAKE_REASONS.includes(reason)) {
|
|
464
474
|
counters.failed += 1
|
|
465
|
-
const draft = draftWake({ target, reason: String(reason), dedupKey, text: wakeText, at })
|
|
475
|
+
const draft = draftWake({ target, reason: String(reason), dedupKey, text: wakeText, at, teamId })
|
|
466
476
|
const { wake, durable } = commit(draft, 'failed', { at, detail: `invalid-reason:${reason}` })
|
|
467
477
|
return { ok: false, result: 'failed', wakeId: wake.id, reason: 'invalid-reason', durable }
|
|
468
478
|
}
|
|
@@ -474,7 +484,7 @@ export function createDispatcher(ctx, config = {}) {
|
|
|
474
484
|
let wakeId
|
|
475
485
|
let durable = probe.writable
|
|
476
486
|
if (last === undefined || at - last >= SKIP_RECORD_INTERVAL_MS) {
|
|
477
|
-
const draft = draftWake({ target, reason, dedupKey, text: wakeText, at })
|
|
487
|
+
const draft = draftWake({ target, reason, dedupKey, text: wakeText, at, teamId })
|
|
478
488
|
const committed = commit(draft, 'skipped', { at })
|
|
479
489
|
wakeId = committed.wake.id
|
|
480
490
|
durable = committed.durable
|
|
@@ -490,7 +500,7 @@ export function createDispatcher(ctx, config = {}) {
|
|
|
490
500
|
if (queue.sizeOf(target) >= maxPerTarget) {
|
|
491
501
|
counters.backpressured += 1
|
|
492
502
|
counters.failed += 1
|
|
493
|
-
const draft = draftWake({ target, reason, dedupKey, text: wakeText, at })
|
|
503
|
+
const draft = draftWake({ target, reason, dedupKey, text: wakeText, at, teamId })
|
|
494
504
|
// 请求被**拒绝**(而非「受理后没送成」)⇒ 该 dedupKey 尚未被消费,释放它,
|
|
495
505
|
// 队列疏通后上游仍可用同一个 dedupKey 重发。
|
|
496
506
|
if (dedupKey) dedup.keys.delete(dedupKey)
|
|
@@ -516,7 +526,7 @@ export function createDispatcher(ctx, config = {}) {
|
|
|
516
526
|
deferred = notBefore > at
|
|
517
527
|
}
|
|
518
528
|
|
|
519
|
-
const draft = draftWake({ target, reason, dedupKey, text: wakeText, at })
|
|
529
|
+
const draft = draftWake({ target, reason, dedupKey, text: wakeText, at, teamId })
|
|
520
530
|
const entry = { draft, notBefore, keyHeld: Boolean(dedupKey) }
|
|
521
531
|
queue.add(entry)
|
|
522
532
|
if (deferred) counters.deferred += 1
|
|
@@ -614,25 +624,74 @@ export function createDispatcher(ctx, config = {}) {
|
|
|
614
624
|
/**
|
|
615
625
|
* 从 `wakes.jsonl` 读唤醒记录(**读盘,不是读内存** —— 进程重启后仍可审计)。
|
|
616
626
|
*
|
|
617
|
-
* @param {{ target?: string, limit?: number }} [options]
|
|
627
|
+
* @param {{ target?: string, teamId?: string, limit?: number }} [options]
|
|
628
|
+
* `teamId` 是 t-39 第2批 D 加的:唤醒成本可见(按团队过滤)。
|
|
618
629
|
* @returns {object[]} 终态 Wake 记录(落盘顺序;有 `limit` 时取**最后** limit 条)
|
|
619
630
|
*/
|
|
620
631
|
function list(options = {}) {
|
|
621
|
-
const { target, limit } = options
|
|
622
|
-
// 先按 limit 取尾,再按 target 过滤 —— 语义是「最近 limit 条里属于该 target 的」。
|
|
632
|
+
const { target, teamId, limit } = options
|
|
633
|
+
// 先按 limit 取尾,再按 target / teamId 过滤 —— 语义是「最近 limit 条里属于该 target/team 的」。
|
|
623
634
|
const { rows, bad, total } = readJsonl(wakesFile, limit ? { limit } : {})
|
|
624
|
-
|
|
635
|
+
let filtered = rows
|
|
636
|
+
if (target) filtered = filtered.filter((row) => row?.target === target)
|
|
637
|
+
if (teamId) filtered = filtered.filter((row) => row?.teamId === teamId)
|
|
625
638
|
// 坏行**不静默**:作为非枚举属性挂上,调用方可用 `Object.keys` 忽略、也可显式读取。
|
|
626
639
|
Object.defineProperty(filtered, 'bad', { value: bad, enumerable: false })
|
|
627
640
|
Object.defineProperty(filtered, 'total', { value: total, enumerable: false })
|
|
628
641
|
return filtered
|
|
629
642
|
}
|
|
630
643
|
|
|
644
|
+
/**
|
|
645
|
+
* 按团队聚合唤醒成本(t-39 第2批 D)。
|
|
646
|
+
*
|
|
647
|
+
* 为什么需要:Boss 要看「哪个团队在消耗唤醒配额」。单看 target 看不出团队归属
|
|
648
|
+
* (target 是会话 id,团队归属在 `teamId` 上)。
|
|
649
|
+
*
|
|
650
|
+
* 只聚合**真正有 teamId 的记录**;`teamId === null` 的(任务派活、房间广播等
|
|
651
|
+
* 与团队无关的唤醒)单独归入 `teamId: null` 桶,不混进任何团队数字里。
|
|
652
|
+
*
|
|
653
|
+
* @param {{ since?: number }} [options] `since` = epoch ms,只统计该时刻之后的记录
|
|
654
|
+
* @returns {{ since?: number, teams: object[], unassigned: object, total: number }}
|
|
655
|
+
*/
|
|
656
|
+
function byTeam(options = {}) {
|
|
657
|
+
const { since } = options
|
|
658
|
+
const { rows } = readJsonl(wakesFile, { limit: 100_000 })
|
|
659
|
+
const buckets = new Map()
|
|
660
|
+
let total = 0
|
|
661
|
+
for (const row of rows) {
|
|
662
|
+
const at = row?.scheduledAt ?? row?.at
|
|
663
|
+
if (since !== undefined && Number.isFinite(since) && Number.isFinite(at) && at < since) continue
|
|
664
|
+
total += 1
|
|
665
|
+
const key = row?.teamId ?? null
|
|
666
|
+
if (!buckets.has(key)) {
|
|
667
|
+
buckets.set(key, { teamId: key, count: 0, sent: 0, skipped: 0, failed: 0, pending: 0, targets: new Set() })
|
|
668
|
+
}
|
|
669
|
+
const b = buckets.get(key)
|
|
670
|
+
b.count += 1
|
|
671
|
+
const r = row?.result
|
|
672
|
+
if (r === 'sent') b.sent += 1
|
|
673
|
+
else if (r === 'skipped') b.skipped += 1
|
|
674
|
+
else if (r === 'failed') b.failed += 1
|
|
675
|
+
else if (r === 'pending') b.pending += 1
|
|
676
|
+
if (row?.target) b.targets.add(row.target)
|
|
677
|
+
}
|
|
678
|
+
const teams = [...buckets.entries()]
|
|
679
|
+
.filter(([key]) => key !== null)
|
|
680
|
+
.map(([, b]) => ({ ...b, targets: b.targets.size, targetsList: [...b.targets] }))
|
|
681
|
+
.sort((a, b) => b.count - a.count)
|
|
682
|
+
const unassignedEntry = buckets.get(null)
|
|
683
|
+
const unassigned = unassignedEntry
|
|
684
|
+
? { teamId: null, count: unassignedEntry.count, sent: unassignedEntry.sent, skipped: unassignedEntry.skipped, failed: unassignedEntry.failed, pending: unassignedEntry.pending, targets: unassignedEntry.targets.size, targetsList: [...unassignedEntry.targets] }
|
|
685
|
+
: { teamId: null, count: 0, sent: 0, skipped: 0, failed: 0, pending: 0, targets: 0, targetsList: [] }
|
|
686
|
+
return { since: since ?? null, teams, unassigned, total }
|
|
687
|
+
}
|
|
688
|
+
|
|
631
689
|
return {
|
|
632
690
|
notify,
|
|
633
691
|
flush,
|
|
634
692
|
stats,
|
|
635
693
|
list,
|
|
694
|
+
byTeam,
|
|
636
695
|
/** 只读诊断:状态目录 / 文件路径 / 是否可写。 */
|
|
637
696
|
paths: { stateDir: stateDirectory, wakesFile },
|
|
638
697
|
/** 状态目录可否持久化;`false` ⇒ 所有返回值都会带 `durable:false`。 */
|
package/src/index.js
CHANGED
|
@@ -127,7 +127,14 @@ function applyInner(ctx, config, { log, warn }) {
|
|
|
127
127
|
// ⚠️ 2026-11-05(P0):createBoard 必须拿到 settings —— wipLimitPerBot(WIP 上限)
|
|
128
128
|
// 与 requireTriageForReady(Ready 闸门)都从 config.settings 读。只传 stateDir 会让
|
|
129
129
|
// 平台设置对任务板静默失效(永远退回代码内默认值),且用户改了设置看不到任何变化。
|
|
130
|
-
services.board = createBoard(ctx, {
|
|
130
|
+
services.board = createBoard(ctx, {
|
|
131
|
+
stateDir: config.stateDir,
|
|
132
|
+
leaseMs: settings.defaultLeaseMs,
|
|
133
|
+
settings,
|
|
134
|
+
// 指标旁路(bots04 欠账2):传**取值函数**而不是实例 ——
|
|
135
|
+
// metrics 在 L276 才装配,此刻还是 undefined。传函数可规避装配顺序耦合。
|
|
136
|
+
metrics: () => services.metrics,
|
|
137
|
+
})
|
|
131
138
|
log(`[cs-bots] BoardService 就绪(stateDir=${services.board.paths?.stateDir ?? '?'})`)
|
|
132
139
|
// ⚠️ 修复(2026-11-04 测试,P1-4):expireLeases 之前无任何调用者,
|
|
133
140
|
// owner 崩溃/失联后任务永久停在 in_progress(连"重启时回收"都不做)。
|
package/src/kernel.js
CHANGED
|
@@ -297,6 +297,21 @@ export function hostLlmAvailable() {
|
|
|
297
297
|
* sole ordinary message of its own turn."
|
|
298
298
|
* ⇒ 适合「有新活要干」的唤醒。
|
|
299
299
|
*
|
|
300
|
+
* ⚠️ **`followup` 在 `agent` 对象上,不在 `agents` 服务上** —— 这里极易搞错。
|
|
301
|
+
*
|
|
302
|
+
* 写测试替身时,很多人会自然地把 `followup` 挂到假 `agents` 服务上
|
|
303
|
+
* (像 `create` / `get` / `list` 那样),结果永远拿到 `followup-unsupported`,
|
|
304
|
+
* 而且报错信息不指向真正的原因。2026-11-11 实测踩过一次,
|
|
305
|
+
* 排查时还一度以为是 `assertDeliverable` 拦的。
|
|
306
|
+
*
|
|
307
|
+
* 正确形状:
|
|
308
|
+
* ```js
|
|
309
|
+
* get: (key) => key === 'agents' ? {
|
|
310
|
+
* get: (id) => ({ id, sessionId: id, followup(msg) { ... } }), // ← 挂在这里
|
|
311
|
+
* // 不是这里:followup(msg) {}
|
|
312
|
+
* } : undefined
|
|
313
|
+
* ```
|
|
314
|
+
*
|
|
300
315
|
* @param {object} ctx
|
|
301
316
|
* @param {string} sessionId
|
|
302
317
|
* @param {string} text
|