dsh-bots 0.2.30 → 0.2.31

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 CHANGED
@@ -343,6 +343,7 @@
343
343
  .dbs-fileChip:hover{color:var(--dsw-alias-label-primary)}
344
344
  .dbs-fileChipName{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
345
345
  .dbs-setLabel{font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);margin:8px 0 4px}
346
+ .dbs-select{height:28px;min-width:200px;padding:0 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer-1,#fff));color:var(--dsw-alias-label-primary);font-size:13px;line-height:28px;outline:none}
346
347
  .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))}
347
348
  .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}
348
349
  .dbs-scrollBody{scrollbar-gutter:stable;flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden auto}
@@ -550,6 +551,15 @@
550
551
  'settings.events.off': '未连接',
551
552
  'settings.entry': '入口:',
552
553
  'settings.entry.value': '左侧边栏「工作区 | Bots」折叠导航',
554
+ 'settings.model.title': '模型配置',
555
+ 'settings.model.current': '当前模型',
556
+ 'settings.model.auto': 'auto(freeroute 默认)',
557
+ 'settings.model.engineDefault': '引擎默认(未设置)',
558
+ 'settings.model.freeroute': 'freeroute 自动路由(默认)',
559
+ 'settings.model.pick': '选择模型',
560
+ 'settings.model.hint': '不选择时默认使用 freeroute 自动路由;freeroute 不可用时使用当前默认模型。选择具体模型会写入账号默认模型,重启引擎后仍生效。',
561
+ 'settings.model.offline': 'freeroute 不可达——已回退当前默认模型',
562
+ 'settings.model.saving': '保存中…',
553
563
  'settings.workspaceRoot': '工作区根目录:',
554
564
  'settings.jail.title': 'Bot 工作区隔离',
555
565
  'settings.jail.summary': '开启后,该 Bot 的每条 Shell 命令被 macOS Seatbelt 包裹:只允许写入自己的工作目录(工作区根/<名字>)与系统临时目录,越界写入被内核拒绝;读取不受限(共享黑板仍可读)。下次对话生效,无需重启。注意:同一名字的多个 Bot 会共享同一目录。',
@@ -668,6 +678,15 @@
668
678
  'settings.events.off': 'not connected',
669
679
  'settings.entry': 'Entry point: ',
670
680
  'settings.entry.value': 'Sidebar “Workspaces | Bots” collapsible nav',
681
+ 'settings.model.title': 'Model',
682
+ 'settings.model.current': 'Current model',
683
+ 'settings.model.auto': 'auto (freeroute default)',
684
+ 'settings.model.engineDefault': 'Engine default (unset)',
685
+ 'settings.model.freeroute': 'freeroute auto routing (default)',
686
+ 'settings.model.pick': 'Pick a model',
687
+ 'settings.model.hint': 'Unselected defaults to freeroute auto routing; when freeroute is unavailable the current default model is used. Picking a model writes the account default model and survives engine restarts.',
688
+ 'settings.model.offline': 'freeroute unreachable — using the current default model',
689
+ 'settings.model.saving': 'Saving…',
671
690
  'settings.workspaceRoot': 'Workspace root: ',
672
691
  'settings.jail.title': 'Bot workspace jail',
673
692
  'settings.jail.summary': 'When enabled, every shell command of that bot is wrapped in a macOS Seatbelt profile: writes are confined to its own workspace directory (workspace root /<name>) and the OS temp dir — out-of-bounds writes are denied by the kernel. Reads stay unrestricted (shared blackboards remain readable). Takes effect on the bot’s next turn, no restart. Note: bots sharing a name share one directory.',
@@ -2118,6 +2137,48 @@
2118
2137
  ? (confirmId === row.agentId ? t('settings.jail.offConfirm') : t('settings.jail.off'))
2119
2138
  : t('settings.jail.on')))), err !== null ? e('div', { className: 'dbs-setrow' }, t('settings.mcp.failed'), err) : null);
