dsh-device-fleet 0.1.0-dev.26 → 0.1.0-dev.28
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 +16 -65
- package/docs/notes/dev-release.md +10 -1
- package/package.json +1 -1
- package/src/host/room-service.js +2 -1
- package/src/plugin/index.js +44 -0
package/client.js
CHANGED
|
@@ -1094,24 +1094,22 @@ window.__ModuleLoader__.load({
|
|
|
1094
1094
|
h(NumberControl, { value: draft.execDelegateTimeoutMs, onChange: (value) => set('execDelegateTimeoutMs', value) }),
|
|
1095
1095
|
),
|
|
1096
1096
|
h('div', { style: sectionStyle }, '头像库'),
|
|
1097
|
-
|
|
1098
|
-
h('div',
|
|
1099
|
-
h('
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
h('
|
|
1108
|
-
|
|
1109
|
-
smallButton('删除', () => void removeAvatar(item.name), 'deny'),
|
|
1110
|
-
),
|
|
1097
|
+
h('div', { style: { ...cardStyle, padding: 14 } },
|
|
1098
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } },
|
|
1099
|
+
h('input', { type: 'file', accept: 'image/png,image/jpeg,image/webp', multiple: true, onChange: (event) => void uploadAvatars(event) }),
|
|
1100
|
+
avatarStatus ? h('span', { style: { fontSize: 12, opacity: 0.7 } }, avatarStatus) : null,
|
|
1101
|
+
),
|
|
1102
|
+
library.length === 0
|
|
1103
|
+
? h('div', { style: { fontSize: 12, opacity: 0.6, marginTop: 8 } }, '未上传头像时使用内置头像;上传后 Room 成员按顺序循环分配')
|
|
1104
|
+
: h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 10, marginTop: 10 } },
|
|
1105
|
+
...library.map((item) =>
|
|
1106
|
+
h('div', { key: item.name, style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 } },
|
|
1107
|
+
h('img', { src: item.url, alt: item.name, style: { width: 46, height: 46, borderRadius: '50%', objectFit: 'cover', border: '1px solid var(--dsw-alias-border-l1, #333)' } }),
|
|
1108
|
+
smallButton('删除', () => void removeAvatar(item.name), 'deny'),
|
|
1111
1109
|
),
|
|
1112
1110
|
),
|
|
1113
|
-
|
|
1114
|
-
'
|
|
1111
|
+
),
|
|
1112
|
+
h('div', { style: { fontSize: 11, opacity: 0.55, marginTop: 8 } }, '图片存放在设备数据目录的 avatars/ 下;Room 视图按成员顺序循环取用,可点「换头像」切换到下一组。'),
|
|
1115
1113
|
),
|
|
1116
1114
|
h('div', { style: sectionStyle }, '远端权限'),
|
|
1117
1115
|
h(Row, { label: '允许的根目录', wide: true },
|
|
@@ -1505,55 +1503,8 @@ window.__ModuleLoader__.load({
|
|
|
1505
1503
|
),
|
|
1506
1504
|
)
|
|
1507
1505
|
: null,
|
|
1508
|
-
h('div', { style: { ...cardStyle, marginTop: 8 } },
|
|
1509
|
-
|
|
1510
|
-
h('span', { style: { fontSize: 11, opacity: 0.6 } }, '发给'),
|
|
1511
|
-
sendTo.length === 0
|
|
1512
|
-
? h('span', { style: { fontSize: 11, opacity: 0.8 } }, '所有人')
|
|
1513
|
-
: sendTo.map((memberId) =>
|
|
1514
|
-
h('span', {
|
|
1515
|
-
key: memberId,
|
|
1516
|
-
style: { fontSize: 11, padding: '1px 7px', borderRadius: 999, border: '1px solid var(--dsw-alias-accent, #2f6fed)', color: 'var(--dsw-alias-accent, #2f6fed)', cursor: 'pointer' },
|
|
1517
|
-
onClick: () => setSendTo((current) => current.filter((id) => id !== memberId)),
|
|
1518
|
-
}, '@' + nameOf(memberId) + ' ×'),
|
|
1519
|
-
),
|
|
1520
|
-
h('span', { style: { flex: 1 } }),
|
|
1521
|
-
h('span', { style: { fontSize: 10, opacity: 0.45 } }, '输入 @ 点名'),
|
|
1522
|
-
),
|
|
1523
|
-
h('textarea', {
|
|
1524
|
-
value: sendText,
|
|
1525
|
-
onChange: (event) => setSendText(event.target.value),
|
|
1526
|
-
onKeyDown: (event) => {
|
|
1527
|
-
if (event.key === 'Enter' && !event.shiftKey) {
|
|
1528
|
-
event.preventDefault()
|
|
1529
|
-
void send()
|
|
1530
|
-
}
|
|
1531
|
-
},
|
|
1532
|
-
rows: 2,
|
|
1533
|
-
placeholder: '发消息给房间成员;输入 @ 可点名(Enter 发送,Shift+Enter 换行)',
|
|
1534
|
-
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' },
|
|
1535
|
-
}),
|
|
1536
|
-
mentionInfo && mentionCandidates.length > 0
|
|
1537
|
-
? h('div', { style: { marginTop: 6, border: '1px solid var(--dsw-alias-border-l1, #333)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2, #262626)', overflow: 'hidden' } },
|
|
1538
|
-
...mentionCandidates.map((member) =>
|
|
1539
|
-
h('button', {
|
|
1540
|
-
key: member.memberId,
|
|
1541
|
-
type: 'button',
|
|
1542
|
-
onClick: () => {
|
|
1543
|
-
setSendTo((current) => [...new Set([...current, member.memberId])])
|
|
1544
|
-
setSendText((text) => {
|
|
1545
|
-
const match = /(?:^|\s)@([^\s@]*)$/.exec(text)
|
|
1546
|
-
if (!match) return text
|
|
1547
|
-
const start = text.length - match[1].length - 1
|
|
1548
|
-
return text.slice(0, start).replace(/\s+$/, '')
|
|
1549
|
-
})
|
|
1550
|
-
},
|
|
1551
|
-
style: { display: 'flex', alignItems: 'center', gap: 6, width: '100%', padding: '6px 9px', background: 'transparent', border: 'none', color: 'inherit', cursor: 'pointer', fontSize: 12, textAlign: 'left' },
|
|
1552
|
-
}, avatarImg(memberAvatarSrc(member), member.name ?? member.memberId), member.name + (member.role === 'host' ? '(host)' : '')),
|
|
1553
|
-
),
|
|
1554
|
-
)
|
|
1555
|
-
: null,
|
|
1556
|
-
h('div', { style: { display: 'flex', justifyContent: 'flex-end', marginTop: 6 } }, smallButton('发送', () => void send(), 'allow')),
|
|
1506
|
+
h('div', { style: { ...cardStyle, marginTop: 8, fontSize: 12, opacity: 0.75 } },
|
|
1507
|
+
'在下方 DSH 输入框发送:用 @成员名 点名(例如 @worker-c),不点名则发给所有人。',
|
|
1557
1508
|
),
|
|
1558
1509
|
)
|
|
1559
1510
|
}
|
|
@@ -138,4 +138,13 @@ dsh plugin --profile desktop add dsh-device-fleet@dev
|
|
|
138
138
|
## 0.1.0-dev.25(Room 头像)
|
|
139
139
|
|
|
140
140
|
- Room 对话头像改为图片:worker → worker.png(蓝色大肥鱼),host → host.png(偷吃 Token),user → user.png;
|
|
141
|
-
- Host 新增 `/device-fleet/avatar/{host,worker,user}` 精确路由,从插件 `assets/avatars/` 提供 PNG;package `files` 包含 assets。
|
|
141
|
+
- Host 新增 `/device-fleet/avatar/{host,worker,user}` 精确路由,从插件 `assets/avatars/` 提供 PNG;package `files` 包含 assets。
|
|
142
|
+
## 0.1.0-dev.26(Room 自动切换 + 头像库)
|
|
143
|
+
|
|
144
|
+
- RoomDock 在检测到会话是 Room 时尝试通过标准 slot 的 `useConversation` 钩子自动切到「房间」Conversation View(不同版本的 shell 若未暴露该 API 则保持手动切换);
|
|
145
|
+
- 头像库:Settings → Plugins → Device Fleet 配置页新增「头像库」,可多选上传 png/jpeg/webp;Host 存到 `$DSH_HOME/.../avatars/`,提供列表/预览/删除接口;
|
|
146
|
+
- Room 成员头像按成员顺序循环分配库中图片,可点「换头像」切换到下一组;未上传时使用内置三张图。
|
|
147
|
+
## 0.1.0-dev.27(修复插件配置页空白)
|
|
148
|
+
|
|
149
|
+
- 根因:dev.26 头像库区块误用了不存在的 `sectionCard`,FleetConfig 渲染抛 ReferenceError,导致 Settings → Plugins → Device Fleet 配置页消失。
|
|
150
|
+
- 修复:头像库改用普通卡片容器;client smoke 增加配置页真实渲染用例(plugins.row.config)防止回归。
|
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.28",
|
|
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
|
@@ -186,7 +186,7 @@ export class RoomService {
|
|
|
186
186
|
* Send one addressed message. The Host appends it to the canonical log and
|
|
187
187
|
* wakes only the addressed local members (never the sender).
|
|
188
188
|
*/
|
|
189
|
-
async send({ roomId, from, to, text } = {}) {
|
|
189
|
+
async send({ roomId, from, to, text, skipWakeFor = [] } = {}) {
|
|
190
190
|
const room = this.requireRoom(roomId)
|
|
191
191
|
const body = typeof text === 'string' ? text.trim() : ''
|
|
192
192
|
if (body === '') throw Object.assign(new Error('room message needs text'), { code: 'bad-request' })
|
|
@@ -216,6 +216,7 @@ export class RoomService {
|
|
|
216
216
|
const woken = []
|
|
217
217
|
for (const memberId of recipients) {
|
|
218
218
|
if (sender.memberId === memberId) continue
|
|
219
|
+
if (skipWakeFor.includes(memberId)) continue
|
|
219
220
|
const member = room.members.get(memberId)
|
|
220
221
|
if (await this.wake(room, member, message)) woken.push(memberId)
|
|
221
222
|
}
|
package/src/plugin/index.js
CHANGED
|
@@ -435,6 +435,50 @@ export function apply(ctx, profileConfig) {
|
|
|
435
435
|
return ctx.on('approval/request', handler)
|
|
436
436
|
})
|
|
437
437
|
|
|
438
|
+
// A Room reuses the normal DSH composer: every user message in a Room Host
|
|
439
|
+
// session is logged as a Room message. `@name` addresses specific members;
|
|
440
|
+
// without mentions it goes to every participant. Host itself already has the
|
|
441
|
+
// message as normal Chat input, so it is never re-woken.
|
|
442
|
+
ctx.effect(() =>
|
|
443
|
+
ctx.on?.('session/event', (session, event) => {
|
|
444
|
+
try {
|
|
445
|
+
if (!session?.id || event?.type !== 'user/message') return
|
|
446
|
+
const room = rooms.rooms.get(session.id)
|
|
447
|
+
if (!room) return
|
|
448
|
+
const message = event.data
|
|
449
|
+
if (message?.source?.kind && message.source.kind !== 'user') return
|
|
450
|
+
const text = (Array.isArray(message?.content) ? message.content : [])
|
|
451
|
+
.filter((block) => block?.type === 'text')
|
|
452
|
+
.map((block) => block.text)
|
|
453
|
+
.join('\n')
|
|
454
|
+
.trim()
|
|
455
|
+
if (text === '' || text.startsWith('[Room]') || text.startsWith('[Fleet Room')) return
|
|
456
|
+
const members = rooms.status(session.id).members
|
|
457
|
+
const hostMemberId = `host:${session.id}`
|
|
458
|
+
const participants = members.filter((member) => member.role !== 'host')
|
|
459
|
+
const mentioned = members.filter((member) => member.name && text.includes('@' + member.name))
|
|
460
|
+
const recipients = mentioned.length > 0
|
|
461
|
+
? mentioned.map((member) => member.memberId)
|
|
462
|
+
: participants.map((member) => member.memberId)
|
|
463
|
+
if (recipients.length === 0) return
|
|
464
|
+
let body = text
|
|
465
|
+
for (const member of mentioned) body = body.split('@' + member.name).join('')
|
|
466
|
+
body = body.trim() || '(点名)'
|
|
467
|
+
void rooms.send({
|
|
468
|
+
roomId: session.id,
|
|
469
|
+
from: { kind: 'user' },
|
|
470
|
+
to: recipients,
|
|
471
|
+
text: body,
|
|
472
|
+
skipWakeFor: [hostMemberId],
|
|
473
|
+
}).catch((error) => {
|
|
474
|
+
logger.warn?.(`[device-fleet] cannot route room message: ${String(error?.message ?? error)}`)
|
|
475
|
+
})
|
|
476
|
+
} catch (error) {
|
|
477
|
+
logger.warn?.(`[device-fleet] room message routing failed: ${String(error?.message ?? error)}`)
|
|
478
|
+
}
|
|
479
|
+
}),
|
|
480
|
+
)
|
|
481
|
+
|
|
438
482
|
node.onRequest(async (request) => {
|
|
439
483
|
if (!host) throw Object.assign(new Error('this device cannot serve direct calls (no execution slot)'), { code: 'unsupported' })
|
|
440
484
|
return host.handleRequest(request)
|