dsh-device-fleet 0.1.0-dev.21 → 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 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 recipientIds = Array.isArray(ctl.sendTo) && ctl.sendTo.length > 0 ? ctl.sendTo : memberIds.filter((id) => id.startsWith('host:'));
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 : recipientIds
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; the normal Chat
1162
- // stream carries the conversation.
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),
@@ -114,4 +114,7 @@ dsh plugin --profile desktop add dsh-device-fleet@dev
114
114
  - Room 恢复为「就是普通 Session」的体验:会话输入框下方新增 Room 成员条(composer.dock),显示 Room 成员;普通会话显示「升级为 Room」按钮(Host API `/room/upgrade`,无需工具调用);
115
115
  - Host 监听 `session/event` 的 `user/message`:Room Host 会话里用户正常聊天输入会自动投递给房间参与者并唤醒它们(带 `[Room]` 前缀的注入消息不重播,避免循环);
116
116
  - 参与者回复投递回 Host 时以 `[Room] <成员>: <内容>` 形式进入正常 Chat 流,不再需要独立的房间仪表盘;
117
- - Fleet 控制台保留为设备/配对/审批/配置管理入口。
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.21",
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",
@@ -247,6 +247,9 @@ 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')
251
254
  // A user sender has no member id; the reply target is always the Host member.
252
255
  const replyTarget = message.from.memberId ?? hostMemberId(room.hostSessionId)
@@ -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 recipients = Array.isArray(to) && to.length > 0 ? to : memberIds.filter((id) => id.startsWith('host:'))
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)