2120
2139
  }
2140
+ /** Model configuration card: freeroute availability + the engine's
2141
+ * account-level default model (getHostSettings/setHostSettings). No
2142
+ * selection = freeroute auto; freeroute down = the account default. */
2143
+ function ModelCard() {
2144
+ const [cfg, setCfg] = React.useState(undefined);
2145
+ const [working, setWorking] = React.useState(false);
2146
+ const [err, setErr] = React.useState(null);
2147
+ async function refresh() {
2148
+ setCfg(undefined);
2149
+ setErr(null);
2150
+ try {
2151
+ setCfg(await botsCall('modelConfig', {}));
2152
+ }
2153
+ catch (e2) {
2154
+ setErr(String(e2?.message ?? e2));
2155
+ }
2156
+ }
2157
+ React.useEffect(() => { void refresh(); }, []);
2158
+ const current = cfg?.agentDefaultModel ?? null;
2159
+ const effLabel = current !== null
2160
+ ? current
2161
+ : (cfg?.freerouteReachable === true ? t('settings.model.auto') : t('settings.model.engineDefault'));
2162
+ async function choose(ev) {
2163
+ const v = String(ev.target.value ?? '');
2164
+ setWorking(true);
2165
+ setErr(null);
2166
+ try {
2167
+ await botsCall('setModelConfig', { modelId: v === '' ? null : v });
2168
+ await refresh();
2169
+ }
2170
+ catch (e2) {
2171
+ setErr(String(e2?.message ?? e2));
2172
+ }
2173
+ setWorking(false);
2174
+ }
2175
+ return e('div', { className: 'dbs-setcard' }, e('div', { className: 'dbs-sethead' }, e('span', null, t('settings.model.title')), e('span', { style: { flex: 1 } }), working ? e('span', { className: 'dbs-meta' }, t('settings.model.saving')) : null), e('div', { className: 'dbs-setrow' }, t('settings.model.current'), e('b', null, cfg === undefined ? t('settings.reading') : effLabel)), e('div', { className: 'dbs-setrow' }, t('settings.model.pick'), e('select', {
2176
+ className: 'dbs-select', value: current ?? '', disabled: working || cfg === undefined,
2177
+ onChange: (ev) => void choose(ev),
2178
+ }, e('option', { value: '' }, t('settings.model.freeroute')), (cfg?.models ?? []).map((m) => e('option', { key: m, value: m }, m)))), cfg?.freerouteReachable === false
2179
+ ? e('div', { className: 'dbs-meta', style: { padding: '0 2px' } }, t('settings.model.offline'))
2180
+ : null, e('div', { className: 'dbs-meta', style: { padding: '0 2px' } }, t('settings.model.hint')), err !== null ? e('div', { className: 'dbs-setrow' }, err) : null);
2181
+ }
2121
2182
  function BotsSettings() {
2122
2183
  const [info, setInfo] = React.useState(undefined);
2123
2184
  const [sse, setSse] = React.useState(null);
@@ -2137,7 +2198,7 @@
2137
2198
  const ok = info?.ok === true;
2138
2199
  return e('div', { className: 'dbs-settings' }, e('div', { className: 'dbs-setrow' }, t('settings.summary')), e('div', { className: 'dbs-setcard' }, e('div', { className: 'dbs-sethead' }, StateDot !== null ? e(StateDot, { state: ok ? 'done' : 'failed', size: 10 }) : null, info === undefined ? t('settings.probing') : ok ? t('gateway.online', { port: info.port }) : t('gateway.offline'), e('span', { style: { flex: 1 } }), e(Button, { variant: 'outline', size: 'sm', onClick: () => void refresh() }, t('action.refresh'))), ok ? e('div', null, e('div', { className: 'dbs-setrow' }, t('settings.address'), e('b', null, info.baseUrl)), e('div', { className: 'dbs-setrow' }, t('settings.pid'), e('b', null, String(info.pid)), ' · ' + (info.health?.isBusy === true ? t('settings.busy') : t('settings.idle'))), e('div', { className: 'dbs-setrow' }, t('settings.auth'), e('b', null, info.hasToken === true ? t('settings.auth.token') : t('settings.auth.none')))) : null, info !== undefined && !ok ? e('div', { className: 'dbs-setrow' }, t('settings.reason'), e('b', null, info.reason ?? t('settings.reason.unknown'))) : null, err !== null ? e('div', { className: 'dbs-setrow' }, err) : null), e('div', { className: 'dbs-setcard' }, e('div', { className: 'dbs-setrow' }, t('settings.dataDir'), e('b', null, info?.dataDir ?? t('settings.reading'))), info?.workspaceRoot
2139
2200
  ? e('div', { className: 'dbs-setrow' }, t('settings.workspaceRoot'), e('b', null, info.workspaceRoot))
2140
- : null, e('div', { className: 'dbs-setrow' }, t('settings.events'), e('b', null, sse?.running === true ? t('settings.events.on', { n: sse.buffered ?? 0 }) : t('settings.events.off')), sse?.lastError ? ' · ' + String(sse.lastError) : ''), e('div', { className: 'dbs-setrow' }, t('settings.entry'), e('b', null, t('settings.entry.value')))), info?.ok === true ? e(WorkspaceCard) : null, info?.ok === true ? e(McpCard) : null);
2201
+ : null, e('div', { className: 'dbs-setrow' }, t('settings.events'), e('b', null, sse?.running === true ? t('settings.events.on', { n: sse.buffered ?? 0 }) : t('settings.events.off')), sse?.lastError ? ' · ' + String(sse.lastError) : ''), e('div', { className: 'dbs-setrow' }, t('settings.entry'), e('b', null, t('settings.entry.value')))), info?.ok === true ? e(ModelCard) : null, info?.ok === true ? e(WorkspaceCard) : null, info?.ok === true ? e(McpCard) : null);
2141
2202
  }
