dsh-device-fleet 0.1.0-dev.21 → 0.1.0-dev.23

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,10 +1140,287 @@ 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 } }, '房间消息在「房间」页交流'),
1142
1144
  );
1143
1145
  }
1144
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 roomAvatar(name, tone) {
1234
+ const text = String(name ?? '?').trim().slice(0, 1).toUpperCase() || '?'
1235
+ return h('div', {
1236
+ style: {
1237
+ width: 26,
1238
+ height: 26,
1239
+ borderRadius: '50%',
1240
+ flex: '0 0 26px',
1241
+ display: 'flex',
1242
+ alignItems: 'center',
1243
+ justifyContent: 'center',
1244
+ fontSize: 12,
1245
+ fontWeight: 600,
1246
+ background: tone === 'user' ? '#2f6fed' : 'var(--dsw-alias-bg-layer-2, #262626)',
1247
+ color: tone === 'user' ? '#fff' : 'var(--dsw-alias-label-primary, #e8e8e8)',
1248
+ border: '1px solid var(--dsw-alias-border-l1, #333)',
1249
+ },
1250
+ }, text)
1251
+ }
1252
+
1253
+ // Room conversation rendered as a chat: agents on the left with names and
1254
+ // avatars, the user on the right. This is the Room's presentation instead of
1255
+ // injecting participant speech into the host's ordinary Chat.
1256
+ function RoomChatPane({ roomId }) {
1257
+ const [detail, setDetail] = useState(null);
1258
+ const [missing, setMissing] = useState(false);
1259
+ const [error, setError] = useState('');
1260
+ const [sendText, setSendText] = useState('');
1261
+ const [sendTo, setSendTo] = useState([]);
1262
+ const [status, setStatus] = useState('');
1263
+ const [showBoard, setShowBoard] = useState(false);
1264
+ const [taskTitle, setTaskTitle] = useState('');
1265
+ const [taskOwner, setTaskOwner] = useState('');
1266
+
1267
+ const load = async () => {
1268
+ if (!roomId) return;
1269
+ try {
1270
+ const response = await fetch('/device-fleet/room?id=' + encodeURIComponent(roomId), { headers: { accept: 'application/json' } });
1271
+ if (response.status === 404) {
1272
+ setMissing(true);
1273
+ setDetail(null);
1274
+ return;
1275
+ }
1276
+ if (!response.ok) throw new Error('HTTP ' + response.status);
1277
+ const data = await response.json();
1278
+ setDetail(data.room ?? null);
1279
+ setMissing(false);
1280
+ setError('');
1281
+ } catch (problem) {
1282
+ setError(String(problem.message ?? problem));
1283
+ }
1284
+ };
1285
+
1286
+ useEffect(() => {
1287
+ void load();
1288
+ const timer = setInterval(load, 2000);
1289
+ return () => clearInterval(timer);
1290
+ }, [roomId]);
1291
+
1292
+ const act = async (label, action) => {
1293
+ setStatus(label + '…');
1294
+ try {
1295
+ await action();
1296
+ setStatus(label + '成功');
1297
+ await load();
1298
+ } catch (problem) {
1299
+ setStatus(label + '失败:' + String(problem.message ?? problem));
1300
+ }
1301
+ };
1302
+
1303
+ if (!roomId) return null;
1304
+ if (missing) {
1305
+ return h('div', { style: { padding: 16, opacity: 0.75, fontSize: 12 } }, '这个会话还不是 Room。点击输入框下方的「升级为 Room」即可创建。');
1306
+ }
1307
+ if (!detail) return h('div', { style: { padding: 16, opacity: 0.7, fontSize: 12 } }, error ? '房间加载失败:' + error : '加载房间…');
1308
+
1309
+ const nameOf = (memberId) => detail.members.find((member) => member.memberId === memberId)?.name ?? memberId
1310
+ const participants = detail.members.filter((member) => member.role !== 'host')
1311
+ const defaultRecipients = participants.length > 0 ? participants.map((member) => member.memberId) : detail.members.map((member) => member.memberId)
1312
+ const recipients = sendTo.length > 0 ? sendTo : defaultRecipients
1313
+ const send = () =>
1314
+ act('发送', async () => {
1315
+ if (sendText.trim() === '') throw new Error('消息不能为空')
1316
+ await postJson('/device-fleet/room/send', { roomId, to: recipients, text: sendText })
1317
+ setSendText('')
1318
+ })
1319
+
1320
+ const messages = detail.messages.map((message) => {
1321
+ const fromUser = message.from?.kind === 'user'
1322
+ const author = fromUser ? '你' : message.from?.name ?? 'agent'
1323
+ const targets = (message.to ?? []).map(nameOf).join(', ')
1324
+ return h('div', { key: message.id, style: { display: 'flex', justifyContent: fromUser ? 'flex-end' : 'flex-start', gap: 8, margin: '10px 0' } },
1325
+ fromUser ? null : roomAvatar(author, 'agent'),
1326
+ h('div', { style: { maxWidth: '74%' } },
1327
+ fromUser ? null : h('div', { style: { fontSize: 11, opacity: 0.6, marginBottom: 2 } }, author + (targets ? ' · → ' + targets : '')),
1328
+ h('div', {
1329
+ style: {
1330
+ background: fromUser ? '#2f6fed' : 'var(--dsw-alias-bg-layer-2, #262626)',
1331
+ color: fromUser ? '#fff' : 'var(--dsw-alias-label-primary, #e8e8e8)',
1332
+ padding: '8px 12px',
1333
+ borderRadius: 12,
1334
+ whiteSpace: 'pre-wrap',
1335
+ fontSize: 13,
1336
+ lineHeight: 1.5,
1337
+ },
1338
+ }, message.text),
1339
+ h('div', { style: { fontSize: 10, opacity: 0.45, marginTop: 2, textAlign: fromUser ? 'right' : 'left' } }, fmtTime(message.createdAt)),
1340
+ ),
1341
+ fromUser ? roomAvatar('你', 'user') : null,
1342
+ )
1343
+ })
1344
+
1345
+ return h('div', { style: { height: '100%', display: 'flex', flexDirection: 'column', padding: '12px 16px', boxSizing: 'border-box' } },
1346
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginBottom: 8 } },
1347
+ h('strong', { style: { fontSize: 15 } }, 'Room'),
1348
+ ...detail.members.map((member) =>
1349
+ h('span', { key: member.memberId, style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 11, opacity: 0.8 } },
1350
+ roomAvatar(member.name ?? member.memberId, member.role === 'host' ? 'user' : 'agent'),
1351
+ member.name + (member.role === 'host' ? '(host)' : ''),
1352
+ ),
1353
+ ),
1354
+ h('span', { style: { flex: 1 } }),
1355
+ status ? h('span', { style: { fontSize: 11, opacity: 0.6 } }, status) : null,
1356
+ smallButton(showBoard ? '收起看板' : '看板', () => setShowBoard((value) => !value), 'normal'),
1357
+ smallButton('关闭房间', () => { void act('关闭房间', () => postJson('/device-fleet/room/close', { roomId })) }, 'deny'),
1358
+ ),
1359
+ h('div', { style: { flex: 1, overflow: 'auto', minHeight: 120 } }, messages.length === 0 ? h('div', { style: { opacity: 0.6, fontSize: 12 } }, '还没有消息') : messages),
1360
+ showBoard
1361
+ ? h('div', { style: { ...cardStyle, marginTop: 8 } },
1362
+ h('div', { style: sectionTitle }, '看板'),
1363
+ detail.tasks.length === 0 ? h('div', { style: { opacity: 0.65, fontSize: 12 } }, '暂无任务') : detail.tasks.map((task) =>
1364
+ h('div', { key: task.id, style: { display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0', fontSize: 12 } },
1365
+ h('span', { style: { flex: 1 } }, task.title + ' · ' + nameOf(task.owner)),
1366
+ h('select', {
1367
+ value: task.status,
1368
+ onChange: (event) => void act('更新任务', () => postJson('/device-fleet/room/task', { roomId, action: 'update', id: task.id, status: event.target.value })),
1369
+ style: { height: 24, borderRadius: 5, border: '1px solid var(--dsw-alias-border-l1, #333)', background: 'var(--dsw-alias-bg-layer-2, #262626)', color: 'inherit', fontSize: 11 },
1370
+ }, ...['pending', 'running', 'done', 'blocked'].map((value) => h('option', { key: value, value }, value))),
1371
+ ),
1372
+ ),
1373
+ h('div', { style: { display: 'flex', gap: 6, marginTop: 8, flexWrap: 'wrap' } },
1374
+ h('input', { type: 'text', value: taskTitle, onChange: (event) => setTaskTitle(event.target.value), placeholder: '新任务', style: { ...controlStyleWidth(160), height: 26, fontSize: 12 } }),
1375
+ h('select', { value: taskOwner, onChange: (event) => setTaskOwner(event.target.value), style: { ...controlStyleWidth(150), height: 26, fontSize: 12 } },
1376
+ h('option', { value: '' }, 'owner'),
1377
+ ...detail.members.map((member) => h('option', { key: member.memberId, value: member.memberId }, member.name)),
1378
+ ),
1379
+ smallButton('添加', () => void act('添加任务', () => postJson('/device-fleet/room/task', { roomId, action: 'add', title: taskTitle, owner: taskOwner })), 'allow'),
1380
+ ),
1381
+ )
1382
+ : null,
1383
+ h('div', { style: { ...cardStyle, marginTop: 8 } },
1384
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px 12px', marginBottom: 6 } },
1385
+ h('span', { style: { fontSize: 11, opacity: 0.6, alignSelf: 'center' } }, '发给'),
1386
+ ...detail.members.filter((member) => member.role !== 'host').map((member) =>
1387
+ h('label', { key: member.memberId, style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 } },
1388
+ h('input', {
1389
+ type: 'checkbox',
1390
+ checked: recipients.includes(member.memberId),
1391
+ onChange: (event) => {
1392
+ setSendTo((current) => {
1393
+ const base = current.length > 0 ? current : defaultRecipients
1394
+ return event.target.checked ? [...new Set([...base, member.memberId])] : base.filter((id) => id !== member.memberId)
1395
+ })
1396
+ },
1397
+ }),
1398
+ member.name,
1399
+ ),
1400
+ ),
1401
+ ),
1402
+ h('textarea', {
1403
+ value: sendText,
1404
+ onChange: (event) => setSendText(event.target.value),
1405
+ onKeyDown: (event) => {
1406
+ if (event.key === 'Enter' && !event.shiftKey) {
1407
+ event.preventDefault()
1408
+ void send()
1409
+ }
1410
+ },
1411
+ rows: 2,
1412
+ placeholder: '发消息给房间成员(Enter 发送,Shift+Enter 换行)',
1413
+ style: { width: '100%', boxSizing: 'border-box', padding: '7px 9px', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l1, #333)', background: 'var(--dsw-alias-bg-layer-2, #262626)', color: 'inherit', fontSize: 13, resize: 'vertical', fontFamily: 'inherit' },
1414
+ }),
1415
+ h('div', { style: { display: 'flex', justifyContent: 'flex-end', marginTop: 6 } }, smallButton('发送', () => void send(), 'allow')),
1416
+ ),
1417
+ )
1418
+ }
1419
+
1420
+ function RoomView({ sessionId }) {
1421
+ return h('div', { style: { height: '100%' } }, h(RoomChatPane, { roomId: sessionId }));
1422
+ }
1423
+
1145
1424
  return {
1146
1425
  inject: ['slots'],
1147
1426
  apply(ctx) {
@@ -1158,11 +1437,14 @@ window.__ModuleLoader__.load({
1158
1437
  ctx.slots.register({ name: 'sidebar.panellist', id: 'fleet', order: 20, label: '设备联邦' }, FleetIcon),
1159
1438
  );
1160
1439
  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.
1440
+ // Session-native Room: member strip below the composer, plus a "房间"
1441
+ // Conversation View for the Room transcript/board.
1163
1442
  ctx.slots.inject('conversation.composer.dock', () =>
1164
1443
  ctx.slots.register({ name: 'conversation.composer.dock', id: 'fleet-room', order: 10 }, RoomDock),
1165
1444
  );
1445
+ ctx.slots.inject('conversation.view', () =>
1446
+ ctx.slots.register({ name: 'conversation.view', id: 'room', order: 20, label: '房间' }, RoomView),
1447
+ );
1166
1448
  // Configuration page: Settings → Plugins → Device Fleet (row + bundle).
1167
1449
  ctx.slots.inject('plugins.row.config', () =>
1168
1450
  ctx.slots.register({ name: 'plugins.row.config', key: 'dsh-device-fleet#device-fleet' }, FleetConfig),
@@ -114,4 +114,13 @@ 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 回执发给自己/失败的问题。
121
+ ## 0.1.0-dev.22(Room 独立视图,停止 host 转述)
122
+
123
+ - Session 内新增「房间」Conversation View(`conversation.view`,label 房间):展示成员、时间线(含发言者/收件人)、看板与房间操作,输入默认发给非 host 参与者;
124
+ - 移除 Chat 输入的隐式 Room 广播:普通 Chat 只属于 host agent,Room 交流在「房间」页进行,避免用户在 Chat 里说话被自动转发、或 host agent 转述 worker 回复;
125
+ - host 不再接收参与者回复的 Chat 注入(参与者回复只进入房间时间线);
126
+ - 输入框下方成员条提示「房间消息在『房间』页交流」。
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.23",
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)