@xmanrui/dsh-im 4.21.2 → 4.22.0
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/README.en.md +7 -2
- package/README.md +7 -2
- package/docs/client-integration.md +81 -0
- package/lib/client.js +115 -37
- package/lib/index.js +281 -281
- package/package.json +2 -1
- package/plugin-src/client/i18n.js +1 -0
- package/plugin-src/client/index.js +92 -26
- package/plugin-src/client/panel-error-boundary.js +21 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +3 -1
- package/plugin-src/host/channels/feishu/production.mjs +3 -1
- package/plugin-src/host/channels/qq/production.mjs +3 -1
- package/plugin-src/host/channels/shared/production.mjs +3 -1
- package/plugin-src/host/channels/slack/production.mjs +3 -1
- package/plugin-src/host/channels/wecom/production.mjs +3 -1
- package/plugin-src/host/channels/wecom-app/production.mjs +3 -1
- package/plugin-src/host/channels/weixin/production.mjs +3 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +3 -1
- package/src/channels/shared/bot-workspace-store.mjs +8 -11
- package/src/channels/shared/default-workspace.mjs +28 -0
- package/src/channels/shared/harness-client.mjs +48 -2
- package/src/channels/shared/harness-session-binding.mjs +13 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.22.0",
|
|
4
4
|
"description": "把十一种 IM 渠道和公网 AI Office 接入本机 DeepSeek Harness。 Connect eleven IM channels and a public AI Office to a local DeepSeek Harness.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -114,6 +114,7 @@
|
|
|
114
114
|
"src",
|
|
115
115
|
"PROACTIVE_DELIVERY.md",
|
|
116
116
|
"PROACTIVE_DELIVERY.en.md",
|
|
117
|
+
"docs/client-integration.md",
|
|
117
118
|
"LICENSE",
|
|
118
119
|
"README.md",
|
|
119
120
|
"README.en.md",
|
|
@@ -39,6 +39,7 @@ const EN = Object.freeze({
|
|
|
39
39
|
'macOS Messages 权限': 'macOS Messages permissions',
|
|
40
40
|
'IM机器人': 'IM bots',
|
|
41
41
|
'IM机器人设置': 'IM bot settings',
|
|
42
|
+
'IM 面板加载失败': 'Could not load the IM panel',
|
|
42
43
|
'更多机器人设置': 'More bot settings',
|
|
43
44
|
'机器人设置': 'Bot settings',
|
|
44
45
|
'机器人设置页签': 'Bot settings tabs',
|
|
@@ -74,6 +74,7 @@ import { installImStyles } from './styles.js';
|
|
|
74
74
|
import { installSessionChannelLogos } from './session-channel-logos.js';
|
|
75
75
|
import { UpdatePanel, UPDATE_RPC_CHANNEL } from './update-panel.js';
|
|
76
76
|
import { WorkspaceDirectoryPickerContext } from './workspace-editor.js';
|
|
77
|
+
import { IMPanelErrorBoundary } from './panel-error-boundary.js';
|
|
77
78
|
|
|
78
79
|
export const name = 'im-settings';
|
|
79
80
|
export const inject = ['slots', 'connection', 'locale', 'workspaces'];
|
|
@@ -209,10 +210,15 @@ export function IMSettingsTab({
|
|
|
209
210
|
deliveryRpcCall,
|
|
210
211
|
globalSettingsRpcCall,
|
|
211
212
|
workspaceDirectoryPicker,
|
|
213
|
+
preferredSectionId,
|
|
212
214
|
browserLocation = globalThis.location,
|
|
213
215
|
navigateToRecoveryUrl = replacePageLocation,
|
|
214
216
|
}) {
|
|
215
|
-
const [selected, setSelected] = React.useState(
|
|
217
|
+
const [selected, setSelected] = React.useState(() => (
|
|
218
|
+
preferredSectionId === GLOBAL_SETTINGS_TAB_ID
|
|
219
|
+
|| CHANNELS.some((channel) => channel.id === preferredSectionId)
|
|
220
|
+
? preferredSectionId : 'weixin'
|
|
221
|
+
));
|
|
216
222
|
const [loopbackRecovery, setLoopbackRecovery] = React.useState(null);
|
|
217
223
|
const [runningVersion, setRunningVersion] = React.useState(IM_PLUGIN_VERSION);
|
|
218
224
|
const [deliverySettings, setDeliverySettings] = React.useState(null);
|
|
@@ -471,29 +477,89 @@ export function apply(ctx) {
|
|
|
471
477
|
pickDirectory: () => callWorkspaceDirectoryApi(ctx, 'pickDirectory'),
|
|
472
478
|
});
|
|
473
479
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
480
|
+
const panelDependencies = {
|
|
481
|
+
dingtalkRpcCall,
|
|
482
|
+
discordRpcCall,
|
|
483
|
+
feishuRpcCall,
|
|
484
|
+
qqRpcCall,
|
|
485
|
+
slackRpcCall,
|
|
486
|
+
telegramRpcCall,
|
|
487
|
+
wecomRpcCall,
|
|
488
|
+
wecomAppRpcCall,
|
|
489
|
+
weixinRpcCall,
|
|
490
|
+
whatsappRpcCall,
|
|
491
|
+
imessageRpcCall,
|
|
492
|
+
officeRpcCall,
|
|
493
|
+
updateRpcCall,
|
|
494
|
+
deliveryRpcCall,
|
|
495
|
+
globalSettingsRpcCall,
|
|
496
|
+
workspaceDirectoryPicker,
|
|
497
|
+
};
|
|
498
|
+
const subscribeLocale = (listener) => typeof ctx.on === 'function'
|
|
499
|
+
? ctx.on('locale/change', listener) : () => {};
|
|
500
|
+
const localeSnapshot = () => ctx.locale.getLocale?.()?.active ?? '';
|
|
501
|
+
|
|
502
|
+
// Stable for this plugin lifetime: creating an element must not create a
|
|
503
|
+
// new component type and reset the reader's selected tab or unsaved input.
|
|
504
|
+
function IMPanel({ preferredSectionId }) {
|
|
505
|
+
React.useSyncExternalStore(subscribeLocale, localeSnapshot, localeSnapshot);
|
|
506
|
+
return h(IMPanelErrorBoundary, null,
|
|
507
|
+
h(IMSettingsTab, { ...panelDependencies, preferredSectionId }));
|
|
508
|
+
}
|
|
509
|
+
const buildPanelElement = (props = {}) => h(IMPanel, {
|
|
510
|
+
preferredSectionId: props.preferredSectionId,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
ctx.effect(() => {
|
|
514
|
+
let disposed = false;
|
|
515
|
+
let stopSettings = null;
|
|
516
|
+
let registered = false;
|
|
517
|
+
const setSettingsVisible = (visible) => {
|
|
518
|
+
if (disposed) return;
|
|
519
|
+
if (typeof visible !== 'boolean') throw new TypeError('visible must be a boolean');
|
|
520
|
+
if (visible === (stopSettings !== null)) return;
|
|
521
|
+
if (!visible) {
|
|
522
|
+
const stop = stopSettings;
|
|
523
|
+
stopSettings = null;
|
|
524
|
+
stop();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
// The existing slot controller owns late declarations, withdrawal and
|
|
528
|
+
// re-declaration. Cancelling it also cancels a pending registration.
|
|
529
|
+
stopSettings = ctx.slots.inject('settings.section', () => {
|
|
530
|
+
const unregister = ctx.slots.register({
|
|
531
|
+
name: 'settings.section',
|
|
532
|
+
id: 'xmanrui-dsh-im',
|
|
533
|
+
order: 21,
|
|
534
|
+
label: () => t('IM机器人'),
|
|
535
|
+
locale: IM_LOCALE_NAMESPACE,
|
|
536
|
+
inject: () => panelDependencies,
|
|
537
|
+
}, buildPanelElement);
|
|
538
|
+
registered = true;
|
|
539
|
+
return () => {
|
|
540
|
+
registered = false;
|
|
541
|
+
unregister();
|
|
542
|
+
};
|
|
543
|
+
});
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
// Publish only after the default registration is established, so a
|
|
547
|
+
// consumer hiding it during service discovery cannot be overridden.
|
|
548
|
+
setSettingsVisible(true);
|
|
549
|
+
if (typeof ctx.provide === 'function') {
|
|
550
|
+
ctx.provide('dshImClient', Object.freeze({
|
|
551
|
+
version: 1,
|
|
552
|
+
render: (props) => disposed ? null : buildPanelElement(props),
|
|
553
|
+
setSettingsVisible,
|
|
554
|
+
settingsVisible: () => !disposed && registered,
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
return () => {
|
|
558
|
+
disposed = true;
|
|
559
|
+
const stop = stopSettings;
|
|
560
|
+
stopSettings = null;
|
|
561
|
+
registered = false;
|
|
562
|
+
stop?.();
|
|
563
|
+
};
|
|
564
|
+
}, 'im-settings: client panel service');
|
|
499
565
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { h } from './i18n.js';
|
|
3
|
+
|
|
4
|
+
/** Keep a broken IM subtree local when a shell renders it outside a slot. */
|
|
5
|
+
export class IMPanelErrorBoundary extends React.Component {
|
|
6
|
+
state = { failed: false };
|
|
7
|
+
|
|
8
|
+
static getDerivedStateFromError() {
|
|
9
|
+
return { failed: true };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
render() {
|
|
13
|
+
if (!this.state.failed) return this.props.children;
|
|
14
|
+
return h('section', { className: 'dim-page', role: 'alert' },
|
|
15
|
+
h('p', null, 'IM 面板加载失败'),
|
|
16
|
+
h('button', {
|
|
17
|
+
type: 'button',
|
|
18
|
+
onClick: () => this.setState({ failed: false }),
|
|
19
|
+
}, '重试'));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
createWorkspaceAwareController,
|
|
15
15
|
observeBotWorkspaceRemovals,
|
|
16
16
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
17
|
+
import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
|
|
17
18
|
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
18
19
|
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
19
20
|
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
|
|
@@ -58,7 +59,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
58
59
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
59
60
|
const paths = pluginPaths(config);
|
|
60
61
|
const configStore = await new ConfigStore(paths.config).load();
|
|
61
|
-
const defaultWorkspace =
|
|
62
|
+
const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
|
|
62
63
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
63
64
|
const workspaces = internals.workspaces
|
|
64
65
|
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
@@ -105,6 +106,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
105
106
|
const harness = new Harness({
|
|
106
107
|
...connection,
|
|
107
108
|
workspace: defaultWorkspace,
|
|
109
|
+
ungroupedWorkspace,
|
|
108
110
|
autostart: false,
|
|
109
111
|
dshBin: config.dshBin ?? 'dsh',
|
|
110
112
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
createWorkspaceAwareController,
|
|
27
27
|
observeBotWorkspaceRemovals,
|
|
28
28
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
29
|
+
import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
|
|
29
30
|
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
30
31
|
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
31
32
|
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
|
|
@@ -117,7 +118,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
117
118
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
118
119
|
const paths = pluginPaths(config);
|
|
119
120
|
const configStore = await new ConfigStore(paths.config).load();
|
|
120
|
-
const defaultWorkspace =
|
|
121
|
+
const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
|
|
121
122
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
122
123
|
const workspaces = internals.workspaces
|
|
123
124
|
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
@@ -201,6 +202,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
201
202
|
const harness = new Harness({
|
|
202
203
|
...connection,
|
|
203
204
|
workspace: defaultWorkspace,
|
|
205
|
+
ungroupedWorkspace,
|
|
204
206
|
// This plugin is already hosted by a running DSH process. Starting a
|
|
205
207
|
// second DSH would create a competing server and lifecycle.
|
|
206
208
|
autostart: false,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
createWorkspaceAwareController,
|
|
15
15
|
observeBotWorkspaceRemovals,
|
|
16
16
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
17
|
+
import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
|
|
17
18
|
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
18
19
|
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
19
20
|
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
|
|
@@ -55,7 +56,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
55
56
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
56
57
|
const paths = pluginPaths(config);
|
|
57
58
|
const configStore = await new ConfigStore(paths.config).load();
|
|
58
|
-
const defaultWorkspace =
|
|
59
|
+
const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
|
|
59
60
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
60
61
|
const workspaces = internals.workspaces
|
|
61
62
|
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
@@ -96,6 +97,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
96
97
|
const harness = new Harness({
|
|
97
98
|
...connection,
|
|
98
99
|
workspace: defaultWorkspace,
|
|
100
|
+
ungroupedWorkspace,
|
|
99
101
|
autostart: false,
|
|
100
102
|
dshBin: config.dshBin ?? 'dsh',
|
|
101
103
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
createWorkspaceAwareController,
|
|
17
17
|
observeBotWorkspaceRemovals,
|
|
18
18
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
19
|
+
import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
|
|
19
20
|
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
20
21
|
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
21
22
|
import {
|
|
@@ -65,7 +66,7 @@ export async function createTokenProductionController(ctx, config, internals, de
|
|
|
65
66
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
66
67
|
const paths = pluginPaths(config, channel);
|
|
67
68
|
const configStore = await new ResolvedConfigStore(paths.config).load();
|
|
68
|
-
const defaultWorkspace =
|
|
69
|
+
const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
|
|
69
70
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
70
71
|
const workspaces = internals.workspaces
|
|
71
72
|
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
@@ -105,6 +106,7 @@ export async function createTokenProductionController(ctx, config, internals, de
|
|
|
105
106
|
const harness = new ResolvedHarness({
|
|
106
107
|
...connection,
|
|
107
108
|
workspace: defaultWorkspace,
|
|
109
|
+
ungroupedWorkspace,
|
|
108
110
|
autostart: false,
|
|
109
111
|
dshBin: config.dshBin ?? 'dsh',
|
|
110
112
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
createWorkspaceAwareController,
|
|
13
13
|
observeBotWorkspaceRemovals,
|
|
14
14
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
15
|
+
import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
|
|
15
16
|
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
16
17
|
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
17
18
|
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
|
|
@@ -44,7 +45,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
44
45
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
45
46
|
const paths = pluginPaths(config, 'slack');
|
|
46
47
|
const configStore = await new ResolvedConfigStore(paths.config).load();
|
|
47
|
-
const defaultWorkspace =
|
|
48
|
+
const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
|
|
48
49
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
49
50
|
const workspaces = internals.workspaces
|
|
50
51
|
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
@@ -84,6 +85,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
84
85
|
const harness = new ResolvedHarness({
|
|
85
86
|
...connection,
|
|
86
87
|
workspace: defaultWorkspace,
|
|
88
|
+
ungroupedWorkspace,
|
|
87
89
|
autostart: false,
|
|
88
90
|
dshBin: config.dshBin ?? 'dsh',
|
|
89
91
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
createWorkspaceAwareController,
|
|
15
15
|
observeBotWorkspaceRemovals,
|
|
16
16
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
17
|
+
import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
|
|
17
18
|
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
18
19
|
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
19
20
|
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
|
|
@@ -55,7 +56,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
55
56
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
56
57
|
const paths = pluginPaths(config);
|
|
57
58
|
const configStore = await new ConfigStore(paths.config).load();
|
|
58
|
-
const defaultWorkspace =
|
|
59
|
+
const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
|
|
59
60
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
60
61
|
const workspaces = internals.workspaces
|
|
61
62
|
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
@@ -99,6 +100,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
99
100
|
const harness = new Harness({
|
|
100
101
|
...connection,
|
|
101
102
|
workspace: defaultWorkspace,
|
|
103
|
+
ungroupedWorkspace,
|
|
102
104
|
autostart: false,
|
|
103
105
|
dshBin: config.dshBin ?? 'dsh',
|
|
104
106
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
createWorkspaceAwareController,
|
|
15
15
|
observeBotWorkspaceRemovals,
|
|
16
16
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
17
|
+
import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
|
|
17
18
|
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
18
19
|
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
19
20
|
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
|
|
@@ -69,7 +70,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
69
70
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
70
71
|
const paths = pluginPaths(config);
|
|
71
72
|
const configStore = await new ConfigStore(paths.config).load();
|
|
72
|
-
const defaultWorkspace =
|
|
73
|
+
const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
|
|
73
74
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
74
75
|
const workspaces = internals.workspaces
|
|
75
76
|
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
@@ -133,6 +134,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
133
134
|
const harness = new Harness({
|
|
134
135
|
...connection,
|
|
135
136
|
workspace: defaultWorkspace,
|
|
137
|
+
ungroupedWorkspace,
|
|
136
138
|
autostart: false,
|
|
137
139
|
dshBin: config.dshBin ?? 'dsh',
|
|
138
140
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
createWorkspaceAwareController,
|
|
19
19
|
observeBotWorkspaceRemovals,
|
|
20
20
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
21
|
+
import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
|
|
21
22
|
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
22
23
|
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
23
24
|
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
|
|
@@ -90,7 +91,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
90
91
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
91
92
|
const paths = pluginPaths(config);
|
|
92
93
|
const configStore = await new ConfigStore(paths.config).load();
|
|
93
|
-
const defaultWorkspace =
|
|
94
|
+
const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
|
|
94
95
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
95
96
|
const workspaces = diagnosticWorkspaces(internals.workspaces
|
|
96
97
|
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load());
|
|
@@ -131,6 +132,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
131
132
|
const harness = new Harness({
|
|
132
133
|
...connection,
|
|
133
134
|
workspace: defaultWorkspace,
|
|
135
|
+
ungroupedWorkspace,
|
|
134
136
|
autostart: false,
|
|
135
137
|
dshBin: config.dshBin ?? 'dsh',
|
|
136
138
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
createWorkspaceAwareController,
|
|
18
18
|
observeBotWorkspaceRemovals,
|
|
19
19
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
20
|
+
import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
|
|
20
21
|
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
21
22
|
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
22
23
|
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
|
|
@@ -65,7 +66,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
65
66
|
const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor;
|
|
66
67
|
const paths = pluginPaths(config);
|
|
67
68
|
const configStore = await new ConfigStore(paths.config).load();
|
|
68
|
-
const defaultWorkspace =
|
|
69
|
+
const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
|
|
69
70
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
70
71
|
const workspaces = internals.workspaces
|
|
71
72
|
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
@@ -105,6 +106,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
105
106
|
const harness = new Harness({
|
|
106
107
|
...connection,
|
|
107
108
|
workspace: defaultWorkspace,
|
|
109
|
+
ungroupedWorkspace,
|
|
108
110
|
autostart: false,
|
|
109
111
|
dshBin: config.dshBin ?? 'dsh',
|
|
110
112
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateBotAlias, withBotAlias } from './bot-alias.mjs';
|
|
2
|
+
import { defaultImWorkspace, sameWorkspacePath } from './default-workspace.mjs';
|
|
2
3
|
import {
|
|
3
4
|
mkdir,
|
|
4
5
|
readFile,
|
|
@@ -52,15 +53,6 @@ async function canonicalWorkspacePath(value) {
|
|
|
52
53
|
return resolve(await realpath(value));
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
async function sameWorkspacePath(left, right) {
|
|
56
|
-
if (left === right) return true;
|
|
57
|
-
try {
|
|
58
|
-
return await canonicalWorkspacePath(left) === await canonicalWorkspacePath(right);
|
|
59
|
-
} catch {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
56
|
function botIdOf(value) {
|
|
65
57
|
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(value)) {
|
|
66
58
|
throw new TypeError('Invalid bot id');
|
|
@@ -420,7 +412,7 @@ export class BotWorkspaceStore {
|
|
|
420
412
|
#writeQueue = Promise.resolve();
|
|
421
413
|
#botQueues = new Map();
|
|
422
414
|
|
|
423
|
-
constructor(path, { defaultWorkspace =
|
|
415
|
+
constructor(path, { defaultWorkspace = defaultImWorkspace() } = {}) {
|
|
424
416
|
if (typeof path !== 'string' || !path) throw new TypeError('workspace store path is required');
|
|
425
417
|
this.#path = path;
|
|
426
418
|
this.#defaultWorkspace = resolve(defaultWorkspace);
|
|
@@ -1597,6 +1589,7 @@ export function createBotWorkspaceScope(
|
|
|
1597
1589
|
if (!sessionId) return true;
|
|
1598
1590
|
let sessionWorkspace = sessionGenerations.get(sessionId)?.workspace;
|
|
1599
1591
|
let matches = false;
|
|
1592
|
+
let unregistered = true;
|
|
1600
1593
|
if (!sessionWorkspace && typeof harness.rpc === 'function') {
|
|
1601
1594
|
// Read registration metadata only: adopting a Session is not a lookup.
|
|
1602
1595
|
// Resolve by id before comparing real paths so symlink pins remain valid.
|
|
@@ -1609,7 +1602,11 @@ export function createBotWorkspaceScope(
|
|
|
1609
1602
|
if (owners.length === 1 && typeof owners[0].path === 'string' && isAbsolute(owners[0].path)) {
|
|
1610
1603
|
sessionWorkspace = owners[0].path;
|
|
1611
1604
|
}
|
|
1612
|
-
|
|
1605
|
+
unregistered = owners.length === 0;
|
|
1606
|
+
}
|
|
1607
|
+
if (!sessionWorkspace && unregistered && typeof harness.listWorkspaceSessions === 'function') {
|
|
1608
|
+
// After a restart, default IM sessions have no registry owner. Reuse
|
|
1609
|
+
// the read-only list, which also recognizes ungrouped default sessions.
|
|
1613
1610
|
const listed = await harness.listWorkspaceSessions(await canonicalWorkspacePath(workspace));
|
|
1614
1611
|
if (!Array.isArray(listed?.sessions)) {
|
|
1615
1612
|
throw new TypeError('Harness returned an invalid workspace session list');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mkdir, realpath } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export function defaultImWorkspace(config = {}) {
|
|
6
|
+
const dshHome = config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
7
|
+
return resolve(dshHome, 'im');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function sameWorkspacePath(left, right) {
|
|
11
|
+
if (typeof left !== 'string' || typeof right !== 'string'
|
|
12
|
+
|| !isAbsolute(left) || !isAbsolute(right)) return false;
|
|
13
|
+
if (resolve(left) === resolve(right)) return true;
|
|
14
|
+
try {
|
|
15
|
+
return resolve(await realpath(left)) === resolve(await realpath(right));
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function prepareBotWorkspace(config = {}) {
|
|
22
|
+
const ungroupedWorkspace = defaultImWorkspace(config);
|
|
23
|
+
const defaultWorkspace = resolve(config.workspace ?? ungroupedWorkspace);
|
|
24
|
+
if (await sameWorkspacePath(defaultWorkspace, ungroupedWorkspace)) {
|
|
25
|
+
await mkdir(defaultWorkspace, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
return { defaultWorkspace, ungroupedWorkspace };
|
|
28
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
4
|
import { isAbsolute } from 'node:path';
|
|
4
5
|
|
|
5
6
|
import { adoptRegisteredWorkspaceSession } from './harness-session-binding.mjs';
|
|
7
|
+
import { sameWorkspacePath } from './default-workspace.mjs';
|
|
6
8
|
import {
|
|
7
9
|
appendInboundFilesToPrompt,
|
|
8
10
|
InboundFileError,
|
|
@@ -717,6 +719,7 @@ export class HarnessClient {
|
|
|
717
719
|
#baseUrl;
|
|
718
720
|
#apiProxy;
|
|
719
721
|
#workspace;
|
|
722
|
+
#ungroupedWorkspace;
|
|
720
723
|
#agentPreset;
|
|
721
724
|
#autostart;
|
|
722
725
|
#dshBin;
|
|
@@ -740,6 +743,7 @@ export class HarnessClient {
|
|
|
740
743
|
apiProxy,
|
|
741
744
|
interactionScope = apiProxy,
|
|
742
745
|
workspace,
|
|
746
|
+
ungroupedWorkspace,
|
|
743
747
|
agentPreset,
|
|
744
748
|
autostart = false,
|
|
745
749
|
dshBin = 'dsh',
|
|
@@ -788,6 +792,8 @@ export class HarnessClient {
|
|
|
788
792
|
throw new TypeError('interactionScope must identify the current Host');
|
|
789
793
|
}
|
|
790
794
|
this.#workspace = workspace;
|
|
795
|
+
// Only the reserved IM directory bypasses grouping, even after /ws switches.
|
|
796
|
+
this.#ungroupedWorkspace = ungroupedWorkspace;
|
|
791
797
|
// Keep an omitted preset absent so session.create resolves the Host's current default.
|
|
792
798
|
this.#agentPreset = agentPreset ?? undefined;
|
|
793
799
|
this.#autostart = Boolean(this.#baseUrl && autostart);
|
|
@@ -921,6 +927,38 @@ export class HarnessClient {
|
|
|
921
927
|
await this.ensureRunning(options);
|
|
922
928
|
const workspaceList = await this.rpc('workspace.list', {}, 30_000, options);
|
|
923
929
|
const workspace = workspaceFromList(workspacePath, workspaceList);
|
|
930
|
+
if (await this.isUngroupedWorkspace(workspacePath)) {
|
|
931
|
+
const sessionList = await this.rpc('session.list', {}, 30_000, options);
|
|
932
|
+
if (!Array.isArray(sessionList?.items)) {
|
|
933
|
+
throw new Error('Harness returned an invalid response for session.list');
|
|
934
|
+
}
|
|
935
|
+
const registered = new Set();
|
|
936
|
+
const selected = new Set();
|
|
937
|
+
for (const item of workspaceList.items) {
|
|
938
|
+
if (!Array.isArray(item?.sessionIds)
|
|
939
|
+
|| item.sessionIds.some((id) => typeof id !== 'string' || !id)) {
|
|
940
|
+
throw new Error('Harness returned invalid session IDs for workspace.list');
|
|
941
|
+
}
|
|
942
|
+
for (const id of item.sessionIds) registered.add(id);
|
|
943
|
+
if (await sameWorkspacePath(item.path, workspacePath)) {
|
|
944
|
+
for (const id of item.sessionIds) selected.add(id);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
for (const item of sessionList.items) {
|
|
948
|
+
if (typeof item?.sessionId !== 'string' || !item.sessionId) {
|
|
949
|
+
throw new Error('Harness returned an invalid response for session.list');
|
|
950
|
+
}
|
|
951
|
+
// A matching cwd never overrides an explicit group assignment.
|
|
952
|
+
if (!registered.has(item.sessionId) && await this.isUngroupedWorkspace(item.cwd)) {
|
|
953
|
+
selected.add(item.sessionId);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return workspaceSessions(
|
|
957
|
+
{ path: workspacePath, sessionIds: [...selected] },
|
|
958
|
+
workspaceList.archivedSessionIds,
|
|
959
|
+
sessionList,
|
|
960
|
+
);
|
|
961
|
+
}
|
|
924
962
|
if (!workspace) return { workspace: workspacePath, sessions: [] };
|
|
925
963
|
const sessionList = await this.rpc('session.list', {}, 30_000, options);
|
|
926
964
|
return workspaceSessions(workspace, workspaceList.archivedSessionIds, sessionList);
|
|
@@ -988,6 +1026,10 @@ export class HarnessClient {
|
|
|
988
1026
|
return adoptRegisteredWorkspaceSession(this, value, options);
|
|
989
1027
|
}
|
|
990
1028
|
|
|
1029
|
+
async isUngroupedWorkspace(workspace) {
|
|
1030
|
+
return sameWorkspacePath(workspace, this.#ungroupedWorkspace);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
991
1033
|
async workspaceId(options = {}) {
|
|
992
1034
|
const { workspace = this.#workspace, ...rpcOptions } = options;
|
|
993
1035
|
const { items } = await this.rpc('workspace.list', {}, 30_000, rpcOptions);
|
|
@@ -999,9 +1041,13 @@ export class HarnessClient {
|
|
|
999
1041
|
|
|
1000
1042
|
async createSession(options = {}) {
|
|
1001
1043
|
const { agentPreset: requestedPreset, ...rpcOptions } = options;
|
|
1044
|
+
const workspace = rpcOptions.workspace ?? this.#workspace;
|
|
1045
|
+
const ungrouped = await this.isUngroupedWorkspace(workspace);
|
|
1046
|
+
if (ungrouped) await mkdir(workspace, { recursive: true });
|
|
1002
1047
|
await this.ensureRunning(rpcOptions);
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1048
|
+
const payload = ungrouped
|
|
1049
|
+
? { cwd: workspace }
|
|
1050
|
+
: { workspaceId: await this.workspaceId(rpcOptions) };
|
|
1005
1051
|
const agentPreset = requestedPreset !== undefined ? requestedPreset : this.#agentPreset;
|
|
1006
1052
|
if (agentPreset != null) payload.agentPreset = agentPreset;
|
|
1007
1053
|
const created = await this.rpc('session.create', payload, 30_000, rpcOptions);
|
|
@@ -37,16 +37,13 @@ function sessionWorkspace(sessionId, value) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
if (owners.length
|
|
41
|
-
throw bindingError('session-not-registered', 'The session is not registered to a Harness workspace');
|
|
42
|
-
}
|
|
43
|
-
if (owners.length !== 1) {
|
|
40
|
+
if (owners.length > 1) {
|
|
44
41
|
throw bindingError(
|
|
45
42
|
'session-workspace-ambiguous',
|
|
46
43
|
'The session is registered to more than one Harness workspace',
|
|
47
44
|
);
|
|
48
45
|
}
|
|
49
|
-
if (UNSAFE_WORKSPACE_PATH.test(owners[0].path)) {
|
|
46
|
+
if (owners.length && UNSAFE_WORKSPACE_PATH.test(owners[0].path)) {
|
|
50
47
|
throw new Error('Harness returned an unsafe workspace path for the session');
|
|
51
48
|
}
|
|
52
49
|
return {
|
|
@@ -82,20 +79,28 @@ function sessionSummary(sessionId, value) {
|
|
|
82
79
|
if (title !== undefined && title !== null && typeof title !== 'string') {
|
|
83
80
|
throw new Error('Harness returned an invalid session title for session.list');
|
|
84
81
|
}
|
|
85
|
-
return { title: typeof title === 'string' ? title : null };
|
|
82
|
+
return { cwd: summary.cwd, title: typeof title === 'string' ? title : null };
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
export async function adoptRegisteredWorkspaceSession(client, value, options = {}, timeoutMs = 30_000) {
|
|
89
86
|
const sessionId = validatedSessionId(value);
|
|
90
87
|
await client.ensureRunning(options);
|
|
91
88
|
const workspaceList = await client.rpc('workspace.list', {}, timeoutMs, options);
|
|
92
|
-
|
|
89
|
+
let { workspace, archived } = sessionWorkspace(sessionId, workspaceList);
|
|
93
90
|
const summary = sessionSummary(
|
|
94
91
|
sessionId,
|
|
95
92
|
await client.rpc('session.list', {}, timeoutMs, options),
|
|
96
93
|
);
|
|
94
|
+
if (!workspace) {
|
|
95
|
+
if (typeof summary.cwd !== 'string' || !isAbsolute(summary.cwd)
|
|
96
|
+
|| UNSAFE_WORKSPACE_PATH.test(summary.cwd)
|
|
97
|
+
|| !await client.isUngroupedWorkspace?.(summary.cwd)) {
|
|
98
|
+
throw bindingError('session-not-registered', 'The session is not registered to a Harness workspace');
|
|
99
|
+
}
|
|
100
|
+
workspace = { path: summary.cwd };
|
|
101
|
+
}
|
|
97
102
|
const adopted = await client.rpc('session.create', {
|
|
98
|
-
workspaceId: workspace.workspaceId,
|
|
103
|
+
...(workspace.workspaceId ? { workspaceId: workspace.workspaceId } : { cwd: workspace.path }),
|
|
99
104
|
sessionId,
|
|
100
105
|
}, timeoutMs, options);
|
|
101
106
|
if (!adopted || adopted.sessionId !== sessionId) {
|