2142
2203
  // =========================================================
2143
2204
  // Overlay root: hosts the chat surface only.
package/lib/index.js CHANGED
@@ -38,6 +38,11 @@ const DEFAULT_DATA_DIR = '~/.dsh-bots';
38
38
  /** Where the gateway lived before the 0.2.16 default rename — discovery and
39
39
  * every data-dir consumer fall back to it until the engine migrates. */
40
40
  const LEGACY_DATA_DIR = '~/.sdk-bots';
41
+ /** Local OpenAI-compatible router the sdk-bots engine defaults to (same
42
+ * default as the engine bootstrap). Reachability here decides whether the
43
+ * model picker can offer the router's catalog or must fall back to the
44
+ * account's configured default model. */
45
+ const FREEROUTE_BASE_URL = (process.env.SAND_OPENROUTER_BASE_URL ?? '').trim() || 'http://127.0.0.1:3080/freeroute/v1';
41
46
  /** Append-only diagnostics file, read when a shadow takeover misbehaves. */
42
47
  const DIAG_FILE = 'dsh-bots-diag.jsonl';
43
48
  /** Persisted read markers ("读到哪了") backing the sidebar unread badge. */
@@ -515,6 +520,47 @@ export class BotsRemote extends TypertRemoteService {
515
520
  allowPaths: request?.allowPaths,
516
521
  });
517
522
  }
