dsh-bots 0.2.11 → 0.2.12
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/lib/client.js +92 -4
- package/lib/index.js +49 -3
- package/lib/types/index.d.ts +21 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -317,6 +317,12 @@
|
|
|
317
317
|
.dbs-error{margin:4px 8px;padding:5px 9px;border-radius:8px;font-size:12px;line-height:18px;cursor:pointer;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-state-error-primary,#f85149)}
|
|
318
318
|
|
|
319
319
|
.dbs-chatview{position:absolute;top:0;bottom:0;pointer-events:auto;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base);font-family:var(--dsw-font-family,inherit);z-index:2;--dsh-chat-content-width:748px;--dsh-composer-card-max-width:calc(var(--dsh-chat-content-width) + 32px);--dsh-composer-side-clearance:16px;--dsh-composer-dock-inset:8px;--dsh-composer-text-max-height:336px;min-width:0}
|
|
320
|
+
.dbs-mediaRefs{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;align-items:center}
|
|
321
|
+
.dbs-mediaImg{max-width:280px;max-height:210px;border-radius:10px;cursor:zoom-in;display:block;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08))}
|
|
322
|
+
.dbs-mediaLoading{width:28px;height:20px;display:inline-flex;align-items:center;color:var(--dsw-alias-label-tertiary)}
|
|
323
|
+
.dbs-fileChip{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:999px;background:var(--dsw-alias-interactive-bg-hover);font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);cursor:pointer;max-width:100%;user-select:none}
|
|
324
|
+
.dbs-fileChip:hover{color:var(--dsw-alias-label-primary)}
|
|
325
|
+
.dbs-fileChipName{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
320
326
|
.dbs-chatbar{flex:none;display:flex;align-items:center;gap:8px;height:44px;padding:0 12px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08))}
|
|
321
327
|
.dbs-chatbarName{font-size:14px;line-height:20px;font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
|
322
328
|
.dbs-scrollBody{scrollbar-gutter:stable;flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden auto}
|
|
@@ -450,6 +456,7 @@
|
|
|
450
456
|
'action.saving': '保存中…',
|
|
451
457
|
'action.stop': '停止生成',
|
|
452
458
|
'chat.stop.noop': '当前没有进行中的生成',
|
|
459
|
+
'media.openFailed': '打开失败,文件可能已移动或被删除',
|
|
453
460
|
'chat.members.manage': '管理成员',
|
|
454
461
|
'modal.members.title': '管理群成员',
|
|
455
462
|
'modal.members.hint': '勾选的 Bot 为群成员;保存后立即生效(可随时再改)。',
|
|
@@ -551,6 +558,7 @@
|
|
|
551
558
|
'action.saving': 'Saving…',
|
|
552
559
|
'action.stop': 'Stop generating',
|
|
553
560
|
'chat.stop.noop': 'No generation in progress',
|
|
561
|
+
'media.openFailed': 'Open failed — the file may have moved or been deleted',
|
|
554
562
|
'chat.members.manage': 'Manage members',
|
|
555
563
|
'modal.members.title': 'Manage group members',
|
|
556
564
|
'modal.members.hint': 'Checked bots are members; changes apply immediately on save (editable again anytime).',
|
|
@@ -1292,6 +1300,86 @@
|
|
|
1292
1300
|
const p2 = (n) => String(n).padStart(2, '0');
|
|
1293
1301
|
return e('span', { className: 'dbs-msgTime', title: stampOf(ms) }, d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate()) + ' ' + clockOf(ms));
|
|
1294
1302
|
}
|
|
1303
|
+
// ---- Media references: bot-written local files in message text. ----
|
|
1304
|
+
// Images render inline (host readImage → data URL, session-cached);
|
|
1305
|
+
// documents/media render as a click-to-open chip (host openFile). The
|
|
1306
|
+
// host enforces the real boundary — gateway data dir only, ≤8MB, image
|
|
1307
|
+
// extension allowlist — so the client regexes are UX filters, not trust.
|
|
1308
|
+
const MEDIA_IMG_RE = /\.(png|jpe?g|gif|webp|bmp|svg)$/i;
|
|
1309
|
+
const MEDIA_FILE_RE = /\.(pdf|mp3|wav|m4a|mp4|mov|zip|csv|xlsx|docx|pptx|md|txt|json)$/i;
|
|
1310
|
+
const MEDIA_PATH_RE = /\/[^\s,。;、!?:;""''()【】《》<>"'`\\|]+/g;
|
|
1311
|
+
const MEDIA_MAX_REFS = 6;
|
|
1312
|
+
const mediaUrlCache = new Map();
|
|
1313
|
+
function mediaPathsOf(text) {
|
|
1314
|
+
if (typeof text !== 'string' || text.indexOf('/') === -1)
|
|
1315
|
+
return [];
|
|
1316
|
+
const out = [];
|
|
1317
|
+
for (const m of text.matchAll(MEDIA_PATH_RE)) {
|
|
1318
|
+
const p = m[0].replace(/[.,;:!?、。,;:!?))】\]>}]+$/, '');
|
|
1319
|
+
if (p.length < 4 || p.startsWith('//'))
|
|
1320
|
+
continue; // //… = URL fragment
|
|
1321
|
+
if (!MEDIA_IMG_RE.test(p) && !MEDIA_FILE_RE.test(p))
|
|
1322
|
+
continue;
|
|
1323
|
+
if (out.indexOf(p) !== -1)
|
|
1324
|
+
continue;
|
|
1325
|
+
out.push(p);
|
|
1326
|
+
if (out.length >= MEDIA_MAX_REFS)
|
|
1327
|
+
break;
|
|
1328
|
+
}
|
|
1329
|
+
return out;
|
|
1330
|
+
}
|
|
1331
|
+
function FileChip(p) {
|
|
1332
|
+
const [failed, setFailed] = React.useState(false);
|
|
1333
|
+
const base = p.path.slice(p.path.lastIndexOf('/') + 1) || p.path;
|
|
1334
|
+
const open = () => { botsCall('openFile', { path: p.path }).catch(() => setFailed(true)); };
|
|
1335
|
+
return e('span', {
|
|
1336
|
+
className: 'dbs-fileChip', title: p.path, role: 'button', tabIndex: 0,
|
|
1337
|
+
onClick: open,
|
|
1338
|
+
onKeyDown: (ev) => { if (ev.key === 'Enter' || ev.key === ' ') {
|
|
1339
|
+
ev.preventDefault();
|
|
1340
|
+
open();
|
|
1341
|
+
} },
|
|
1342
|
+
}, Ico('IconFolderClose16', { size: 12 }), e('span', { className: 'dbs-fileChipName' }, base), failed === true ? e('span', { className: 'dbs-meta' }, t('media.openFailed')) : null);
|
|
1343
|
+
}
|
|
1344
|
+
function MediaImage(p) {
|
|
1345
|
+
const [url, setUrl] = React.useState(mediaUrlCache.get(p.path) ?? null);
|
|
1346
|
+
const [err, setErr] = React.useState(false);
|
|
1347
|
+
React.useEffect(() => {
|
|
1348
|
+
if (mediaUrlCache.has(p.path))
|
|
1349
|
+
return;
|
|
1350
|
+
let alive = true;
|
|
1351
|
+
botsCall('readImage', { path: p.path })
|
|
1352
|
+
.then((r) => {
|
|
1353
|
+
const u = 'data:' + r.mime + ';base64,' + r.dataBase64;
|
|
1354
|
+
mediaUrlCache.set(p.path, u);
|
|
1355
|
+
if (alive)
|
|
1356
|
+
setUrl(u);
|
|
1357
|
+
})
|
|
1358
|
+
.catch(() => { if (alive)
|
|
1359
|
+
setErr(true); });
|
|
1360
|
+
return () => { alive = false; };
|
|
1361
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1362
|
+
}, [p.path]);
|
|
1363
|
+
if (url !== null) {
|
|
1364
|
+
return e('img', {
|
|
1365
|
+
className: 'dbs-mediaImg', src: url, alt: p.path, title: p.path,
|
|
1366
|
+
onClick: () => { botsCall('openFile', { path: p.path }).catch(() => undefined); },
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
if (err === true)
|
|
1370
|
+
return e(FileChip, { path: p.path });
|
|
1371
|
+
return e('span', { className: 'dbs-mediaLoading' }, Ico('IconLoadingOutline16', { size: 14 }));
|
|
1372
|
+
}
|
|
1373
|
+
function MediaRefs(p) {
|
|
1374
|
+
const refs = mediaPathsOf(p.text);
|
|
1375
|
+
if (refs.length === 0)
|
|
1376
|
+
return null;
|
|
1377
|
+
const imgs = refs.filter((r) => MEDIA_IMG_RE.test(r));
|
|
1378
|
+
const files = p.files === true ? refs.filter((r) => !MEDIA_IMG_RE.test(r)) : [];
|
|
1379
|
+
if (imgs.length === 0 && files.length === 0)
|
|
1380
|
+
return null;
|
|
1381
|
+
return e('div', { className: 'dbs-mediaRefs' }, imgs.map((r) => e(MediaImage, { key: r, path: r })), files.map((r) => e(FileChip, { key: r, path: r })));
|
|
1382
|
+
}
|
|
1295
1383
|
function ToolCard(p) {
|
|
1296
1384
|
const [open, setOpen] = React.useState(false);
|
|
1297
1385
|
const en = p.entry;
|
|
@@ -1311,12 +1399,12 @@
|
|
|
1311
1399
|
} },
|
|
1312
1400
|
}, StateDot !== null
|
|
1313
1401
|
? e(StateDot, { state: en.toolStatus === 'running' ? 'ongoing' : en.toolStatus === 'error' ? 'error' : 'done', size: 10 })
|
|
1314
|
-
: e('span', { style: { width: 10, height: 10, borderRadius: 999, background: tone, display: 'inline-block' } }), e('span', { className: 'dbs-toolName' }, en.toolName ?? t('tool.fallbackName')), e('span', { className: 'dbs-toolTrail' }, en.content !== '' ? e('span', { className: 'dbs-meta' }, open ? t('action.collapse') : t('action.expand')) : null, e(MsgTime, { entry: en }))), open && en.content !== '' ? e('div', { className: 'dbs-toolBody' }, en.content) : null);
|
|
1402
|
+
: e('span', { style: { width: 10, height: 10, borderRadius: 999, background: tone, display: 'inline-block' } }), e('span', { className: 'dbs-toolName' }, en.toolName ?? t('tool.fallbackName')), e('span', { className: 'dbs-toolTrail' }, en.content !== '' ? e('span', { className: 'dbs-meta' }, open ? t('action.collapse') : t('action.expand')) : null, e(MsgTime, { entry: en }))), open && en.content !== '' ? e('div', { className: 'dbs-toolBody' }, en.content, e(MediaRefs, { text: en.content })) : null);
|
|
1315
1403
|
}
|
|
1316
1404
|
function Entry(p) {
|
|
1317
1405
|
const en = p.entry;
|
|
1318
1406
|
if (en.display === 'user') {
|
|
1319
|
-
return e('div', { className: 'dbs-userRow' }, e('div', { className: 'dbs-userStack' }, e('div', { className: 'dbs-bubble' }, e(MessageText, { text: en.content }))), e(MsgTime, { entry: en }));
|
|
1407
|
+
return e('div', { className: 'dbs-userRow' }, e('div', { className: 'dbs-userStack' }, e('div', { className: 'dbs-bubble' }, e(MessageText, { text: en.content }), e(MediaRefs, { text: en.content, files: true }))), e(MsgTime, { entry: en }));
|
|
1320
1408
|
}
|
|
1321
1409
|
if (en.display === 'tool')
|
|
1322
1410
|
return e(ToolCard, { entry: en });
|
|
@@ -1332,7 +1420,7 @@
|
|
|
1332
1420
|
// stands above; only the reply clock rides along, right-aligned and
|
|
1333
1421
|
// quiet — the way native IMs chain quick follow-ups under one name.
|
|
1334
1422
|
if (p.compact === true) {
|
|
1335
|
-
return e('div', { className: 'dbs-botRow', 'data-compact': 'true' }, e('div', { className: 'dbs-mdRow' }, e(MarkdownText, { text: en.content, streaming: en.isStreaming === true }), en.isStreaming === true ? e('span', { className: 'dbs-caret' }) : null), e('div', { className: 'dbs-compactTime' }, e(MsgTime, { entry: en })));
|
|
1423
|
+
return e('div', { className: 'dbs-botRow', 'data-compact': 'true' }, e('div', { className: 'dbs-mdRow' }, e(MarkdownText, { text: en.content, streaming: en.isStreaming === true }), en.isStreaming === true ? e('span', { className: 'dbs-caret' }) : null, en.isStreaming === true ? null : e(MediaRefs, { text: en.content, files: true })), e('div', { className: 'dbs-compactTime' }, e(MsgTime, { entry: en })));
|
|
1336
1424
|
}
|
|
1337
1425
|
// Bot message: avatar + prominent per-author name + full-datetime in
|
|
1338
1426
|
// one header row, so multi-member rooms read at a glance.
|
|
@@ -1349,7 +1437,7 @@
|
|
|
1349
1437
|
const authorColor = 'hsl(' + String(hueOf(avAgent.id)) + ' 55% 45%)';
|
|
1350
1438
|
return e('div', { className: 'dbs-botRow' }, e('div', { className: 'dbs-author' }, e(Avatar, { agent: avAgent, size: 18 }), displayName != null && displayName !== ''
|
|
1351
1439
|
? e('span', { className: 'dbs-authorName', style: { color: authorColor } }, displayName)
|
|
1352
|
-
: null, e(MsgTime, { entry: en })), e('div', { className: 'dbs-mdRow' }, e(MarkdownText, { text: en.content, streaming: en.isStreaming === true }), en.isStreaming === true ? e('span', { className: 'dbs-caret' }) : null));
|
|
1440
|
+
: null, e(MsgTime, { entry: en })), e('div', { className: 'dbs-mdRow' }, e(MarkdownText, { text: en.content, streaming: en.isStreaming === true }), en.isStreaming === true ? e('span', { className: 'dbs-caret' }) : null, en.isStreaming === true ? null : e(MediaRefs, { text: en.content, files: true })));
|
|
1353
1441
|
}
|
|
1354
1442
|
function ChatView(p) {
|
|
1355
1443
|
const s = useStore();
|
package/lib/index.js
CHANGED
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
* @module dsh-bots
|
|
20
20
|
*/
|
|
21
21
|
import { createRequire } from 'node:module';
|
|
22
|
-
import { dirname, join } from 'node:path';
|
|
23
|
-
import { appendFileSync, realpathSync } from 'node:fs';
|
|
22
|
+
import { dirname, extname, join, resolve, sep } from 'node:path';
|
|
23
|
+
import { appendFileSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
24
|
+
import { spawn } from 'node:child_process';
|
|
24
25
|
import { pathToFileURL } from 'node:url';
|
|
25
26
|
import { callGateway, discover, expandHome, nextNonce, normalizeAgents, readDiscovery, trimAgent, trimEntry } from './gateway.js';
|
|
26
27
|
import { GatewaySseClient, SseRingBuffer } from './sse.js';
|
|
@@ -284,6 +285,51 @@ export class BotsRemote extends TypertRemoteService {
|
|
|
284
285
|
});
|
|
285
286
|
return trimAgent(updated?.agent ?? updated);
|
|
286
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Resolve a bot-written path against the media allowlist: only files inside
|
|
290
|
+
* the gateway data dir (agent dirs, box-workspace, swarm blackboard…) may
|
|
291
|
+
* be served or opened — the workbench must never become a disk-wide file
|
|
292
|
+
* reader. Symlinks resolve before the check so escapes fail closed.
|
|
293
|
+
*/
|
|
294
|
+
resolveMediaPath(raw) {
|
|
295
|
+
const p = String(raw ?? '').trim();
|
|
296
|
+
if (p === '')
|
|
297
|
+
throw new Error('path is required');
|
|
298
|
+
const root = realpathSync(expandHome(this.cfg.dataDir));
|
|
299
|
+
let resolved = resolve(expandHome(p));
|
|
300
|
+
try {
|
|
301
|
+
resolved = realpathSync(resolved);
|
|
302
|
+
}
|
|
303
|
+
catch { /* missing → prefix-check the literal path */ }
|
|
304
|
+
if (resolved !== root && !resolved.startsWith(root + sep)) {
|
|
305
|
+
throw new Error('path is outside the gateway data dir: ' + resolved);
|
|
306
|
+
}
|
|
307
|
+
return { resolved, ext: extname(resolved).toLowerCase() };
|
|
308
|
+
}
|
|
309
|
+
/** Inline-preview support: read a bot-written image as base64 for a data URL. */
|
|
310
|
+
async readImage(request) {
|
|
311
|
+
const { resolved, ext } = this.resolveMediaPath(request?.path);
|
|
312
|
+
const mime = {
|
|
313
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
|
|
314
|
+
'.webp': 'image/webp', '.bmp': 'image/bmp', '.svg': 'image/svg+xml',
|
|
315
|
+
}[ext];
|
|
316
|
+
if (mime === undefined)
|
|
317
|
+
throw new Error('not a supported image: ' + ext);
|
|
318
|
+
const stat = statSync(resolved);
|
|
319
|
+
if (!stat.isFile())
|
|
320
|
+
throw new Error('not a file: ' + resolved);
|
|
321
|
+
if (stat.size > 8 * 1024 * 1024)
|
|
322
|
+
throw new Error('image exceeds the 8MB preview cap (' + stat.size + ' bytes)');
|
|
323
|
+
return { mime, dataBase64: readFileSync(resolved).toString('base64'), sizeBytes: stat.size };
|
|
324
|
+
}
|
|
325
|
+
/** Open a bot-written file with the desktop default app (macOS `open`). */
|
|
326
|
+
async openFile(request) {
|
|
327
|
+
const { resolved } = this.resolveMediaPath(request?.path);
|
|
328
|
+
statSync(resolved); // missing → honest error instead of a silent no-op
|
|
329
|
+
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
330
|
+
spawn(cmd, [resolved], { detached: true, stdio: 'ignore' }).unref();
|
|
331
|
+
return { opened: true };
|
|
332
|
+
}
|
|
287
333
|
async update(request) {
|
|
288
334
|
const updated = await callGateway(this.cfg.dataDir, 'updateAgent', {
|
|
289
335
|
id: request?.id,
|
|
@@ -493,7 +539,7 @@ export class BotsRemote extends TypertRemoteService {
|
|
|
493
539
|
}
|
|
494
540
|
for (const m of [
|
|
495
541
|
'gatewayInfo', 'list', 'workspaces', 'sessions',
|
|
496
|
-
'create', 'createGroup', 'setGroupMembers', 'update', 'remove', 'send', 'interrupt', 'transcriptTail', 'markRead', 'diag',
|
|
542
|
+
'create', 'createGroup', 'setGroupMembers', 'update', 'remove', 'send', 'interrupt', 'readImage', 'openFile', 'transcriptTail', 'markRead', 'diag',
|
|
497
543
|
'mcpServers', 'mcpTools', 'mcpAdd', 'mcpRemove', 'mcpRefresh', 'mcpExecute',
|
|
498
544
|
'workspaceList', 'workspaceGet', 'workspaceSet',
|
|
499
545
|
'eventsSince', 'sseState',
|
package/lib/types/index.d.ts
CHANGED
|
@@ -92,6 +92,27 @@ export declare class BotsRemote extends TypertRemoteService {
|
|
|
92
92
|
id?: string;
|
|
93
93
|
memberIds?: string[];
|
|
94
94
|
} | null): Promise<AgentInfo | null>;
|
|
95
|
+
/**
|
|
96
|
+
* Resolve a bot-written path against the media allowlist: only files inside
|
|
97
|
+
* the gateway data dir (agent dirs, box-workspace, swarm blackboard…) may
|
|
98
|
+
* be served or opened — the workbench must never become a disk-wide file
|
|
99
|
+
* reader. Symlinks resolve before the check so escapes fail closed.
|
|
100
|
+
*/
|
|
101
|
+
private resolveMediaPath;
|
|
102
|
+
/** Inline-preview support: read a bot-written image as base64 for a data URL. */
|
|
103
|
+
readImage(request: {
|
|
104
|
+
path?: string;
|
|
105
|
+
} | null): Promise<{
|
|
106
|
+
mime: string;
|
|
107
|
+
dataBase64: string;
|
|
108
|
+
sizeBytes: number;
|
|
109
|
+
}>;
|
|
110
|
+
/** Open a bot-written file with the desktop default app (macOS `open`). */
|
|
111
|
+
openFile(request: {
|
|
112
|
+
path?: string;
|
|
113
|
+
} | null): Promise<{
|
|
114
|
+
opened: boolean;
|
|
115
|
+
}>;
|
|
95
116
|
update(request: {
|
|
96
117
|
id?: string;
|
|
97
118
|
profile?: Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-bots",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Multi-bot workbench for DeepSeek Harness: bridges the sdk-bots orchestration gateway (group chats, single bots) into the dsh web shell with official-styled UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|