dsh-bots 0.2.11 → 0.2.13
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 +236 -6
- 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,13 @@
|
|
|
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}
|
|
326
|
+
.dbs-setLabel{font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);margin:8px 0 4px}
|
|
320
327
|
.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
328
|
.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
329
|
.dbs-scrollBody{scrollbar-gutter:stable;flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden auto}
|
|
@@ -450,7 +457,18 @@
|
|
|
450
457
|
'action.saving': '保存中…',
|
|
451
458
|
'action.stop': '停止生成',
|
|
452
459
|
'chat.stop.noop': '当前没有进行中的生成',
|
|
460
|
+
'media.openFailed': '打开失败,文件可能已移动或被删除',
|
|
453
461
|
'chat.members.manage': '管理成员',
|
|
462
|
+
'chat.settings': '设置',
|
|
463
|
+
'chat.settings.title': '会话设置',
|
|
464
|
+
'chat.settings.name': '名称',
|
|
465
|
+
'chat.settings.desc': '简介',
|
|
466
|
+
'chat.settings.workspace': '工作区隔离',
|
|
467
|
+
'chat.settings.root': '工作区根目录',
|
|
468
|
+
'chat.settings.rootPh': '留空 = 不启用',
|
|
469
|
+
'chat.settings.paths': '额外可写路径(逗号分隔)',
|
|
470
|
+
'chat.settings.pathsPh': '/tmp/xxx, /Users/you/shared',
|
|
471
|
+
'chat.settings.hint': '写入被沙盒限制在工作区(含额外路径)内,读取不受限;同 slug 的 Bot 共享同一目录。',
|
|
454
472
|
'modal.members.title': '管理群成员',
|
|
455
473
|
'modal.members.hint': '勾选的 Bot 为群成员;保存后立即生效(可随时再改)。',
|
|
456
474
|
'action.delete': '删除',
|
|
@@ -551,7 +569,18 @@
|
|
|
551
569
|
'action.saving': 'Saving…',
|
|
552
570
|
'action.stop': 'Stop generating',
|
|
553
571
|
'chat.stop.noop': 'No generation in progress',
|
|
572
|
+
'media.openFailed': 'Open failed — the file may have moved or been deleted',
|
|
554
573
|
'chat.members.manage': 'Manage members',
|
|
574
|
+
'chat.settings': 'Settings',
|
|
575
|
+
'chat.settings.title': 'Session settings',
|
|
576
|
+
'chat.settings.name': 'Name',
|
|
577
|
+
'chat.settings.desc': 'Description',
|
|
578
|
+
'chat.settings.workspace': 'Workspace jail',
|
|
579
|
+
'chat.settings.root': 'Workspace root',
|
|
580
|
+
'chat.settings.rootPh': 'Empty = disabled',
|
|
581
|
+
'chat.settings.paths': 'Extra writable paths (comma-separated)',
|
|
582
|
+
'chat.settings.pathsPh': '/tmp/xxx, /Users/you/shared',
|
|
583
|
+
'chat.settings.hint': 'Writes are sandboxed to the workspace (plus extra paths); reads stay unrestricted. Bots sharing a slug share one directory.',
|
|
555
584
|
'modal.members.title': 'Manage group members',
|
|
556
585
|
'modal.members.hint': 'Checked bots are members; changes apply immediately on save (editable again anytime).',
|
|
557
586
|
'action.delete': 'Delete',
|
|
@@ -754,6 +783,7 @@
|
|
|
754
783
|
confirmDelete: null,
|
|
755
784
|
/** Manage-members dialog: group agentId | null (system modal). */
|
|
756
785
|
manageMembers: null,
|
|
786
|
+
settingsAgentId: null,
|
|
757
787
|
/** Bumped on a language switch so module-scope `t` output re-renders. */
|
|
758
788
|
localeRev: 0,
|
|
759
789
|
};
|
|
@@ -1292,6 +1322,93 @@
|
|
|
1292
1322
|
const p2 = (n) => String(n).padStart(2, '0');
|
|
1293
1323
|
return e('span', { className: 'dbs-msgTime', title: stampOf(ms) }, d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate()) + ' ' + clockOf(ms));
|
|
1294
1324
|
}
|
|
1325
|
+
// ---- Media references: bot-written local files in message text. ----
|
|
1326
|
+
// Images render inline (host readImage → data URL, session-cached);
|
|
1327
|
+
// documents/media render as a click-to-open chip (host openFile). The
|
|
1328
|
+
// host enforces the real boundary — gateway data dir only, ≤8MB, image
|
|
1329
|
+
// extension allowlist — so the client regexes are UX filters, not trust.
|
|
1330
|
+
const MEDIA_IMG_RE = /\.(png|jpe?g|gif|webp|bmp|svg)$/i;
|
|
1331
|
+
const MEDIA_FILE_RE = /\.(pdf|mp3|wav|m4a|mp4|mov|zip|csv|xlsx|docx|pptx|md|txt|json)$/i;
|
|
1332
|
+
const MEDIA_PATH_RE = /\/[^\s,。;、!?:;""''()【】《》<>"'`\\|]+/g;
|
|
1333
|
+
const MEDIA_MAX_REFS = 6;
|
|
1334
|
+
const mediaUrlCache = new Map();
|
|
1335
|
+
function mediaPathsOf(text) {
|
|
1336
|
+
if (typeof text !== 'string' || text.indexOf('/') === -1)
|
|
1337
|
+
return [];
|
|
1338
|
+
const out = [];
|
|
1339
|
+
for (const m of text.matchAll(MEDIA_PATH_RE)) {
|
|
1340
|
+
const p = m[0].replace(/[.,;:!?、。,;:!?))】\]>}]+$/, '');
|
|
1341
|
+
if (p.length < 4 || p.startsWith('//'))
|
|
1342
|
+
continue; // //… = URL fragment
|
|
1343
|
+
if (!MEDIA_IMG_RE.test(p) && !MEDIA_FILE_RE.test(p))
|
|
1344
|
+
continue;
|
|
1345
|
+
if (out.indexOf(p) !== -1)
|
|
1346
|
+
continue;
|
|
1347
|
+
out.push(p);
|
|
1348
|
+
if (out.length >= MEDIA_MAX_REFS)
|
|
1349
|
+
break;
|
|
1350
|
+
}
|
|
1351
|
+
return out;
|
|
1352
|
+
}
|
|
1353
|
+
function FileChip(p) {
|
|
1354
|
+
const [failed, setFailed] = React.useState(false);
|
|
1355
|
+
const base = p.path.slice(p.path.lastIndexOf('/') + 1) || p.path;
|
|
1356
|
+
const open = () => { botsCall('openFile', { path: p.path }).catch(() => setFailed(true)); };
|
|
1357
|
+
return e('span', {
|
|
1358
|
+
className: 'dbs-fileChip', title: p.path, role: 'button', tabIndex: 0,
|
|
1359
|
+
onClick: open,
|
|
1360
|
+
onKeyDown: (ev) => { if (ev.key === 'Enter' || ev.key === ' ') {
|
|
1361
|
+
ev.preventDefault();
|
|
1362
|
+
open();
|
|
1363
|
+
} },
|
|
1364
|
+
}, Ico('IconFolderClose16', { size: 12 }), e('span', { className: 'dbs-fileChipName' }, base), failed === true ? e('span', { className: 'dbs-meta' }, t('media.openFailed')) : null);
|
|
1365
|
+
}
|
|
1366
|
+
function MediaImage(p) {
|
|
1367
|
+
const [url, setUrl] = React.useState(mediaUrlCache.get(p.path) ?? null);
|
|
1368
|
+
const [err, setErr] = React.useState(false);
|
|
1369
|
+
React.useEffect(() => {
|
|
1370
|
+
if (mediaUrlCache.has(p.path))
|
|
1371
|
+
return;
|
|
1372
|
+
let alive = true;
|
|
1373
|
+
botsCall('readImage', { path: p.path })
|
|
1374
|
+
.then((r) => {
|
|
1375
|
+
const u = 'data:' + r.mime + ';base64,' + r.dataBase64;
|
|
1376
|
+
mediaUrlCache.set(p.path, u);
|
|
1377
|
+
if (alive)
|
|
1378
|
+
setUrl(u);
|
|
1379
|
+
})
|
|
1380
|
+
.catch(() => { if (alive)
|
|
1381
|
+
setErr(true); });
|
|
1382
|
+
return () => { alive = false; };
|
|
1383
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1384
|
+
}, [p.path]);
|
|
1385
|
+
if (url !== null) {
|
|
1386
|
+
return e('img', {
|
|
1387
|
+
className: 'dbs-mediaImg', src: url, alt: p.path, title: p.path,
|
|
1388
|
+
onClick: () => { botsCall('openFile', { path: p.path }).catch(() => undefined); },
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
if (err === true)
|
|
1392
|
+
return e(FileChip, { path: p.path });
|
|
1393
|
+
return e('span', { className: 'dbs-mediaLoading' }, Ico('IconLoadingOutline16', { size: 14 }));
|
|
1394
|
+
}
|
|
1395
|
+
function MediaRefs(p) {
|
|
1396
|
+
const refs = mediaPathsOf(p.text);
|
|
1397
|
+
if (refs.length === 0)
|
|
1398
|
+
return null;
|
|
1399
|
+
const imgs = refs.filter((r) => MEDIA_IMG_RE.test(r));
|
|
1400
|
+
const files = p.files === true ? refs.filter((r) => !MEDIA_IMG_RE.test(r)) : [];
|
|
1401
|
+
if (imgs.length === 0 && files.length === 0)
|
|
1402
|
+
return null;
|
|
1403
|
+
return e('div', { className: 'dbs-mediaRefs' }, imgs.map((r) => e(MediaImage, { key: r, path: r })), files.map((r) => e(FileChip, { key: r, path: r })));
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Settings glyph — three sliders, drawn inline (the primitives package
|
|
1407
|
+
* exposes no verified gear export; same policy as SendUpIcon).
|
|
1408
|
+
*/
|
|
1409
|
+
function SettingsGlyph() {
|
|
1410
|
+
return e('svg', { viewBox: '0 0 16 16', width: '16', height: '16', 'aria-hidden': true }, e('g', { stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round' }, e('line', { x1: 2, y1: 4, x2: 14, y2: 4 }), e('line', { x1: 2, y1: 8, x2: 14, y2: 8 }), e('line', { x1: 2, y1: 12, x2: 14, y2: 12 })), e('g', { fill: 'currentColor' }, e('circle', { cx: 10, cy: 4, r: 2 }), e('circle', { cx: 5.5, cy: 8, r: 2 }), e('circle', { cx: 11.5, cy: 12, r: 2 })));
|
|
1411
|
+
}
|
|
1295
1412
|
function ToolCard(p) {
|
|
1296
1413
|
const [open, setOpen] = React.useState(false);
|
|
1297
1414
|
const en = p.entry;
|
|
@@ -1311,12 +1428,12 @@
|
|
|
1311
1428
|
} },
|
|
1312
1429
|
}, StateDot !== null
|
|
1313
1430
|
? 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);
|
|
1431
|
+
: 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
1432
|
}
|
|
1316
1433
|
function Entry(p) {
|
|
1317
1434
|
const en = p.entry;
|
|
1318
1435
|
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 }));
|
|
1436
|
+
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
1437
|
}
|
|
1321
1438
|
if (en.display === 'tool')
|
|
1322
1439
|
return e(ToolCard, { entry: en });
|
|
@@ -1332,7 +1449,7 @@
|
|
|
1332
1449
|
// stands above; only the reply clock rides along, right-aligned and
|
|
1333
1450
|
// quiet — the way native IMs chain quick follow-ups under one name.
|
|
1334
1451
|
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 })));
|
|
1452
|
+
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
1453
|
}
|
|
1337
1454
|
// Bot message: avatar + prominent per-author name + full-datetime in
|
|
1338
1455
|
// one header row, so multi-member rooms read at a glance.
|
|
@@ -1349,7 +1466,7 @@
|
|
|
1349
1466
|
const authorColor = 'hsl(' + String(hueOf(avAgent.id)) + ' 55% 45%)';
|
|
1350
1467
|
return e('div', { className: 'dbs-botRow' }, e('div', { className: 'dbs-author' }, e(Avatar, { agent: avAgent, size: 18 }), displayName != null && displayName !== ''
|
|
1351
1468
|
? 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));
|
|
1469
|
+
: 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
1470
|
}
|
|
1354
1471
|
function ChatView(p) {
|
|
1355
1472
|
const s = useStore();
|
|
@@ -1607,12 +1724,16 @@
|
|
|
1607
1724
|
variant: 'ghost', size: 'sm', title: t('action.close'), 'aria-label': t('action.close'),
|
|
1608
1725
|
icon: Ico('IconCloseOutline16', { size: 16 }),
|
|
1609
1726
|
onClick: () => patch({ chatAgentId: null }),
|
|
1610
|
-
}), agent !== null ? e(Avatar, { agent, size: 24 }) : null, e('span', { className: 'dbs-chatbarName' }, agent?.name ?? t('chat.loading')), e('span', { className: 'dbs-meta' }, isGroup ? t('chat.group', { n: memberNames.length }) : t('chat.single')), isGroup
|
|
1727
|
+
}), agent !== null ? e(Avatar, { agent, size: 24 }) : null, e('span', { className: 'dbs-chatbarName' }, agent?.name ?? t('chat.loading')), e('span', { className: 'dbs-meta' }, isGroup ? t('chat.group', { n: memberNames.length }) : t('chat.single')), e('span', { style: { flex: 1 } }), isGroup
|
|
1611
1728
|
? e(Button, {
|
|
1612
1729
|
variant: 'ghost', size: 'sm', title: t('chat.members.manage'), 'aria-label': t('chat.members.manage'),
|
|
1613
1730
|
onClick: () => patch({ manageMembers: p.agentId }),
|
|
1614
1731
|
}, t('chat.members.manage'))
|
|
1615
|
-
: null, e('
|
|
1732
|
+
: null, e('button', {
|
|
1733
|
+
className: 'dbs-railBtn', type: 'button',
|
|
1734
|
+
title: t('chat.settings'), 'aria-label': t('chat.settings'),
|
|
1735
|
+
onClick: () => patch({ settingsAgentId: p.agentId }),
|
|
1736
|
+
}, e(SettingsGlyph, null))), error !== null
|
|
1616
1737
|
? e('div', { className: 'dbs-error', onClick: () => setError(null) }, error)
|
|
1617
1738
|
: null, e('div', { className: 'dbs-scrollArea' }, e('div', { className: 'dbs-scrollBody', ref: scrollRef, onScroll: onScrollBody }, e('div', { className: 'dbs-scroll' }, e('div', { className: 'dbs-column' }, entries === null
|
|
1618
1739
|
? e('div', { className: 'dbs-skeleton' }, e('div', { className: 'dbs-skelRow', style: { width: '42%' } }), e('div', { className: 'dbs-skelRow', style: { width: '76%' } }), e('div', { className: 'dbs-skelRow', style: { width: '58%' } }))
|
|
@@ -2073,6 +2194,113 @@
|
|
|
2073
2194
|
onClick: () => void submit(),
|
|
2074
2195
|
}, working ? t('action.saving') : t('action.save')))));
|
|
2075
2196
|
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Per-session settings: name (+ description) for every chat; singles
|
|
2199
|
+
* additionally get the §14 workspace-jail editor (root + extra writable
|
|
2200
|
+
* paths). updateAgent merges only the name/description/title/avatar
|
|
2201
|
+
* fields engine-side, so saving cannot clobber the rest of a profile;
|
|
2202
|
+
* an empty root removes the jail (workspaceRoot: null).
|
|
2203
|
+
*/
|
|
2204
|
+
function AgentSettingsModal() {
|
|
2205
|
+
const s = useStore();
|
|
2206
|
+
const agent = agentById(s.settingsAgentId);
|
|
2207
|
+
const isGroup = agent?.isGroup === true;
|
|
2208
|
+
const [name, setName] = React.useState('');
|
|
2209
|
+
const [desc, setDesc] = React.useState('');
|
|
2210
|
+
const [root, setRoot] = React.useState('');
|
|
2211
|
+
const [paths, setPaths] = React.useState('');
|
|
2212
|
+
const [wsLoaded, setWsLoaded] = React.useState(false);
|
|
2213
|
+
const [working, setWorking] = React.useState(false);
|
|
2214
|
+
const [err, setErr] = React.useState(null);
|
|
2215
|
+
React.useEffect(() => {
|
|
2216
|
+
if (agent === null || agent === undefined)
|
|
2217
|
+
return;
|
|
2218
|
+
setName(String(agent.name ?? ''));
|
|
2219
|
+
setDesc(String(agent.description ?? ''));
|
|
2220
|
+
if (agent.isGroup === true) {
|
|
2221
|
+
setWsLoaded(true);
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
let alive = true;
|
|
2225
|
+
botsCall('workspaceGet', { agentId: agent.id })
|
|
2226
|
+
.then((c) => {
|
|
2227
|
+
if (alive === false)
|
|
2228
|
+
return;
|
|
2229
|
+
setRoot(c.workspaceRoot ?? '');
|
|
2230
|
+
setPaths((c.allowPaths ?? []).join(', '));
|
|
2231
|
+
setWsLoaded(true);
|
|
2232
|
+
})
|
|
2233
|
+
.catch(() => { if (alive)
|
|
2234
|
+
setWsLoaded(true); });
|
|
2235
|
+
return () => { alive = false; };
|
|
2236
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2237
|
+
}, [s.settingsAgentId]);
|
|
2238
|
+
if (agent === null || agent === undefined)
|
|
2239
|
+
return null;
|
|
2240
|
+
const valid = name.trim() !== '';
|
|
2241
|
+
async function save() {
|
|
2242
|
+
if (working || valid === false)
|
|
2243
|
+
return;
|
|
2244
|
+
setWorking(true);
|
|
2245
|
+
setErr(null);
|
|
2246
|
+
try {
|
|
2247
|
+
await botsCall('update', { id: agent.id, profile: { name: name.trim(), description: desc.trim() } });
|
|
2248
|
+
if (agent.isGroup !== true && wsLoaded === true) {
|
|
2249
|
+
const list = paths.split(/[,,]/).map((x) => x.trim()).filter((x) => x !== '');
|
|
2250
|
+
await botsCall('workspaceSet', {
|
|
2251
|
+
agentId: agent.id,
|
|
2252
|
+
workspaceRoot: root.trim() === '' ? null : root.trim(),
|
|
2253
|
+
allowPaths: list,
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
patch({ settingsAgentId: null });
|
|
2257
|
+
await refreshAgents();
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
catch (e2) {
|
|
2261
|
+
setErr(String(e2?.message ?? e2));
|
|
2262
|
+
}
|
|
2263
|
+
setWorking(false);
|
|
2264
|
+
}
|
|
2265
|
+
return e('div', {
|
|
2266
|
+
className: 'dbs-modalBackdrop',
|
|
2267
|
+
onClick: () => { if (working === false)
|
|
2268
|
+
patch({ settingsAgentId: null }); },
|
|
2269
|
+
}, e('div', {
|
|
2270
|
+
className: 'dbs-modalCard', role: 'dialog', 'aria-modal': true,
|
|
2271
|
+
'aria-label': t('chat.settings.title'),
|
|
2272
|
+
onClick: (ev) => { ev.stopPropagation(); },
|
|
2273
|
+
}, e('div', { className: 'dbs-modalTitleRow' }, e('span', { className: 'dbs-modalTitle' }, t('chat.settings.title')), e(Button, {
|
|
2274
|
+
variant: 'ghost', size: 'sm', title: t('action.close'), 'aria-label': t('action.close'),
|
|
2275
|
+
disabled: working,
|
|
2276
|
+
icon: Ico('IconCloseOutline16', { size: 16 }),
|
|
2277
|
+
onClick: () => patch({ settingsAgentId: null }),
|
|
2278
|
+
})), e('div', { className: 'dbs-modalBody' }, e('div', { className: 'dbs-setLabel' }, t('chat.settings.name')), e(Input, {
|
|
2279
|
+
value: name, autoFocus: true, disabled: working,
|
|
2280
|
+
onChange: (ev) => setName(ev.target.value),
|
|
2281
|
+
}), isGroup === false
|
|
2282
|
+
? e('div', null, e('div', { className: 'dbs-setLabel' }, t('chat.settings.desc')), e(Input, {
|
|
2283
|
+
value: desc, disabled: working,
|
|
2284
|
+
onChange: (ev) => setDesc(ev.target.value),
|
|
2285
|
+
}), e('div', { className: 'dbs-setLabel', style: { marginTop: 12, fontWeight: 600 } }, t('chat.settings.workspace')), e('div', { className: 'dbs-setLabel' }, t('chat.settings.root')), e(Input, {
|
|
2286
|
+
value: root, disabled: working || wsLoaded === false,
|
|
2287
|
+
placeholder: t('chat.settings.rootPh'),
|
|
2288
|
+
onChange: (ev) => setRoot(ev.target.value),
|
|
2289
|
+
}), e('div', { className: 'dbs-setLabel' }, t('chat.settings.paths')), e(Input, {
|
|
2290
|
+
value: paths, disabled: working || wsLoaded === false,
|
|
2291
|
+
placeholder: t('chat.settings.pathsPh'),
|
|
2292
|
+
onChange: (ev) => setPaths(ev.target.value),
|
|
2293
|
+
}), e('div', { className: 'dbs-meta', style: { marginTop: 8 } }, t('chat.settings.hint')))
|
|
2294
|
+
: null, err !== null
|
|
2295
|
+
? e('div', { className: 'dbs-error', onClick: () => { setErr(null); } }, err)
|
|
2296
|
+
: null), e('div', { className: 'dbs-modalFooter' }, e(Button, {
|
|
2297
|
+
variant: 'ghost', size: 'sm', disabled: working,
|
|
2298
|
+
onClick: () => patch({ settingsAgentId: null }),
|
|
2299
|
+
}, t('action.cancel')), e(Button, {
|
|
2300
|
+
variant: 'primary', size: 'sm', disabled: valid === false || working,
|
|
2301
|
+
onClick: () => void save(),
|
|
2302
|
+
}, working === true ? t('action.saving') : t('action.save')))));
|
|
2303
|
+
}
|
|
2076
2304
|
/** Delete confirmation modal: same system-dialog chrome as CreateModal. */
|
|
2077
2305
|
function ConfirmDeleteModal() {
|
|
2078
2306
|
const s = useStore();
|
|
@@ -2151,6 +2379,8 @@
|
|
|
2151
2379
|
layers.push(e(CreateModal, { key: 'create' }));
|
|
2152
2380
|
if (s.manageMembers !== null)
|
|
2153
2381
|
layers.push(e(ManageMembersModal, { key: 'manage-members' }));
|
|
2382
|
+
if (s.settingsAgentId !== null)
|
|
2383
|
+
layers.push(e(AgentSettingsModal, { key: 'agent-settings' }));
|
|
2154
2384
|
if (s.confirmDelete !== null)
|
|
2155
2385
|
layers.push(e(ConfirmDeleteModal, { key: 'confirm-delete' }));
|
|
2156
2386
|
return layers.length === 0 ? null : e('div', null, layers);
|
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.13",
|
|
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",
|