523
+ /** Model configuration surface: the engine's account-level default model
524
+ * (setHostSettings.agentDefaultModel) plus freeroute availability. The
525
+ * engine keeps no per-bot model — this is the one runtime-effective knob. */
526
+ async modelConfig(request) {
527
+ const settings = await callGateway(this.cfg.dataDir, 'getHostSettings', {});
528
+ const selection = settings?.agentDefaultModel ?? null;
529
+ const probe = await this.probeFreerouteModels();
530
+ return {
531
+ provider: typeof settings?.inferenceProvider === 'string' ? settings.inferenceProvider : null,
532
+ agentDefaultModel: typeof selection?.modelId === 'string' ? selection.modelId : null,
533
+ freerouteReachable: probe.ok,
534
+ models: probe.models,
535
+ };
536
+ }
537
+ async probeFreerouteModels() {
538
+ try {
539
+ const res = await fetch(`${FREEROUTE_BASE_URL}/models`, { signal: AbortSignal.timeout(2500) });
540
+ if (res.ok === false)
541
+ return { ok: false, models: [] };
542
+ const body = await res.json();
543
+ const models = Array.isArray(body?.data)
544
+ ? body.data.map((m) => (typeof m?.id === 'string' ? m.id : '')).filter((id) => id !== '')
545
+ : [];
546
+ return { ok: true, models };
547
+ }
548
+ catch {
549
+ return { ok: false, models: [] };
550
+ }
551
+ }
552
+ /** Unset (null) = freeroute auto routing; a modelId pins the account's
553
+ * default model (the engine's own settings store validates the shape). */
554
+ async setModelConfig(request) {
555
+ const modelId = typeof request?.modelId === 'string' && request.modelId.trim() !== '' ? request.modelId.trim() : null;
556
+ const updated = await callGateway(this.cfg.dataDir, 'setHostSettings', {
557
+ agentDefaultModel: modelId === null
558
+ ? null
559
+ : { modelId, maxMode: true, parameters: [] },
560
+ });
561
+ const selection = updated?.agentDefaultModel ?? null;
562
+ return { agentDefaultModel: typeof selection?.modelId === 'string' ? selection.modelId : null };
563
+ }
518
564
  /**
519
565
  * Append one diagnostic record to `<dataDir>/dsh-bots-diag.jsonl`.
520
566
  *
@@ -560,7 +606,7 @@ for (const m of [
560
606
  'gatewayInfo', 'list', 'workspaces', 'sessions',
561
607
  'create', 'createGroup', 'setGroupMembers', 'update', 'remove', 'send', 'interrupt', 'readImage', 'openFile', 'transcriptTail', 'markRead', 'diag',
562
608
  'mcpServers', 'mcpTools', 'mcpAdd', 'mcpRemove', 'mcpRefresh', 'mcpExecute',
563
- 'workspaceList', 'workspaceGet', 'workspaceSet',
609
+ 'workspaceList', 'workspaceGet', 'workspaceSet', 'modelConfig', 'setModelConfig',
564
610
  'eventsSince', 'sseState',
565
611
  ]) {
566
612
  markRemoteMethod(BotsRemote.prototype, m);
@@ -204,6 +204,23 @@ export declare class BotsRemote extends TypertRemoteService {
204
204
  workspaceRoot?: string | null;
205
205
  allowPaths?: string[];
206
206
  } | null): Promise<import("./workspace.js").AgentWorkspaceConfig>;
207
+ /** Model configuration surface: the engine's account-level default model
208
+ * (setHostSettings.agentDefaultModel) plus freeroute availability. The
209
+ * engine keeps no per-bot model — this is the one runtime-effective knob. */
210
+ modelConfig(request: unknown): Promise<{
211
+ provider: string | null;
212
+ agentDefaultModel: string | null;
213
+ freerouteReachable: boolean;
214
+ models: string[];
215
+ }>;
216
+ private probeFreerouteModels;
217
+ /** Unset (null) = freeroute auto routing; a modelId pins the account's
218
+ * default model (the engine's own settings store validates the shape). */
219
+ setModelConfig(request: {
220
+ modelId?: string | null;
221
+ } | null): Promise<{
222
+ agentDefaultModel: string | null;
223
+ }>;
207
224
  /**
208
225
  * Append one diagnostic record to `<dataDir>/dsh-bots-diag.jsonl`.
209
226
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-bots",
3
- "version": "0.2.30",
3
+ "version": "0.2.31",
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",