@xmanrui/dsh-im 4.21.1 → 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 +17 -2
- package/README.md +17 -2
- package/docs/client-integration.md +81 -0
- package/lib/client.js +152 -45
- package/lib/index.js +281 -281
- package/package.json +2 -1
- package/plugin-src/client/channels/feishu/index.js +22 -7
- package/plugin-src/client/credential-binding.js +2 -0
- package/plugin-src/client/i18n.js +7 -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/feishu/rpc.mjs +2 -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/scripts/verify-lan-management.mjs +53 -8
- 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
|
@@ -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) {
|