@xmanrui/dsh-im 4.18.0 → 4.19.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 +8 -3
- package/README.md +8 -3
- package/lib/client.js +809 -515
- package/lib/index.js +269 -267
- package/package.json +5 -1
- package/plugin-src/client/bot-alias.js +92 -0
- package/plugin-src/client/channels/dingtalk/api.js +3 -0
- package/plugin-src/client/channels/dingtalk/index.js +7 -1
- package/plugin-src/client/channels/feishu/api.js +3 -0
- package/plugin-src/client/channels/feishu/index.js +7 -1
- package/plugin-src/client/channels/qq/api.js +3 -0
- package/plugin-src/client/channels/qq/index.js +9 -1
- package/plugin-src/client/channels/shared/token-api.js +3 -0
- package/plugin-src/client/channels/shared/token-channel.js +9 -2
- package/plugin-src/client/channels/wecom/api.js +3 -0
- package/plugin-src/client/channels/wecom/index.js +9 -1
- package/plugin-src/client/channels/wecom-app/api.js +3 -0
- package/plugin-src/client/channels/wecom-app/index.js +9 -1
- package/plugin-src/client/channels/weixin/api.js +3 -0
- package/plugin-src/client/channels/weixin/index.js +7 -1
- package/plugin-src/client/channels/whatsapp/api.js +3 -0
- package/plugin-src/client/channels/whatsapp/index.js +9 -1
- package/plugin-src/client/i18n.js +10 -0
- package/plugin-src/client/styles.js +26 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/feishu/production.mjs +22 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +14 -0
- package/plugin-src/host/channels/imessage/rpc.mjs +1 -0
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/bot-alias-rpc.mjs +15 -0
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/slack/rpc.mjs +10 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +10 -0
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +13 -0
- package/plugin-src/host/rpc-authority.mjs +3 -2
- package/plugin-src/host/session-sync-coordinator.mjs +19 -1
- package/scripts/verify-lan-management.mjs +176 -0
- package/src/channels/feishu/bridge.mjs +370 -7
- package/src/channels/feishu/feishu-cards.mjs +1 -1
- package/src/channels/feishu/feishu-runtime.mjs +6 -0
- package/src/channels/feishu/state-store.mjs +17 -0
- package/src/channels/shared/bot-alias.mjs +29 -0
- package/src/channels/shared/bot-workspace-store.mjs +71 -3
- package/src/channels/shared/i18n-en/shared-a.mjs +2 -2
- package/src/channels/shared/message-failure.mjs +2 -1
- package/src/channels/shared/model-command.mjs +3 -2
- package/src/channels/shared/session-sync-registry.mjs +34 -0
- package/src/channels/shared/text-harness-bridge.mjs +38 -0
- package/src/channels/telegram/telegram-runtime.mjs +77 -25
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
|
|
1
2
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
2
3
|
import QRCode from 'qrcode';
|
|
3
4
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
|
@@ -25,6 +26,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
|
|
|
25
26
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
26
27
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
|
27
28
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
|
29
|
+
setAlias: SET_ALIAS_ENDPOINT,
|
|
28
30
|
approveSender: 'bot.sender.approve',
|
|
29
31
|
revokeSender: 'bot.sender.revoke',
|
|
30
32
|
});
|
|
@@ -112,6 +114,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
112
114
|
return validAccessPolicyPayload(payload)
|
|
113
115
|
? null : '请提交有效的访问设置。';
|
|
114
116
|
}
|
|
117
|
+
if (endpoint === DINGTALK_ENDPOINTS.setAlias) {
|
|
118
|
+
return validAliasPayload(payload)
|
|
119
|
+
? null : '请输入有效的别名(最多 80 个字符)。';
|
|
120
|
+
}
|
|
115
121
|
if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
|
|
116
122
|
return exactKeys(payload, ['botId', 'requestId', 'confirm'])
|
|
117
123
|
&& validId(payload.botId)
|
|
@@ -301,6 +307,11 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|
|
301
307
|
value = await controller.updateContextEnhancement(
|
|
302
308
|
payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
|
|
303
309
|
);
|
|
310
|
+
} else if (endpoint === DINGTALK_ENDPOINTS.setAlias) {
|
|
311
|
+
if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
|
|
312
|
+
value = await controller.updateAlias(
|
|
313
|
+
payload.botId, payload.alias, (status) => publicStatus(status, cachedEncode),
|
|
314
|
+
);
|
|
304
315
|
} else if (endpoint === DINGTALK_ENDPOINTS.setAccessPolicy) {
|
|
305
316
|
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
|
306
317
|
value = await controller.updateAccessPolicy(
|
|
@@ -162,6 +162,27 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
162
162
|
if (!botConfig) throw new Error('Unknown Feishu bot');
|
|
163
163
|
return stateFor(botConfig);
|
|
164
164
|
};
|
|
165
|
+
// Session-sync mirror targets: which synced DMs (openId) belong to a
|
|
166
|
+
// Session. Resolved lazily per turn start so rebinding a delivery target
|
|
167
|
+
// takes effect without a restart.
|
|
168
|
+
const sessionSyncTargetsFor = async (sessionId) => {
|
|
169
|
+
if (typeof workspaces.listSessionSyncTargets !== 'function') return [];
|
|
170
|
+
const matches = [];
|
|
171
|
+
for (const entry of workspaces.listSessionSyncTargets()) {
|
|
172
|
+
let state;
|
|
173
|
+
try {
|
|
174
|
+
state = await stateForBotId(entry.botId);
|
|
175
|
+
} catch {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const bound = state?.snapshot?.().sessions?.[entry.conversationKey];
|
|
179
|
+
if (bound !== sessionId) continue;
|
|
180
|
+
const target = workspaces.deliveryTargetFor(entry.botId, entry.targetId);
|
|
181
|
+
const openId = target?.route?.openId;
|
|
182
|
+
if (openId) matches.push({ openId, botId: entry.botId, targetId: entry.targetId });
|
|
183
|
+
}
|
|
184
|
+
return matches;
|
|
185
|
+
};
|
|
165
186
|
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
166
187
|
const inboundTtl = internals.inboundTtl ?? getInboundTtlRuntime(ctx, config);
|
|
167
188
|
const inboundTtlService = inboundTtl?.service ?? inboundTtl;
|
|
@@ -220,6 +241,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
220
241
|
groupTopicReply: botConfig.groupTopicReply,
|
|
221
242
|
stepPush: botConfig.stepPush,
|
|
222
243
|
stepPushMode: botConfig.stepPushMode,
|
|
244
|
+
sessionSyncTargetsFor: sessionSyncTargetsFor,
|
|
223
245
|
ownerOpenIds: botConfig.ownerOpenIds ?? [botConfig.ownerOpenId],
|
|
224
246
|
harness: workspaceScope.harness,
|
|
225
247
|
state: workspaceScope.state,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
|
|
2
|
+
import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
|
|
1
3
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
2
4
|
import QRCode from 'qrcode';
|
|
3
5
|
import {
|
|
@@ -34,6 +36,7 @@ import {
|
|
|
34
36
|
export const FEISHU_ENDPOINTS = Object.freeze({
|
|
35
37
|
...FEISHU_CLIENT_ENDPOINTS,
|
|
36
38
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
|
39
|
+
setAlias: SET_ALIAS_ENDPOINT,
|
|
37
40
|
});
|
|
38
41
|
export { FEISHU_RPC_CHANNEL };
|
|
39
42
|
export const FEISHU_MULTI_ENDPOINTS = Object.freeze({
|
|
@@ -214,6 +217,7 @@ function connectionFacts(connection) {
|
|
|
214
217
|
function publicBot(bot) {
|
|
215
218
|
const source = bot && typeof bot === 'object' ? bot : {};
|
|
216
219
|
const result = {
|
|
220
|
+
...normalizeBotAlias(source),
|
|
217
221
|
name: typeof source.name === 'string' && source.name.length > 0 ? source.name : '飞书机器人',
|
|
218
222
|
};
|
|
219
223
|
if (typeof source.avatarUrl === 'string') result.avatarUrl = source.avatarUrl;
|
|
@@ -451,6 +455,10 @@ function validPayload(endpoint, payload) {
|
|
|
451
455
|
return validAccessPolicyPayload(payload)
|
|
452
456
|
? null : '请提交有效的访问设置。';
|
|
453
457
|
}
|
|
458
|
+
if (endpoint === FEISHU_ENDPOINTS.setAlias) {
|
|
459
|
+
return validAliasPayload(payload)
|
|
460
|
+
? null : '请输入有效的别名(最多 80 个字符)。';
|
|
461
|
+
}
|
|
454
462
|
if (endpoint === FEISHU_ENDPOINTS.setGroupResponseMode) {
|
|
455
463
|
return hasOnlyKeys(payload, new Set(['botId', 'groupResponseMode']))
|
|
456
464
|
&& safeOpaqueId(payload.botId)
|
|
@@ -735,6 +743,12 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
|
|
|
735
743
|
payload.botId, payload.config,
|
|
736
744
|
(status) => toPublicFeishuStatus(status, { encodeQr: cachedEncodeQr }),
|
|
737
745
|
);
|
|
746
|
+
} else if (endpoint === FEISHU_ENDPOINTS.setAlias) {
|
|
747
|
+
if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
|
|
748
|
+
value = await controller.updateAlias(
|
|
749
|
+
payload.botId, payload.alias,
|
|
750
|
+
(status) => toPublicFeishuStatus(status, { encodeQr: cachedEncodeQr }),
|
|
751
|
+
);
|
|
738
752
|
} else if (endpoint === FEISHU_ENDPOINTS.setAccessPolicy) {
|
|
739
753
|
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
|
740
754
|
value = await controller.updateAccessPolicy(
|
|
@@ -15,6 +15,7 @@ export const IMESSAGE_ENDPOINTS = Object.freeze({
|
|
|
15
15
|
setAgentPreset: 'bot.agent-preset.set',
|
|
16
16
|
setContextEnhancement: 'bot.context-enhancement.set',
|
|
17
17
|
setAccessPolicy: 'bot.access-policy.set',
|
|
18
|
+
setAlias: 'bot.alias.set',
|
|
18
19
|
});
|
|
19
20
|
export const IMESSAGE_RPC_ENDPOINTS = Object.freeze(Object.values(IMESSAGE_ENDPOINTS));
|
|
20
21
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
|
|
1
2
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
2
3
|
import QRCode from 'qrcode';
|
|
3
4
|
import {
|
|
@@ -25,6 +26,7 @@ export const QQ_ENDPOINTS = Object.freeze({
|
|
|
25
26
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
26
27
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
|
27
28
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
|
29
|
+
setAlias: SET_ALIAS_ENDPOINT,
|
|
28
30
|
});
|
|
29
31
|
export const QQ_RPC_ENDPOINTS = Object.freeze(Object.values(QQ_ENDPOINTS));
|
|
30
32
|
|
|
@@ -94,6 +96,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
94
96
|
return validAccessPolicyPayload(payload)
|
|
95
97
|
? null : '请提交有效的访问设置。';
|
|
96
98
|
}
|
|
99
|
+
if (endpoint === QQ_ENDPOINTS.setAlias) {
|
|
100
|
+
return validAliasPayload(payload)
|
|
101
|
+
? null : '请输入有效的别名(最多 80 个字符)。';
|
|
102
|
+
}
|
|
97
103
|
return 'Unknown QQ endpoint.';
|
|
98
104
|
}
|
|
99
105
|
|
|
@@ -200,6 +206,11 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
|
|
|
200
206
|
value = await controller.updateContextEnhancement(
|
|
201
207
|
payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
|
|
202
208
|
);
|
|
209
|
+
} else if (endpoint === QQ_ENDPOINTS.setAlias) {
|
|
210
|
+
if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
|
|
211
|
+
value = await controller.updateAlias(
|
|
212
|
+
payload.botId, payload.alias, (status) => publicStatus(status, cachedEncode),
|
|
213
|
+
);
|
|
203
214
|
} else if (endpoint === QQ_ENDPOINTS.setAccessPolicy) {
|
|
204
215
|
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
|
205
216
|
value = await controller.updateAccessPolicy(
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { validateBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
|
|
2
|
+
export { SET_ALIAS_ENDPOINT } from '../../../../src/channels/shared/bot-alias.mjs';
|
|
3
|
+
|
|
4
|
+
export function validAliasPayload(payload) {
|
|
5
|
+
try {
|
|
6
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)
|
|
7
|
+
|| Object.keys(payload).length !== 2
|
|
8
|
+
|| typeof payload.botId !== 'string'
|
|
9
|
+
|| !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) return false;
|
|
10
|
+
validateBotAlias(payload.alias);
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SET_ALIAS_ENDPOINT, validAliasPayload } from './bot-alias-rpc.mjs';
|
|
1
2
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
2
3
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-rpc.mjs';
|
|
3
4
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from './access-policy-rpc.mjs';
|
|
@@ -24,6 +25,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
|
|
|
24
25
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
25
26
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
|
26
27
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
|
28
|
+
setAlias: SET_ALIAS_ENDPOINT,
|
|
27
29
|
});
|
|
28
30
|
|
|
29
31
|
const ENDPOINTS = Object.freeze(Object.values(TOKEN_BOT_ENDPOINTS));
|
|
@@ -90,6 +92,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
90
92
|
return validAccessPolicyPayload(payload)
|
|
91
93
|
? null : '请提交有效的访问设置。';
|
|
92
94
|
}
|
|
95
|
+
if (endpoint === TOKEN_BOT_ENDPOINTS.setAlias) {
|
|
96
|
+
return validAliasPayload(payload)
|
|
97
|
+
? null : '请输入有效的别名(最多 80 个字符)。';
|
|
98
|
+
}
|
|
93
99
|
return 'Unknown bot endpoint.';
|
|
94
100
|
}
|
|
95
101
|
|
|
@@ -177,6 +183,9 @@ export function createTokenBotRpcHandler(controller, { channel }) {
|
|
|
177
183
|
} else if (endpoint === TOKEN_BOT_ENDPOINTS.setContextEnhancement) {
|
|
178
184
|
if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
|
|
179
185
|
value = await controller.updateContextEnhancement(payload.botId, payload.config);
|
|
186
|
+
} else if (endpoint === TOKEN_BOT_ENDPOINTS.setAlias) {
|
|
187
|
+
if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
|
|
188
|
+
value = await controller.updateAlias(payload.botId, payload.alias);
|
|
180
189
|
} else if (endpoint === TOKEN_BOT_ENDPOINTS.setAccessPolicy) {
|
|
181
190
|
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
|
182
191
|
value = await controller.updateAccessPolicy(payload.botId, payload.policy);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
|
|
1
2
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
2
3
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
|
3
4
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
|
@@ -25,6 +26,7 @@ export const SLACK_ENDPOINTS = Object.freeze({
|
|
|
25
26
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
26
27
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
|
27
28
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
|
29
|
+
setAlias: SET_ALIAS_ENDPOINT,
|
|
28
30
|
});
|
|
29
31
|
export const SLACK_RPC_ENDPOINTS = Object.freeze(Object.values(SLACK_ENDPOINTS));
|
|
30
32
|
|
|
@@ -93,6 +95,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
93
95
|
return validAccessPolicyPayload(payload)
|
|
94
96
|
? null : '请提交有效的访问设置。';
|
|
95
97
|
}
|
|
98
|
+
if (endpoint === SLACK_ENDPOINTS.setAlias) {
|
|
99
|
+
return validAliasPayload(payload)
|
|
100
|
+
? null : '请输入有效的别名(最多 80 个字符)。';
|
|
101
|
+
}
|
|
96
102
|
return 'Unknown Slack endpoint.';
|
|
97
103
|
}
|
|
98
104
|
|
|
@@ -180,6 +186,10 @@ export function createSlackRpcHandler(controller) {
|
|
|
180
186
|
if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
|
|
181
187
|
value = await controller.updateContextEnhancement(payload.botId, payload.config);
|
|
182
188
|
}
|
|
189
|
+
else if (endpoint === SLACK_ENDPOINTS.setAlias) {
|
|
190
|
+
if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
|
|
191
|
+
value = await controller.updateAlias(payload.botId, payload.alias);
|
|
192
|
+
}
|
|
183
193
|
else if (endpoint === SLACK_ENDPOINTS.setAccessPolicy) {
|
|
184
194
|
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
|
185
195
|
value = await controller.updateAccessPolicy(payload.botId, payload.policy);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
|
|
1
2
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
2
3
|
import QRCode from 'qrcode';
|
|
3
4
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
|
@@ -25,6 +26,7 @@ export const WECOM_ENDPOINTS = Object.freeze({
|
|
|
25
26
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
26
27
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
|
27
28
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
|
29
|
+
setAlias: SET_ALIAS_ENDPOINT,
|
|
28
30
|
});
|
|
29
31
|
export const WECOM_RPC_ENDPOINTS = Object.freeze(Object.values(WECOM_ENDPOINTS));
|
|
30
32
|
|
|
@@ -94,6 +96,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
94
96
|
return validAccessPolicyPayload(payload)
|
|
95
97
|
? null : '请提交有效的访问设置。';
|
|
96
98
|
}
|
|
99
|
+
if (endpoint === WECOM_ENDPOINTS.setAlias) {
|
|
100
|
+
return validAliasPayload(payload)
|
|
101
|
+
? null : '请输入有效的别名(最多 80 个字符)。';
|
|
102
|
+
}
|
|
97
103
|
return 'Unknown Enterprise WeChat endpoint.';
|
|
98
104
|
}
|
|
99
105
|
|
|
@@ -201,6 +207,11 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
|
|
|
201
207
|
value = await controller.updateContextEnhancement(
|
|
202
208
|
payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
|
|
203
209
|
);
|
|
210
|
+
} else if (endpoint === WECOM_ENDPOINTS.setAlias) {
|
|
211
|
+
if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
|
|
212
|
+
value = await controller.updateAlias(
|
|
213
|
+
payload.botId, payload.alias, (status) => publicStatus(status, cachedEncode),
|
|
214
|
+
);
|
|
204
215
|
} else if (endpoint === WECOM_ENDPOINTS.setAccessPolicy) {
|
|
205
216
|
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
|
206
217
|
value = await controller.updateAccessPolicy(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
|
|
1
2
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
2
3
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
|
3
4
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
|
@@ -23,6 +24,7 @@ export const WECOM_APP_ENDPOINTS = Object.freeze({
|
|
|
23
24
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
24
25
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
|
25
26
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
|
27
|
+
setAlias: SET_ALIAS_ENDPOINT,
|
|
26
28
|
});
|
|
27
29
|
export const WECOM_APP_RPC_ENDPOINTS = Object.freeze(Object.values(WECOM_APP_ENDPOINTS));
|
|
28
30
|
|
|
@@ -113,6 +115,9 @@ function payloadFailure(endpoint, payload) {
|
|
|
113
115
|
if (endpoint === WECOM_APP_ENDPOINTS.setAccessPolicy) {
|
|
114
116
|
return validAccessPolicyPayload(payload) ? null : '请提交有效的访问设置。';
|
|
115
117
|
}
|
|
118
|
+
if (endpoint === WECOM_APP_ENDPOINTS.setAlias) {
|
|
119
|
+
return validAliasPayload(payload) ? null : '请输入有效的别名(最多 80 个字符)。';
|
|
120
|
+
}
|
|
116
121
|
return 'Unknown Enterprise WeChat app endpoint.';
|
|
117
122
|
}
|
|
118
123
|
|
|
@@ -189,6 +194,11 @@ export function createWecomAppRpcHandler(controller) {
|
|
|
189
194
|
value = await controller.updateContextEnhancement(
|
|
190
195
|
payload.botId, payload.config, (status) => publicStatus(status),
|
|
191
196
|
);
|
|
197
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.setAlias) {
|
|
198
|
+
if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
|
|
199
|
+
value = await controller.updateAlias(
|
|
200
|
+
payload.botId, payload.alias, (status) => publicStatus(status),
|
|
201
|
+
);
|
|
192
202
|
} else if (endpoint === WECOM_APP_ENDPOINTS.setAccessPolicy) {
|
|
193
203
|
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
|
194
204
|
value = await controller.updateAccessPolicy(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
|
|
1
2
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
2
3
|
import QRCode from 'qrcode';
|
|
3
4
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
|
@@ -32,6 +33,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
|
|
|
32
33
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
33
34
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
|
34
35
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
|
36
|
+
setAlias: SET_ALIAS_ENDPOINT,
|
|
35
37
|
});
|
|
36
38
|
export const WEIXIN_RPC_ENDPOINTS = Object.freeze(Object.values(WEIXIN_ENDPOINTS));
|
|
37
39
|
|
|
@@ -101,6 +103,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
101
103
|
return validAccessPolicyPayload(payload)
|
|
102
104
|
? null : '请提交有效的访问设置。';
|
|
103
105
|
}
|
|
106
|
+
if (endpoint === WEIXIN_ENDPOINTS.setAlias) {
|
|
107
|
+
return validAliasPayload(payload)
|
|
108
|
+
? null : '请输入有效的别名(最多 80 个字符)。';
|
|
109
|
+
}
|
|
104
110
|
return 'Unknown Weixin endpoint.';
|
|
105
111
|
}
|
|
106
112
|
|
|
@@ -236,6 +242,11 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
|
|
|
236
242
|
value = await controller.updateContextEnhancement(
|
|
237
243
|
payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
|
|
238
244
|
);
|
|
245
|
+
} else if (endpoint === WEIXIN_ENDPOINTS.setAlias) {
|
|
246
|
+
if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
|
|
247
|
+
value = await controller.updateAlias(
|
|
248
|
+
payload.botId, payload.alias, (status) => publicStatus(status, cachedEncode),
|
|
249
|
+
);
|
|
239
250
|
} else if (endpoint === WEIXIN_ENDPOINTS.setAccessPolicy) {
|
|
240
251
|
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
|
241
252
|
value = await controller.updateAccessPolicy(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
|
|
1
2
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
2
3
|
import QRCode from 'qrcode';
|
|
3
4
|
|
|
@@ -18,6 +19,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|
|
18
19
|
reconnectBot: 'bot.reconnect',
|
|
19
20
|
deleteBot: 'bot.delete',
|
|
20
21
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
|
22
|
+
setAlias: SET_ALIAS_ENDPOINT,
|
|
21
23
|
setWorkspace: SET_WORKSPACE_ENDPOINT,
|
|
22
24
|
setModel: SET_MODEL_ENDPOINT,
|
|
23
25
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
@@ -61,6 +63,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
61
63
|
return validAccessPolicyPayload(payload)
|
|
62
64
|
? null : '请提交有效的访问设置。';
|
|
63
65
|
}
|
|
66
|
+
if (endpoint === WHATSAPP_ENDPOINTS.setAlias) {
|
|
67
|
+
return validAliasPayload(payload)
|
|
68
|
+
? null : '请输入有效的别名(最多 80 个字符)。';
|
|
69
|
+
}
|
|
64
70
|
if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
|
|
65
71
|
return validWorkspacePayload(payload)
|
|
66
72
|
? null : '请输入工作区绝对路径。';
|
|
@@ -195,6 +201,13 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|
|
195
201
|
await controller.updateAgentPreset(payload.botId, payload.agentPreset),
|
|
196
202
|
cachedEncode,
|
|
197
203
|
);
|
|
204
|
+
} else if (endpoint === WHATSAPP_ENDPOINTS.setAlias) {
|
|
205
|
+
if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
|
|
206
|
+
value = await controller.updateAlias(
|
|
207
|
+
payload.botId,
|
|
208
|
+
payload.alias,
|
|
209
|
+
(status) => publicStatus(status, cachedEncode),
|
|
210
|
+
);
|
|
198
211
|
} else if (endpoint === WHATSAPP_ENDPOINTS.setAccessPolicy) {
|
|
199
212
|
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
|
200
213
|
value = await controller.updateAccessPolicy(
|
|
@@ -2,10 +2,11 @@ const RPC_AUTHORITIES = new Set(['loopback', 'trusted-host']);
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Resolve the browser authority accepted by an IM management RPC channel.
|
|
5
|
-
*
|
|
5
|
+
* By default, use the browser authentication and Host/Origin trust checks
|
|
6
|
+
* already enforced by Harness before it dispatches a management Fetch route.
|
|
6
7
|
*/
|
|
7
8
|
export function resolveRpcAuthority(value) {
|
|
8
|
-
if (value === undefined) return '
|
|
9
|
+
if (value === undefined) return 'trusted-host';
|
|
9
10
|
if (RPC_AUTHORITIES.has(value)) return value;
|
|
10
11
|
throw new TypeError('dsh-im rpcAuthority must be "loopback" or "trusted-host"');
|
|
11
12
|
}
|
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
consumeDshImInputOrigin,
|
|
4
4
|
textFromHarnessContent,
|
|
5
5
|
} from '../../src/channels/shared/harness-client.mjs';
|
|
6
|
+
import {
|
|
7
|
+
isSessionSyncMirrored,
|
|
8
|
+
releaseSessionSyncMirror,
|
|
9
|
+
} from '../../src/channels/shared/session-sync-registry.mjs';
|
|
6
10
|
|
|
7
11
|
const DSH_USER_PREFIX = '[来自 DSH]\n';
|
|
8
12
|
const DSH_ASSISTANT_PREFIX = '[DSH 助手]\n';
|
|
@@ -111,6 +115,10 @@ export function createSessionSyncCoordinator({ deliveryService, logger = console
|
|
|
111
115
|
if (event.surfaceOp !== 'append') return;
|
|
112
116
|
if (state.origin === 'unknown') state.origin = origin;
|
|
113
117
|
if (state.origin !== 'dsh' || origin !== 'dsh') return;
|
|
118
|
+
// The user echo stays as plain text even when the mirror owns the
|
|
119
|
+
// turn: the card also quotes the question, but keeping the echo here
|
|
120
|
+
// guarantees recipients are established so a failed mirror can still
|
|
121
|
+
// fall back to the final-answer text delivery.
|
|
114
122
|
const text = textFromHarnessContent(event.data?.content);
|
|
115
123
|
if (!text) return;
|
|
116
124
|
|
|
@@ -151,9 +159,19 @@ export function createSessionSyncCoordinator({ deliveryService, logger = console
|
|
|
151
159
|
turns.delete(sessionId);
|
|
152
160
|
if (state.origin !== 'dsh' || !completedTurn(event.data?.reason)
|
|
153
161
|
|| !state.recipients?.size || !state.assistant.text) return;
|
|
162
|
+
// Per-target suppression: targets whose process-card mirror delivered
|
|
163
|
+
// the answer are skipped; every other synced target still gets the text.
|
|
164
|
+
const mirrored = [];
|
|
165
|
+
const plain = [];
|
|
166
|
+
for (const target of state.recipients.values()) {
|
|
167
|
+
if (isSessionSyncMirrored(sessionId, target.targetId, state.turn)) mirrored.push(target);
|
|
168
|
+
else plain.push(target);
|
|
169
|
+
}
|
|
170
|
+
for (const target of mirrored) releaseSessionSyncMirror(sessionId, target.targetId);
|
|
171
|
+
if (plain.length === 0) return;
|
|
154
172
|
await deliver(
|
|
155
173
|
sessionId,
|
|
156
|
-
|
|
174
|
+
plain,
|
|
157
175
|
`${DSH_ASSISTANT_PREFIX}${state.assistant.text}`,
|
|
158
176
|
'assistant delivery',
|
|
159
177
|
);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/** Run the built plugin through an original DSH CLI with an isolated, empty home. */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { access, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { request as httpRequest } from 'node:http';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const harnessRoot = process.argv[2];
|
|
10
|
+
if (!harnessRoot || process.argv.includes('--help')) {
|
|
11
|
+
console.log('Usage: node scripts/verify-lan-management.mjs /path/to/built/deepseek-harness');
|
|
12
|
+
process.exit(harnessRoot ? 0 : 1);
|
|
13
|
+
}
|
|
14
|
+
const pluginRoot = resolve(import.meta.dirname, '..');
|
|
15
|
+
const cli = resolve(harnessRoot, 'apps/cli/lib/bin.js');
|
|
16
|
+
await access(cli);
|
|
17
|
+
await access(join(pluginRoot, 'lib/index.js'));
|
|
18
|
+
// DSH 0.1.5 CLI intentionally permits only loopback listening. Exercise its
|
|
19
|
+
// real HTTP carrier with a trusted LAN authority, while keeping TCP local.
|
|
20
|
+
const lanIp = '192.168.1.100';
|
|
21
|
+
|
|
22
|
+
const directory = await mkdtemp(join(tmpdir(), 'dsh-im-lan-test-'));
|
|
23
|
+
const home = join(directory, 'home');
|
|
24
|
+
const profile = join(home, 'profiles/web');
|
|
25
|
+
const results = [];
|
|
26
|
+
let child;
|
|
27
|
+
|
|
28
|
+
function request(url, { method = 'GET', headers = {}, body } = {}) {
|
|
29
|
+
return new Promise((resolveRequest, reject) => {
|
|
30
|
+
const req = httpRequest({
|
|
31
|
+
hostname: '127.0.0.1', port: url.port, path: `${url.pathname}${url.search}`,
|
|
32
|
+
method, headers: { host: url.host, ...headers },
|
|
33
|
+
}, res => {
|
|
34
|
+
let text = '';
|
|
35
|
+
res.setEncoding('utf8');
|
|
36
|
+
res.on('data', chunk => { text += chunk; });
|
|
37
|
+
res.on('error', reject);
|
|
38
|
+
res.on('end', () => resolveRequest({ status: res.statusCode, headers: res.headers, body: text }));
|
|
39
|
+
});
|
|
40
|
+
req.on('error', reject);
|
|
41
|
+
req.setTimeout(10_000, () => req.destroy(new Error('HTTP request timed out')));
|
|
42
|
+
req.end(body);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function start() {
|
|
47
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => (
|
|
48
|
+
!/^DSH_/i.test(key) && !/(?:KEY|SECRET|TOKEN|PASSWORD|PROXY)/i.test(key)
|
|
49
|
+
)));
|
|
50
|
+
child = spawn(process.execPath, [cli, 'web', '--no-open', '--host', '127.0.0.1',
|
|
51
|
+
'--port', '0', '--trusted-host', lanIp], {
|
|
52
|
+
cwd: directory,
|
|
53
|
+
env: { ...env, DSH_HOME: home, DSH_AGENTS_HOME: join(directory, '.agents'),
|
|
54
|
+
DSH_TELEMETRY_DISABLED: '1', SSH_CONNECTION: '', SSH_TTY: '' },
|
|
55
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
56
|
+
});
|
|
57
|
+
return new Promise((resolveStart, reject) => {
|
|
58
|
+
let output = '';
|
|
59
|
+
const timer = setTimeout(() => finish(new Error('DSH startup timed out')), 45_000);
|
|
60
|
+
let settled = false;
|
|
61
|
+
function finish(error, url) {
|
|
62
|
+
if (settled) return;
|
|
63
|
+
settled = true;
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
if (error) {
|
|
66
|
+
const safeOutput = output.replace(/([?&]token=)[^\s)]+/g, '$1<redacted>');
|
|
67
|
+
reject(new Error(`${error.message}\n${safeOutput}`));
|
|
68
|
+
} else resolveStart(new URL(url));
|
|
69
|
+
}
|
|
70
|
+
function append(chunk) {
|
|
71
|
+
output = `${output}${chunk}`.slice(-100_000);
|
|
72
|
+
const match = /dsh web: (http:\/\/[^\s]+)/.exec(output);
|
|
73
|
+
if (match) finish(null, match[1]);
|
|
74
|
+
}
|
|
75
|
+
child.stdout.on('data', append);
|
|
76
|
+
child.stderr.on('data', append);
|
|
77
|
+
child.once('error', error => finish(error));
|
|
78
|
+
child.once('exit', code => finish(new Error(`DSH exited before readiness: ${code}`)));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function stop() {
|
|
83
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
84
|
+
const stopped = new Promise(resolveStop => child.once('exit', resolveStop));
|
|
85
|
+
child.kill('SIGTERM');
|
|
86
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), 10_000);
|
|
87
|
+
try { await stopped; } finally { clearTimeout(timer); }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function login(launchUrl, hostname) {
|
|
91
|
+
const url = new URL(launchUrl);
|
|
92
|
+
url.hostname = hostname;
|
|
93
|
+
const response = await request(url);
|
|
94
|
+
assert.equal(response.status, 303, 'Harness must exchange its launch token for a browser cookie');
|
|
95
|
+
const cookie = response.headers['set-cookie']?.map(value => value.split(';', 1)[0]).join('; ');
|
|
96
|
+
assert.ok(cookie, 'Harness did not issue a browser cookie');
|
|
97
|
+
return { origin: url.origin, cookie };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function rpc(browser, channel = 'feishu', method = 'connection.status', headers = {}, payload = {}) {
|
|
101
|
+
return request(new URL(`/api/dsh-im/${channel}`, browser.origin), {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'content-type': 'application/json', origin: browser.origin,
|
|
104
|
+
...(browser.cookie ? { cookie: browser.cookie } : {}), ...headers },
|
|
105
|
+
body: JSON.stringify({ type: 'client-request', rpcId: 'lan-test',
|
|
106
|
+
method: `dsh-im/${channel}`, payload: { method, payload } }),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function expectStatus(name, response, expected, businessOk = false) {
|
|
111
|
+
assert.equal(response.status, expected, `${name}: ${response.body.slice(0, 300)}`);
|
|
112
|
+
if (businessOk) {
|
|
113
|
+
const envelope = JSON.parse(response.body);
|
|
114
|
+
assert.equal(envelope.type, 'server-response', name);
|
|
115
|
+
assert.equal(envelope.rpcId, 'lan-test', name);
|
|
116
|
+
assert.equal(envelope.result.ok, true, `${name}: ${response.body.slice(0, 300)}`);
|
|
117
|
+
}
|
|
118
|
+
results.push({ check: name, status: response.status, result: 'PASS' });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await mkdir(profile, { recursive: true });
|
|
123
|
+
const packages = {
|
|
124
|
+
'@deepseek-ai/dsh-base': resolve(harnessRoot, 'packages/bundle/base'),
|
|
125
|
+
'@deepseek-ai/dsh-web-app': resolve(harnessRoot, 'packages/bundle/web-app'),
|
|
126
|
+
'@xmanrui/dsh-im': pluginRoot,
|
|
127
|
+
};
|
|
128
|
+
for (const [name, path] of Object.entries(packages)) {
|
|
129
|
+
const target = join(profile, 'node_modules', name);
|
|
130
|
+
await mkdir(dirname(target), { recursive: true });
|
|
131
|
+
await symlink(path, target, 'dir');
|
|
132
|
+
}
|
|
133
|
+
await writeFile(join(profile, 'cordis.yml'), '[]\n');
|
|
134
|
+
await writeFile(join(profile, 'package.json'), JSON.stringify({
|
|
135
|
+
name: 'dsh-im-lan-test', private: true,
|
|
136
|
+
dependencies: Object.fromEntries(Object.entries(packages).map(([name, path]) => [name, `link:${path}`])),
|
|
137
|
+
dsh: { profile: { bundles: Object.keys(packages) } },
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
const launchUrl = await start();
|
|
141
|
+
const lanUrl = new URL(launchUrl);
|
|
142
|
+
lanUrl.hostname = lanIp;
|
|
143
|
+
const anonymous = { origin: lanUrl.origin };
|
|
144
|
+
expectStatus('LAN without login', await rpc(anonymous), 401);
|
|
145
|
+
const lan = await login(launchUrl, lanIp);
|
|
146
|
+
expectStatus('LAN authenticated web page', await request(new URL('/', lan.origin), {
|
|
147
|
+
headers: { cookie: lan.cookie },
|
|
148
|
+
}), 200);
|
|
149
|
+
for (const channel of ['feishu', 'weixin', 'dingtalk', 'wecom', 'wecom-app', 'qq',
|
|
150
|
+
'slack', 'telegram', 'discord', 'whatsapp', 'imessage', 'office']) {
|
|
151
|
+
expectStatus(`LAN default: ${channel}`, await rpc(lan, channel), 200, true);
|
|
152
|
+
}
|
|
153
|
+
const delivery = await rpc(lan, 'dsh-im-delivery', 'target.list', {}, { botId: 'bot_missing' });
|
|
154
|
+
expectStatus('LAN delivery reaches business handler', delivery, 200);
|
|
155
|
+
assert.equal(JSON.parse(delivery.body).result.error.code, 'unknown-bot');
|
|
156
|
+
expectStatus('LAN forged cookie', await rpc({ ...lan, cookie: 'invalid=invalid' }), 401);
|
|
157
|
+
expectStatus('Untrusted Host', await rpc(lan, 'feishu', 'connection.status', { host: 'untrusted.invalid' }), 403);
|
|
158
|
+
expectStatus('Cross-origin request', await rpc(lan, 'feishu', 'connection.status', { origin: 'https://untrusted.invalid' }), 403);
|
|
159
|
+
expectStatus('LAN update remains local-only', await rpc(lan, 'dsh-im', 'update.status'), 403);
|
|
160
|
+
expectStatus('LAN TTL remains local-only', await rpc(lan, 'dsh-im-settings', 'settings.inbound-ttl.get'), 403);
|
|
161
|
+
const local = await login(launchUrl, '127.0.0.1');
|
|
162
|
+
expectStatus('Loopback default: feishu', await rpc(local), 200, true);
|
|
163
|
+
|
|
164
|
+
await stop();
|
|
165
|
+
await writeFile(join(profile, 'cordis.patch.yml'), '- id: xmanrui-dsh-im\n config:\n rpcAuthority: loopback\n');
|
|
166
|
+
const restrictedUrl = await start();
|
|
167
|
+
expectStatus('Explicit loopback rejects LAN', await rpc(await login(restrictedUrl, lanIp)), 403);
|
|
168
|
+
expectStatus('Explicit loopback accepts localhost', await rpc(await login(restrictedUrl, '127.0.0.1')), 200, true);
|
|
169
|
+
console.table(results);
|
|
170
|
+
console.log(`Passed ${results.length} real HTTP checks using LAN Host/Origin ${lanIp}.`);
|
|
171
|
+
console.log('TCP connections stayed on loopback. This checks the original CLI, authentication and built plugin, not a second-device browser.');
|
|
172
|
+
console.log('The temporary profile contains no bot credentials and is removed after the server stops.');
|
|
173
|
+
} finally {
|
|
174
|
+
await stop();
|
|
175
|
+
await rm(directory, { recursive: true, force: true });
|
|
176
|
+
}
|