dsh-device-fleet 0.1.0-dev.20 → 0.1.0-dev.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client.js +106 -5
- package/docs/notes/dev-release.md +21 -12
- package/package.json +1 -1
- package/src/host/room-service.js +6 -1
- package/src/plugin/index.js +3 -32
package/client.js
CHANGED
|
@@ -608,7 +608,9 @@ window.__ModuleLoader__.load({
|
|
|
608
608
|
function renderRoomDetail(detail, ctl) {
|
|
609
609
|
if (!detail) return h('div', { style: { opacity: 0.65, fontSize: 12 } }, '加载房间…');
|
|
610
610
|
const memberIds = detail.members.map((member) => member.memberId);
|
|
611
|
-
const
|
|
611
|
+
const participantIds = memberIds.filter((id) => !id.startsWith('host:'));
|
|
612
|
+
const defaultRecipients = participantIds.length > 0 ? participantIds : memberIds;
|
|
613
|
+
const recipientIds = Array.isArray(ctl.sendTo) && ctl.sendTo.length > 0 ? ctl.sendTo : defaultRecipients;
|
|
612
614
|
return h(
|
|
613
615
|
'div',
|
|
614
616
|
null,
|
|
@@ -636,7 +638,7 @@ window.__ModuleLoader__.load({
|
|
|
636
638
|
checked: recipientIds.includes(member.memberId),
|
|
637
639
|
onChange: (event) => {
|
|
638
640
|
ctl.setSendTo((current) => {
|
|
639
|
-
const base = current.length > 0 ? current :
|
|
641
|
+
const base = current.length > 0 ? current : defaultRecipients
|
|
640
642
|
return event.target.checked ? [...new Set([...base, member.memberId])] : base.filter((id) => id !== member.memberId)
|
|
641
643
|
})
|
|
642
644
|
},
|
|
@@ -1138,7 +1140,103 @@ window.__ModuleLoader__.load({
|
|
|
1138
1140
|
h('span', null, room.members.length + ' 名成员'),
|
|
1139
1141
|
names.length > 0 ? h('span', null, '· ' + names.join(', ')) : h('span', null, '· 仅 host'),
|
|
1140
1142
|
h('span', { style: { flex: 1 } }),
|
|
1141
|
-
h('span', { style: { opacity: 0.55 } }, '
|
|
1143
|
+
h('span', { style: { opacity: 0.55 } }, '房间消息在「房间」页交流'),
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Session-native Room view: the Room's transcript, members and board live
|
|
1148
|
+
// in their own Conversation View instead of the normal Chat stream.
|
|
1149
|
+
function RoomPane({ roomId }) {
|
|
1150
|
+
const [detail, setDetail] = useState(null);
|
|
1151
|
+
const [missing, setMissing] = useState(false);
|
|
1152
|
+
const [error, setError] = useState('');
|
|
1153
|
+
const [sendText, setSendText] = useState('');
|
|
1154
|
+
const [sendTo, setSendTo] = useState([]);
|
|
1155
|
+
const [taskTitle, setTaskTitle] = useState('');
|
|
1156
|
+
const [taskOwner, setTaskOwner] = useState('');
|
|
1157
|
+
const [status, setStatus] = useState('');
|
|
1158
|
+
|
|
1159
|
+
const load = async () => {
|
|
1160
|
+
if (!roomId) return;
|
|
1161
|
+
try {
|
|
1162
|
+
const response = await fetch('/device-fleet/room?id=' + encodeURIComponent(roomId), { headers: { accept: 'application/json' } });
|
|
1163
|
+
if (response.status === 404) {
|
|
1164
|
+
setMissing(true);
|
|
1165
|
+
setDetail(null);
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
1169
|
+
const data = await response.json();
|
|
1170
|
+
setDetail(data.room ?? null);
|
|
1171
|
+
setMissing(false);
|
|
1172
|
+
setError('');
|
|
1173
|
+
} catch (problem) {
|
|
1174
|
+
setError(String(problem.message ?? problem));
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
useEffect(() => {
|
|
1179
|
+
void load();
|
|
1180
|
+
const timer = setInterval(load, 2500);
|
|
1181
|
+
return () => clearInterval(timer);
|
|
1182
|
+
}, [roomId]);
|
|
1183
|
+
|
|
1184
|
+
const act = async (label, action) => {
|
|
1185
|
+
setStatus(label + '…');
|
|
1186
|
+
try {
|
|
1187
|
+
await action();
|
|
1188
|
+
setStatus(label + '成功');
|
|
1189
|
+
await load();
|
|
1190
|
+
} catch (problem) {
|
|
1191
|
+
setStatus(label + '失败:' + String(problem.message ?? problem));
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
if (!roomId) return null;
|
|
1196
|
+
if (missing) {
|
|
1197
|
+
return h('div', { style: { opacity: 0.75, fontSize: 12 } }, '这个会话还不是 Room。在下方输入框旁点击「升级为 Room」,或让 agent 执行 room_create。');
|
|
1198
|
+
}
|
|
1199
|
+
if (!detail) return h('div', { style: { opacity: 0.7, fontSize: 12 } }, error ? '房间加载失败:' + error : '加载房间…');
|
|
1200
|
+
|
|
1201
|
+
const ctl = {
|
|
1202
|
+
sendText,
|
|
1203
|
+
setSendText,
|
|
1204
|
+
sendTo,
|
|
1205
|
+
setSendTo,
|
|
1206
|
+
sendRoomMessage: () =>
|
|
1207
|
+
act('发送', async () => {
|
|
1208
|
+
if (sendText.trim() === '') throw new Error('消息不能为空');
|
|
1209
|
+
await postJson('/device-fleet/room/send', { roomId, to: sendTo, text: sendText });
|
|
1210
|
+
setSendText('');
|
|
1211
|
+
}),
|
|
1212
|
+
taskTitle,
|
|
1213
|
+
setTaskTitle,
|
|
1214
|
+
taskOwner,
|
|
1215
|
+
setTaskOwner,
|
|
1216
|
+
addRoomTask: () =>
|
|
1217
|
+
act('添加任务', async () => {
|
|
1218
|
+
if (taskTitle.trim() === '') throw new Error('任务标题不能为空');
|
|
1219
|
+
await postJson('/device-fleet/room/task', { roomId, action: 'add', title: taskTitle, owner: taskOwner });
|
|
1220
|
+
setTaskTitle('');
|
|
1221
|
+
}),
|
|
1222
|
+
updateRoomTask: (id, nextStatus) => act('更新任务', () => postJson('/device-fleet/room/task', { roomId, action: 'update', id, status: nextStatus })),
|
|
1223
|
+
closeCurrentRoom: () => act('关闭房间', () => postJson('/device-fleet/room/close', { roomId })),
|
|
1224
|
+
removeRoomMember: (memberId) => act('移除成员', () => postJson('/device-fleet/room/member/remove', { roomId, memberId })),
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
return h('div', null,
|
|
1228
|
+
status ? h('div', { style: { fontSize: 11, opacity: 0.6, marginBottom: 6 } }, status) : null,
|
|
1229
|
+
renderRoomDetail(detail, ctl),
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function RoomView({ sessionId }) {
|
|
1234
|
+
return h('div', { style: { height: '100%', overflow: 'auto', padding: '14px 16px', boxSizing: 'border-box' } },
|
|
1235
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 } },
|
|
1236
|
+
h('strong', { style: { fontSize: 15 } }, '房间'),
|
|
1237
|
+
h('span', { style: { fontSize: 11, opacity: 0.55 } }, '多设备成员共享的会话'),
|
|
1238
|
+
),
|
|
1239
|
+
h(RoomPane, { roomId: sessionId }),
|
|
1142
1240
|
);
|
|
1143
1241
|
}
|
|
1144
1242
|
|
|
@@ -1158,11 +1256,14 @@ window.__ModuleLoader__.load({
|
|
|
1158
1256
|
ctx.slots.register({ name: 'sidebar.panellist', id: 'fleet', order: 20, label: '设备联邦' }, FleetIcon),
|
|
1159
1257
|
);
|
|
1160
1258
|
ctx.slots.register({ name: 'main', key: 'fleet' }, FleetConsole);
|
|
1161
|
-
// Session-native Room: member strip below the composer
|
|
1162
|
-
//
|
|
1259
|
+
// Session-native Room: member strip below the composer, plus a "房间"
|
|
1260
|
+
// Conversation View for the Room transcript/board.
|
|
1163
1261
|
ctx.slots.inject('conversation.composer.dock', () =>
|
|
1164
1262
|
ctx.slots.register({ name: 'conversation.composer.dock', id: 'fleet-room', order: 10 }, RoomDock),
|
|
1165
1263
|
);
|
|
1264
|
+
ctx.slots.inject('conversation.view', () =>
|
|
1265
|
+
ctx.slots.register({ name: 'conversation.view', id: 'room', order: 20, label: '房间' }, RoomView),
|
|
1266
|
+
);
|
|
1166
1267
|
// Configuration page: Settings → Plugins → Device Fleet (row + bundle).
|
|
1167
1268
|
ctx.slots.inject('plugins.row.config', () =>
|
|
1168
1269
|
ctx.slots.register({ name: 'plugins.row.config', key: 'dsh-device-fleet#device-fleet' }, FleetConfig),
|
|
@@ -39,19 +39,19 @@ dsh plugin --profile desktop add dsh-device-fleet@dev
|
|
|
39
39
|
- 邀请里新增 `endpoints`(创建者的局域网/热点 IPv4 + direct 端口),加入方重启后自动向这些地址回连,绕开组播发现失败;
|
|
40
40
|
- 新增 `fleet_status`(deviceId/直连端口/发现/连接/roster/hints)与 `fleet_connect(host, port)`(手动直连,成功后会从握手学到对端 deviceId);
|
|
41
41
|
- `fleet_list` 仍是本地视图;配对成功后双方 roster 会自动固定对端公钥。
|
|
42
|
-
## 0.1.0-dev.
|
|
42
|
+
## 0.1.0-dev.20(稳定化)
|
|
43
43
|
|
|
44
44
|
- 直连端口可固定(profile `direct.port`),插件重载不再换端口;
|
|
45
45
|
- 连接断开后自动重试已知地址(不再只在启动时试一次);
|
|
46
46
|
- `fleet_connect` 手动地址会持久化到 `peer-hints.json`,重启后继续回连;
|
|
47
47
|
- 新增 `fleet_delegate`(dev.5):目标设备用 `SubagentProvider` + `delivery:'caller'` 跑 agent 并返回最终结果。
|
|
48
|
-
## 0.1.0-dev.
|
|
48
|
+
## 0.1.0-dev.20(审批收件箱,逻辑层)
|
|
49
49
|
|
|
50
50
|
- 新增 `ApprovalInbox`:ask 档请求排队、超时 fail-closed、approve(once/ttl)/deny、关闭时全部失败关闭;
|
|
51
51
|
- 直调与委派的 ask 档改为等待本机收件箱结果(不再一律拒绝);
|
|
52
52
|
- 新工具:`fleet_approvals` / `fleet_approve` / `fleet_deny`;`fleet_simulate_request`(仅 `approvals.simulation: true`)用于本机无第二台设备时演练流程;
|
|
53
53
|
- 审批面板(Client UI)与通知尚未实现,当前通过工具/后续 UI 应答。
|
|
54
|
-
## 0.1.0-dev.
|
|
54
|
+
## 0.1.0-dev.20(Room 逻辑层,本机模拟)
|
|
55
55
|
|
|
56
56
|
- `RoomService`:当前会话升级为 Room Host;本地模拟参与者(真实 DSH session/agent);
|
|
57
57
|
- 寻址消息:`room_send` 只唤醒被点名成员,Host 会话写权威 `room/message` 事件;
|
|
@@ -59,53 +59,62 @@ dsh plugin --profile desktop add dsh-device-fleet@dev
|
|
|
59
59
|
- Room Board:`room_task_add/update/list`,`dependsOn` 串行交接(前置完成时唤醒下一个 owner),无依赖任务并行不受影响;
|
|
60
60
|
- 工具:`room_create / room_add_local / room_remove_member / room_send / room_messages / room_status / room_task_* / room_close`;
|
|
61
61
|
- 远程参与者传输与 Client UI 未做(下一步)。
|
|
62
|
-
## 0.1.0-dev.
|
|
62
|
+
## 0.1.0-dev.20(审批面板 UI,第一版)
|
|
63
63
|
|
|
64
64
|
- Host 新增本地 HTTP 数据面:GET `/device-fleet/approvals`、POST `/device-fleet/approvals/decide`(仅本机 web 载体,决策仍走 ApprovalInbox);
|
|
65
65
|
- Client half:`client.js` 闭包工厂,注册 `shell.overlay` 浮动「Fleet 审批」按钮 + 面板,轮询待审批列表,支持允许一次 / 允许 15 分钟 / 拒绝;
|
|
66
66
|
- package.json 增加 `dsh.client` 与 `./client` 导出;`files` 增加 client.js;
|
|
67
67
|
- Client 视觉验证需要刷新页面;当前无浏览器控制,仅验证了 bundle 语法与 Host 路由注册。
|
|
68
|
-
## 0.1.0-dev.
|
|
68
|
+
## 0.1.0-dev.20(审批入口不遮挡)
|
|
69
69
|
|
|
70
70
|
- 触发按钮从右下角浮动改为 `sidebar.footer.action`(设置旁边),不再遮挡任务列表/内容面板;
|
|
71
71
|
- 审批 inbox 改为点击后居中的模态对话框(带遮罩,Esc/点遮罩关闭);
|
|
72
72
|
- 轮询在 Client apply 中启动并由 ctx.effect 清理;待审批数量作为按钮徽标;
|
|
73
73
|
- 已实测:面板点击「允许」后 Host `fleet_approvals` 清空。
|
|
74
|
-
## 0.1.0-dev.
|
|
74
|
+
## 0.1.0-dev.20(Phase B:运行时配置 + 配置页)
|
|
75
75
|
|
|
76
76
|
- `RuntimeConfig`:profile patch 为 base,UI 修改写入 `$DSH_HOME/device-fleet/config-overrides.json`,apply 时合并并持久化;
|
|
77
77
|
- 保存即生效:policy/execution/approvals/deviceName/promptSection/autoApproveAsk 直接更新运行时对象;direct/discovery/relay 通过 `FleetNode.reconfigure()` 原地重启网络监听(不重启插件/DSH);`dataDir` 只读;
|
|
78
78
|
- Host:`GET /device-fleet/config`、`POST /device-fleet/config`(校验/清洗后应用);
|
|
79
79
|
- Client:注册 `plugins.row.config`(key `dsh-device-fleet#device-fleet`)与 `plugins.bundle.config`(key `dsh-device-fleet`),配置页含设备/网络、执行上下文、Tool Policy、审批与提示四组表单;
|
|
80
80
|
- 单测 18/18、网络 9/9。
|
|
81
|
-
## 0.1.0-dev.
|
|
81
|
+
## 0.1.0-dev.20(配置页 DSH 化改版)
|
|
82
82
|
|
|
83
83
|
- Host 新增 `GET /device-fleet/options`:本机地址、可用 preset 列表、shell 工具、subagent providers、工作目录候选;
|
|
84
84
|
- 配置保存前会 ensure 工作目录存在(Device Fleet 工作区自动创建);
|
|
85
85
|
- 配置页重做:行式布局(label 左/控件右)、DSH token 配色、分组标题、去掉多余说明文字与 dataDir 展示;
|
|
86
86
|
- 下拉化:直连地址、局域网发现、Relay 模式、工作目录(工作区/默认/DSH 主目录/自定义)、Agent Preset、Shell、委派 Provider、7 项能力权限、开关项;
|
|
87
87
|
- 仍保留文本输入:设备名称、端口、超时、根目录/工具列表、自定义工作目录与 Relay 地址。
|
|
88
|
-
## 0.1.0-dev.
|
|
88
|
+
## 0.1.0-dev.20(房间详情与配置页修复)
|
|
89
89
|
|
|
90
90
|
- 房间详情接口从 prefix 路由改为精确路由 `GET /device-fleet/room?id=...`,修复控制台一直「加载房间…」;客户端加载失败时显示错误文案;
|
|
91
91
|
- 配置页宽行改为「标签在上、控件在下」,修复根目录等标签竖排换行;
|
|
92
92
|
- 保存按钮改为固定品牌蓝底白字,修复按钮看起来空白;
|
|
93
93
|
- 「允许的原始工具」改为「免审批即用的工具」:从 Host options 拉取本机真实工具清单,以复选框选择(不再手输工具名)。
|
|
94
|
-
## 0.1.0-dev.
|
|
94
|
+
## 0.1.0-dev.20(原始工具遵循 DSH 审批)
|
|
95
95
|
|
|
96
96
|
- 移除「免审批即用的工具」白名单;raw tool 调用不再由插件 allowlist 决定;
|
|
97
97
|
- `policy.capabilities.tool` 默认 `auto`:完全走 DSH 工具流水线(target Session 的 pre-execute policy + approval seam);`deny` 禁用 raw tools,`ask` 在 DSH 前加一层本机审批;
|
|
98
98
|
- 新增 `approval/request` 监听:Execution Slot 里 DSH 需要审批的工具调用会转到本设备审批收件箱(面板可处理),不需要审批的工具直接执行;
|
|
99
99
|
- 旧配置里 `tool: deny` + 空 allowedTools 自动迁移为 `auto`;
|
|
100
100
|
- 配置页移除工具复选框列表,「原始工具(遵循 DSH 审批)」保留 自动/询问/拒绝 下拉。
|
|
101
|
-
## 0.1.0-dev.
|
|
101
|
+
## 0.1.0-dev.20(Phase C:交互动作)
|
|
102
102
|
|
|
103
103
|
- Room 用户动作:以用户身份发消息(选择收件人)、看板任务添加/状态更新、关闭房间、移除成员;
|
|
104
104
|
- 设备动作:生成/复制邀请码、手动地址连接(host+port);
|
|
105
105
|
- 审批历史:ApprovalInbox 记录最近 50 条已决请求(approved/denied/expired/closed),Console 审批 Tab 展示历史;
|
|
106
106
|
- 新增 Host 动作路由:`/room/send`、`/room/task`、`/room/close`、`/room/member/remove`、`/pair/invite`、`/pair/connect`、`/approvals/history`。
|
|
107
|
-
## 0.1.0-dev.
|
|
107
|
+
## 0.1.0-dev.20(修复控制台空白)
|
|
108
108
|
|
|
109
109
|
- 根因:FleetConsole 的设备/房间交互误用了从未定义的 `ActionButton` 组件(React 渲染抛 ReferenceError,主面板空白)。
|
|
110
110
|
- 修复:统一改用已有的 `smallButton`。
|
|
111
|
-
- 新增 `scripts/client-smoke.mjs`:用最小 Hooks 渲染器 + mock fetch 实际渲染控制台(初始/设备/房间列表/房间详情),防止这类渲染崩溃再次漏过 `node --check`。
|
|
111
|
+
- 新增 `scripts/client-smoke.mjs`:用最小 Hooks 渲染器 + mock fetch 实际渲染控制台(初始/设备/房间列表/房间详情),防止这类渲染崩溃再次漏过 `node --check`。
|
|
112
|
+
## 0.1.0-dev.20(Room 回归 Session)
|
|
113
|
+
|
|
114
|
+
- Room 恢复为「就是普通 Session」的体验:会话输入框下方新增 Room 成员条(composer.dock),显示 Room 成员;普通会话显示「升级为 Room」按钮(Host API `/room/upgrade`,无需工具调用);
|
|
115
|
+
- Host 监听 `session/event` 的 `user/message`:Room Host 会话里用户正常聊天输入会自动投递给房间参与者并唤醒它们(带 `[Room]` 前缀的注入消息不重播,避免循环);
|
|
116
|
+
- 参与者回复投递回 Host 时以 `[Room] <成员>: <内容>` 形式进入正常 Chat 流,不再需要独立的房间仪表盘;
|
|
117
|
+
- Fleet 控制台保留为设备/配对/审批/配置管理入口。
|
|
118
|
+
## 0.1.0-dev.21(Room 回复地址修复)
|
|
119
|
+
|
|
120
|
+
- 参与者被用户消息唤醒时,回复引导里的目标从无效的 "host" 修正为真实 Host member id(`host:<sessionId>`),修复 worker 回执发给自己/失败的问题。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-device-fleet",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.22",
|
|
4
4
|
"description": "DeepSeek Harness device fleet: enroll devices, direct-call remote tools, shared Rooms with lazy-woken agents, and delegated subagents across devices.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/plugin/index.js",
|
package/src/host/room-service.js
CHANGED
|
@@ -247,7 +247,12 @@ export class RoomService {
|
|
|
247
247
|
if (!member || typeof this.createUserMessage !== 'function') return false
|
|
248
248
|
if (!(await this.ensureAgent(member))) return false
|
|
249
249
|
if (typeof member.agent?.followup !== 'function') return false
|
|
250
|
+
// Participant replies are rendered by the Room view; do not inject them into
|
|
251
|
+
// the Host's normal Chat stream (that caused the host agent to relay them).
|
|
252
|
+
if (member.role === 'host' && message.from.kind !== 'user') return false
|
|
250
253
|
const senderLabel = message.from.kind === 'user' ? 'user' : (message.from.name ?? 'agent')
|
|
254
|
+
// A user sender has no member id; the reply target is always the Host member.
|
|
255
|
+
const replyTarget = message.from.memberId ?? hostMemberId(room.hostSessionId)
|
|
251
256
|
// The host is the user's own session: deliver a plain attributed line into
|
|
252
257
|
// the normal Chat stream instead of an agent-to-agent reply instruction.
|
|
253
258
|
const text =
|
|
@@ -257,7 +262,7 @@ export class RoomService {
|
|
|
257
262
|
`[Fleet Room ${room.roomId}] ${senderLabel} → you:`,
|
|
258
263
|
message.text,
|
|
259
264
|
'',
|
|
260
|
-
`Reply with room_send (roomId="${room.roomId}", to=["${
|
|
265
|
+
`Reply with room_send (roomId="${room.roomId}", to=["${replyTarget}"]) when you have an answer.`,
|
|
261
266
|
].join('\n')
|
|
262
267
|
const userMessage = this.createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } })
|
|
263
268
|
try {
|
package/src/plugin/index.js
CHANGED
|
@@ -304,7 +304,9 @@ export function apply(ctx, profileConfig) {
|
|
|
304
304
|
async sendRoomMessage({ roomId, to, text } = {}) {
|
|
305
305
|
if (typeof roomId !== 'string' || roomId === '') throw Object.assign(new Error('roomId is required'), { code: 'bad-request' })
|
|
306
306
|
const memberIds = rooms.status(roomId).members.map((member) => member.memberId)
|
|
307
|
-
const
|
|
307
|
+
const hostMemberId = `host:${roomId}`
|
|
308
|
+
const defaultRecipients = memberIds.filter((id) => id !== hostMemberId)
|
|
309
|
+
const recipients = Array.isArray(to) && to.length > 0 ? to : defaultRecipients.length > 0 ? defaultRecipients : [hostMemberId]
|
|
308
310
|
const result = await rooms.send({ roomId, from: { kind: 'user' }, to: recipients, text })
|
|
309
311
|
return { roomId, ...result }
|
|
310
312
|
},
|
|
@@ -424,37 +426,6 @@ export function apply(ctx, profileConfig) {
|
|
|
424
426
|
return ctx.on('approval/request', handler)
|
|
425
427
|
})
|
|
426
428
|
|
|
427
|
-
// A Room is a normal Session whose user input is also delivered to its
|
|
428
|
-
// participants. Typing in the Host conversation therefore needs no separate
|
|
429
|
-
// Room composer.
|
|
430
|
-
ctx.effect(() =>
|
|
431
|
-
ctx.on?.('session/event', (session, event) => {
|
|
432
|
-
try {
|
|
433
|
-
if (!session?.id || event?.type !== 'user/message') return
|
|
434
|
-
const room = rooms.rooms.get(session.id)
|
|
435
|
-
if (!room) return
|
|
436
|
-
const message = event.data
|
|
437
|
-
if (message?.source?.kind && message.source.kind !== 'user') return
|
|
438
|
-
const text = (Array.isArray(message?.content) ? message.content : [])
|
|
439
|
-
.filter((block) => block?.type === 'text')
|
|
440
|
-
.map((block) => block.text)
|
|
441
|
-
.join('\n')
|
|
442
|
-
.trim()
|
|
443
|
-
if (text === '' || text.startsWith('[Room]') || text.startsWith('[Fleet Room')) return
|
|
444
|
-
const recipients = rooms
|
|
445
|
-
.status(session.id)
|
|
446
|
-
.members.filter((member) => member.role !== 'host')
|
|
447
|
-
.map((member) => member.memberId)
|
|
448
|
-
if (recipients.length === 0) return
|
|
449
|
-
void rooms.send({ roomId: session.id, from: { kind: 'user' }, to: recipients, text }).catch((error) => {
|
|
450
|
-
logger.warn?.(`[device-fleet] cannot broadcast room message: ${String(error?.message ?? error)}`)
|
|
451
|
-
})
|
|
452
|
-
} catch (error) {
|
|
453
|
-
logger.warn?.(`[device-fleet] room broadcast failed: ${String(error?.message ?? error)}`)
|
|
454
|
-
}
|
|
455
|
-
}),
|
|
456
|
-
)
|
|
457
|
-
|
|
458
429
|
node.onRequest(async (request) => {
|
|
459
430
|
if (!host) throw Object.assign(new Error('this device cannot serve direct calls (no execution slot)'), { code: 'unsupported' })
|
|
460
431
|
return host.handleRequest(request)
|