@xmanrui/dsh-im 1.1.0 → 1.3.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 +5 -3
- package/README.md +5 -3
- package/lib/client.js +212 -0
- package/lib/index.js +166 -163
- package/package.json +1 -1
- package/plugin-src/client/channels/whatsapp/api.js +11 -0
- package/plugin-src/client/channels/whatsapp/index.js +125 -0
- package/plugin-src/client/channels/whatsapp/styles.js +25 -0
- package/plugin-src/client/i18n.js +18 -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/weixin/production.mjs +3 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +3 -1
- package/plugin-src/host/channels/whatsapp/rpc.mjs +19 -1
- package/plugin-src/host/harness-session-coordinator.mjs +32 -5
- package/src/channels/dingtalk/dingtalk-api.mjs +93 -27
- package/src/channels/dingtalk/dingtalk-bridge.mjs +60 -13
- package/src/channels/discord/discord-runtime.mjs +23 -0
- package/src/channels/feishu/bridge.mjs +18 -10
- package/src/channels/feishu/message-utils.mjs +47 -0
- package/src/channels/qq/qq-bridge.mjs +80 -28
- package/src/channels/shared/file-download.mjs +64 -0
- package/src/channels/shared/harness-client.mjs +45 -0
- package/src/channels/shared/inbound-file.mjs +206 -0
- package/src/channels/shared/text-harness-bridge.mjs +31 -11
- package/src/channels/slack/slack-api.mjs +27 -4
- package/src/channels/slack/slack-runtime.mjs +55 -5
- package/src/channels/telegram/telegram-api.mjs +21 -6
- package/src/channels/telegram/telegram-runtime.mjs +24 -3
- package/src/channels/wecom/wecom-bridge.mjs +73 -10
- package/src/channels/weixin/weixin-api.mjs +45 -0
- package/src/channels/weixin/weixin-bridge.mjs +52 -18
- package/src/channels/whatsapp/config-store.mjs +43 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +22 -1
- package/src/channels/whatsapp/whatsapp-runtime.mjs +83 -2
- package/src/channels/whatsapp/whatsapp-web-session.mjs +1 -1
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|
|
9
9
|
cancelProvisioning: 'provision.cancel',
|
|
10
10
|
reconnectBot: 'bot.reconnect',
|
|
11
11
|
deleteBot: 'bot.delete',
|
|
12
|
+
setAccessPolicy: 'bot.access-policy.set',
|
|
12
13
|
setWorkspace: 'bot.workspace.set',
|
|
13
14
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
14
15
|
});
|
|
@@ -86,6 +87,16 @@ function normalizeBot(value) {
|
|
|
86
87
|
state: connected ? 'connected' : state,
|
|
87
88
|
workspace: text(value.workspace, '', 4_096),
|
|
88
89
|
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
|
90
|
+
accessPolicy: {
|
|
91
|
+
accessMode: ['self-only', 'private-allowlist', 'open'].includes(
|
|
92
|
+
value.accessPolicy?.accessMode,
|
|
93
|
+
) ? value.accessPolicy.accessMode : 'self-only',
|
|
94
|
+
allowedNumbers: Array.isArray(value.accessPolicy?.allowedNumbers)
|
|
95
|
+
? [...new Set(value.accessPolicy.allowedNumbers.filter((entry) => (
|
|
96
|
+
typeof entry === 'string' && /^[1-9]\d{4,14}$/.test(entry)
|
|
97
|
+
)))]
|
|
98
|
+
: [],
|
|
99
|
+
},
|
|
89
100
|
bot: {
|
|
90
101
|
name: text(value.bot?.name, 'WhatsApp机器人', 100),
|
|
91
102
|
idMasked: text(value.bot?.idMasked, 'WhatsApp账号', 140),
|
|
@@ -25,6 +25,119 @@ import { installWhatsappStyles } from './styles.js';
|
|
|
25
25
|
|
|
26
26
|
const ACTIVE_STATES = new Set(['pending', 'connecting']);
|
|
27
27
|
|
|
28
|
+
function accessPolicyFor(account) {
|
|
29
|
+
const accessMode = ['self-only', 'private-allowlist', 'open'].includes(
|
|
30
|
+
account?.accessPolicy?.accessMode,
|
|
31
|
+
) ? account.accessPolicy.accessMode : 'self-only';
|
|
32
|
+
return {
|
|
33
|
+
accessMode,
|
|
34
|
+
allowedNumbers: Array.isArray(account?.accessPolicy?.allowedNumbers)
|
|
35
|
+
? account.accessPolicy.allowedNumbers : [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function allowedNumbersFromText(value) {
|
|
40
|
+
const entries = value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean);
|
|
41
|
+
const normalized = entries.map((entry) => entry.replace(/^\+/, ''));
|
|
42
|
+
if (normalized.some((entry) => !/^[1-9]\d{4,14}$/.test(entry))) {
|
|
43
|
+
throw new TypeError('电话号码必须包含国家或地区代码,每行一个。');
|
|
44
|
+
}
|
|
45
|
+
return [...new Set(normalized)];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function WhatsappAccessSettings({ account, busy = false, onSave }) {
|
|
49
|
+
const policy = accessPolicyFor(account);
|
|
50
|
+
const sourceNumbers = policy.allowedNumbers.join('\n');
|
|
51
|
+
const helpId = React.useId();
|
|
52
|
+
const [accessMode, setAccessMode] = React.useState(policy.accessMode);
|
|
53
|
+
const [allowedNumbers, setAllowedNumbers] = React.useState(sourceNumbers);
|
|
54
|
+
const [error, setError] = React.useState(null);
|
|
55
|
+
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
setAccessMode(policy.accessMode);
|
|
58
|
+
setAllowedNumbers(sourceNumbers);
|
|
59
|
+
setError(null);
|
|
60
|
+
}, [policy.accessMode, sourceNumbers]);
|
|
61
|
+
|
|
62
|
+
const save = async (event) => {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
setError(null);
|
|
65
|
+
try {
|
|
66
|
+
const normalized = allowedNumbersFromText(allowedNumbers);
|
|
67
|
+
if (typeof onSave !== 'function') throw new Error('WhatsApp 访问设置暂不可用。');
|
|
68
|
+
await onSave({ accessMode, allowedNumbers: normalized });
|
|
69
|
+
} catch (caught) {
|
|
70
|
+
setError(caught?.message ?? 'WhatsApp 访问设置保存失败。');
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const allowlistEnabled = accessMode === 'private-allowlist';
|
|
75
|
+
const labels = {
|
|
76
|
+
'self-only': '仅自己模式',
|
|
77
|
+
'private-allowlist': '指定联系人模式',
|
|
78
|
+
open: '开放响应模式',
|
|
79
|
+
};
|
|
80
|
+
return h('form', { className: 'dwa-access', onSubmit: save },
|
|
81
|
+
h('div', { className: 'dwa-accessHeading' },
|
|
82
|
+
h('strong', null, '访问设置'),
|
|
83
|
+
h('span', { className: 'dwa-accessStatus' },
|
|
84
|
+
h('span', { className: 'dwa-accessBadge', 'data-mode': policy.accessMode },
|
|
85
|
+
['已生效:', labels[policy.accessMode]]),
|
|
86
|
+
h('span', { className: 'dwa-accessHelp' },
|
|
87
|
+
h('button', {
|
|
88
|
+
type: 'button',
|
|
89
|
+
className: 'dwa-accessHelpButton',
|
|
90
|
+
'aria-label': '查看 WhatsApp 访问模式说明',
|
|
91
|
+
'aria-describedby': helpId,
|
|
92
|
+
}, h('span', { 'aria-hidden': 'true' }, '?')),
|
|
93
|
+
h('span', { id: helpId, className: 'dwa-accessTooltip', role: 'tooltip' },
|
|
94
|
+
h('span', { className: 'dwa-accessTooltipItem' },
|
|
95
|
+
h('strong', null, '仅自己模式'),
|
|
96
|
+
h('span', null, '只响应已绑定 WhatsApp 账号的自聊消息。')),
|
|
97
|
+
h('span', { className: 'dwa-accessTooltipItem' },
|
|
98
|
+
h('strong', null, '指定联系人模式'),
|
|
99
|
+
h('span', null, '响应自聊和白名单联系人的私聊,忽略群聊。')),
|
|
100
|
+
h('span', { className: 'dwa-accessTooltipItem' },
|
|
101
|
+
h('strong', null, '开放响应模式'),
|
|
102
|
+
h('span', null, '响应所有私聊,以及群聊中的提及或回复。')))))),
|
|
103
|
+
h('label', { className: 'dwa-accessField' },
|
|
104
|
+
h('span', null, '模式'),
|
|
105
|
+
h('select', {
|
|
106
|
+
value: accessMode,
|
|
107
|
+
disabled: busy,
|
|
108
|
+
'aria-label': 'WhatsApp 访问模式',
|
|
109
|
+
onChange: (event) => { setAccessMode(event.target.value); setError(null); },
|
|
110
|
+
},
|
|
111
|
+
h('option', { value: 'self-only' }, '仅自己模式(默认)'),
|
|
112
|
+
h('option', { value: 'private-allowlist' }, '指定联系人模式'),
|
|
113
|
+
h('option', { value: 'open' }, '开放响应模式'))),
|
|
114
|
+
allowlistEnabled
|
|
115
|
+
? h('label', { className: 'dwa-accessField' },
|
|
116
|
+
h('span', null, '允许私聊的 WhatsApp 电话号码'),
|
|
117
|
+
h('textarea', {
|
|
118
|
+
value: allowedNumbers,
|
|
119
|
+
disabled: busy,
|
|
120
|
+
rows: 3,
|
|
121
|
+
placeholder: '每行一个含国家或地区代码的号码',
|
|
122
|
+
'aria-label': '允许私聊的 WhatsApp 电话号码',
|
|
123
|
+
onChange: (event) => { setAllowedNumbers(event.target.value); setError(null); },
|
|
124
|
+
}),
|
|
125
|
+
h('small', null, '可以包含开头的 +,保存时会自动移除。'))
|
|
126
|
+
: null,
|
|
127
|
+
allowlistEnabled && allowedNumbers.trim() === ''
|
|
128
|
+
? h('p', { className: 'dwa-accessWarning', role: 'status' },
|
|
129
|
+
'白名单为空;保存后将只接受自聊消息。')
|
|
130
|
+
: null,
|
|
131
|
+
error ? h('p', { className: 'dwa-accessError', role: 'alert' }, error) : null,
|
|
132
|
+
h('div', { className: 'dwa-accessActions' },
|
|
133
|
+
h('button', {
|
|
134
|
+
type: 'submit',
|
|
135
|
+
className: 'ddt-button',
|
|
136
|
+
'data-kind': 'secondary',
|
|
137
|
+
disabled: busy,
|
|
138
|
+
}, busy ? '正在保存…' : '保存访问设置')));
|
|
139
|
+
}
|
|
140
|
+
|
|
28
141
|
const Button = React.forwardRef(function Button(
|
|
29
142
|
{ children, kind = 'secondary', className = '', ...props },
|
|
30
143
|
ref,
|
|
@@ -180,6 +293,7 @@ export function WhatsappAccountCard({
|
|
|
180
293
|
onReconnect,
|
|
181
294
|
onWorkspaceSave,
|
|
182
295
|
onAgentPresetSave,
|
|
296
|
+
onAccessPolicySave,
|
|
183
297
|
onRequestRemove,
|
|
184
298
|
onConfirmRemove,
|
|
185
299
|
onCancelRemove,
|
|
@@ -216,6 +330,11 @@ export function WhatsappAccountCard({
|
|
|
216
330
|
disabled: Boolean(busy),
|
|
217
331
|
onSave: onAgentPresetSave,
|
|
218
332
|
}),
|
|
333
|
+
h(WhatsappAccessSettings, {
|
|
334
|
+
account,
|
|
335
|
+
busy: Boolean(busy),
|
|
336
|
+
onSave: onAccessPolicySave,
|
|
337
|
+
}),
|
|
219
338
|
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
|
220
339
|
h('div', { className: 'dim-cardFooterLayout' },
|
|
221
340
|
h('div', { className: 'ddt-actions dim-cardActions' },
|
|
@@ -469,6 +588,12 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
|
|
469
588
|
WHATSAPP_ENDPOINTS.setAgentPreset,
|
|
470
589
|
{ botId: account.botId, agentPreset },
|
|
471
590
|
),
|
|
591
|
+
onAccessPolicySave: (accessPolicy) => botAction(
|
|
592
|
+
account,
|
|
593
|
+
'access',
|
|
594
|
+
WHATSAPP_ENDPOINTS.setAccessPolicy,
|
|
595
|
+
{ botId: account.botId, ...accessPolicy },
|
|
596
|
+
),
|
|
472
597
|
onRequestRemove: () => setRemoveTarget(account.botId),
|
|
473
598
|
onCancelRemove: () => setRemoveTarget(null),
|
|
474
599
|
onConfirmRemove: async () => {
|
|
@@ -4,6 +4,31 @@ const CSS = String.raw`
|
|
|
4
4
|
.dwa-page { --ddt-accent: #25d366; --ddt-accent-deep: #128c7e; --ddt-accent-wash: #eafbf0; }
|
|
5
5
|
.dwa-avatar { color: #fff; background: #25d366; }
|
|
6
6
|
.dwa-avatar svg { display: block; }
|
|
7
|
+
.dwa-access { display: grid; gap: 10px; padding: 12px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 10px; background: var(--dsw-alias-bg-layer-2, #f7f8fa); }
|
|
8
|
+
.dwa-accessHeading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
9
|
+
.dwa-accessHeading > strong { font-size: 13px; }
|
|
10
|
+
.dwa-accessStatus { min-width: 0; display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; }
|
|
11
|
+
.dwa-accessBadge { flex: none; padding: 3px 8px; border-radius: 999px; color: #08785f; background: #eafbf0; font-size: 11px; font-weight: 700; }
|
|
12
|
+
.dwa-accessBadge[data-mode="private-allowlist"] { color: #0f6f8f; background: #eaf7fd; }
|
|
13
|
+
.dwa-accessBadge[data-mode="open"] { color: #a15c00; background: #fff3d6; }
|
|
14
|
+
.dwa-accessHelp { position: relative; display: inline-flex; flex: none; }
|
|
15
|
+
.dwa-accessHelpButton { width: 20px; height: 20px; display: grid; place-items: center; padding: 0; border: 1px solid color-mix(in srgb, #25d366 34%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 50%; color: #128c7e; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; line-height: 1; font-weight: 750; cursor: help; }
|
|
16
|
+
.dwa-accessTooltip { position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; width: 270px; max-width: min(290px, calc(100vw - 48px)); display: grid; gap: 8px; padding: 10px 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 9px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 10px 28px rgb(31 35 41 / 16%); opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
|
|
17
|
+
.dwa-accessTooltipItem { display: grid; gap: 2px; }
|
|
18
|
+
.dwa-accessTooltipItem + .dwa-accessTooltipItem { padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l2, #eef0f3); }
|
|
19
|
+
.dwa-accessTooltipItem strong { font-size: 12px; line-height: 17px; }
|
|
20
|
+
.dwa-accessTooltipItem > span { color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 16px; }
|
|
21
|
+
.dwa-accessHelp:hover .dwa-accessTooltip, .dwa-accessHelp:focus-within .dwa-accessTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
|
|
22
|
+
.dwa-accessField { display: grid; gap: 5px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; font-weight: 600; }
|
|
23
|
+
.dwa-accessField select, .dwa-accessField textarea { width: 100%; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l1, #c9cdd4); border-radius: 7px; color: inherit; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-weight: 400; }
|
|
24
|
+
.dwa-accessField select { height: 34px; padding: 0 9px; }
|
|
25
|
+
.dwa-accessField textarea { min-height: 68px; padding: 8px 9px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
26
|
+
.dwa-accessField textarea:disabled { color: var(--dsw-alias-label-tertiary, #8f959e); background: var(--dsw-alias-bg-module-platform, #f2f3f5); cursor: not-allowed; resize: none; opacity: 1; }
|
|
27
|
+
.dwa-accessField small { color: var(--dsw-alias-label-secondary, #646a73); font-weight: 400; }
|
|
28
|
+
.dwa-accessWarning, .dwa-accessError { margin: 0; font-size: 12px; line-height: 1.5; }
|
|
29
|
+
.dwa-accessWarning { color: #a15c00; }
|
|
30
|
+
.dwa-accessError { color: var(--dsw-alias-state-error-primary, #d83931); }
|
|
31
|
+
.dwa-accessActions { display: flex; justify-content: flex-end; }
|
|
7
32
|
`;
|
|
8
33
|
|
|
9
34
|
export function installWhatsappStyles() {
|
|
@@ -360,6 +360,24 @@ const EN = Object.freeze({
|
|
|
360
360
|
'正在建立安全的关联设备会话。': 'Creating a secure linked-device session.',
|
|
361
361
|
'关联设备正在接入 DeepSeek Harness。': 'Linking the device to DeepSeek Harness.',
|
|
362
362
|
'WhatsApp Web 关联设备运行正常': 'WhatsApp linked device is healthy',
|
|
363
|
+
'查看 WhatsApp 访问模式说明': 'View WhatsApp access mode details',
|
|
364
|
+
'WhatsApp 访问模式': 'WhatsApp access mode',
|
|
365
|
+
'仅自己模式': 'Only me',
|
|
366
|
+
'指定联系人模式': 'Selected contacts',
|
|
367
|
+
'开放响应模式': 'Open responses',
|
|
368
|
+
'仅自己模式(默认)': 'Only me (default)',
|
|
369
|
+
'已生效:': 'Active: ',
|
|
370
|
+
'只响应已绑定 WhatsApp 账号的自聊消息。': 'Only respond to self-chat messages from the linked WhatsApp account.',
|
|
371
|
+
'响应自聊和白名单联系人的私聊,忽略群聊。': 'Respond to self-chat and allowlisted direct messages; ignore group messages.',
|
|
372
|
+
'响应所有私聊,以及群聊中的提及或回复。': 'Respond to all direct messages and to group mentions or replies.',
|
|
373
|
+
'允许私聊的 WhatsApp 电话号码': 'WhatsApp phone numbers allowed to send direct messages',
|
|
374
|
+
'每行一个含国家或地区代码的号码': 'One number with country or region code per line',
|
|
375
|
+
'可以包含开头的 +,保存时会自动移除。': 'A leading + is allowed and removed when saved.',
|
|
376
|
+
'仅指定联系人模式使用白名单,切换模式时会保留。': 'Only Selected contacts uses the allowlist; it is retained when modes change.',
|
|
377
|
+
'白名单为空;保存后将只接受自聊消息。': 'The allowlist is empty; only self-chat messages will be accepted after saving.',
|
|
378
|
+
'电话号码必须包含国家或地区代码,每行一个。': 'Each phone number must include a country or region code on its own line.',
|
|
379
|
+
'WhatsApp 访问设置暂不可用。': 'WhatsApp access settings are currently unavailable.',
|
|
380
|
+
'WhatsApp 访问设置保存失败。': 'Could not save WhatsApp access settings.',
|
|
363
381
|
'Bot API 长轮询': 'Bot API long polling',
|
|
364
382
|
' Gateway 长连接': ' Gateway persistent connection',
|
|
365
383
|
'Gateway 长连接': 'Gateway persistent connection',
|
|
@@ -86,9 +86,10 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
86
86
|
return state;
|
|
87
87
|
};
|
|
88
88
|
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
89
|
-
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
|
|
89
|
+
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
|
|
90
90
|
controlExecutor: internals.controlExecutor,
|
|
91
91
|
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
92
|
+
fileIngressExecutor: internals.fileIngressExecutor,
|
|
92
93
|
});
|
|
93
94
|
const harness = new Harness({
|
|
94
95
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
@@ -98,6 +99,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
98
99
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
99
100
|
...(controlExecutor ? { controlExecutor } : {}),
|
|
100
101
|
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
102
|
+
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
101
103
|
});
|
|
102
104
|
const coreController = new Controller({
|
|
103
105
|
deviceAuth,
|
|
@@ -133,9 +133,10 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
133
133
|
return stateFor(botConfig);
|
|
134
134
|
};
|
|
135
135
|
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
136
|
-
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
|
|
136
|
+
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
|
|
137
137
|
controlExecutor: internals.controlExecutor,
|
|
138
138
|
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
139
|
+
fileIngressExecutor: internals.fileIngressExecutor,
|
|
139
140
|
});
|
|
140
141
|
const harness = new Harness({
|
|
141
142
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
@@ -147,6 +148,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
147
148
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
148
149
|
...(controlExecutor ? { controlExecutor } : {}),
|
|
149
150
|
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
151
|
+
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
150
152
|
});
|
|
151
153
|
const proxyEnv = internals.proxyEnv ?? process.env;
|
|
152
154
|
const wsAgent = createFeishuWebSocketAgent(proxyEnv, internals.createProxyAgent);
|
|
@@ -77,9 +77,10 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
77
77
|
return state;
|
|
78
78
|
};
|
|
79
79
|
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
80
|
-
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
|
|
80
|
+
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
|
|
81
81
|
controlExecutor: internals.controlExecutor,
|
|
82
82
|
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
83
|
+
fileIngressExecutor: internals.fileIngressExecutor,
|
|
83
84
|
});
|
|
84
85
|
const harness = new Harness({
|
|
85
86
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
@@ -89,6 +90,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
89
90
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
90
91
|
...(controlExecutor ? { controlExecutor } : {}),
|
|
91
92
|
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
93
|
+
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
92
94
|
});
|
|
93
95
|
const coreController = new Controller({
|
|
94
96
|
qrAuth,
|
|
@@ -78,9 +78,10 @@ export async function createTokenProductionController(ctx, config, internals, de
|
|
|
78
78
|
return state;
|
|
79
79
|
};
|
|
80
80
|
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
81
|
-
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
|
|
81
|
+
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
|
|
82
82
|
controlExecutor: internals.controlExecutor,
|
|
83
83
|
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
84
|
+
fileIngressExecutor: internals.fileIngressExecutor,
|
|
84
85
|
});
|
|
85
86
|
const harness = new ResolvedHarness({
|
|
86
87
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
@@ -90,6 +91,7 @@ export async function createTokenProductionController(ctx, config, internals, de
|
|
|
90
91
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
91
92
|
...(controlExecutor ? { controlExecutor } : {}),
|
|
92
93
|
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
94
|
+
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
93
95
|
});
|
|
94
96
|
const coreController = new ResolvedController({
|
|
95
97
|
credentials: ctx.credentials,
|
|
@@ -56,9 +56,10 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
56
56
|
return state;
|
|
57
57
|
};
|
|
58
58
|
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
59
|
-
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
|
|
59
|
+
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
|
|
60
60
|
controlExecutor: internals.controlExecutor,
|
|
61
61
|
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
62
|
+
fileIngressExecutor: internals.fileIngressExecutor,
|
|
62
63
|
});
|
|
63
64
|
const harness = new ResolvedHarness({
|
|
64
65
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
@@ -68,6 +69,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
68
69
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
69
70
|
...(controlExecutor ? { controlExecutor } : {}),
|
|
70
71
|
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
72
|
+
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
71
73
|
});
|
|
72
74
|
const coreController = new ResolvedController({
|
|
73
75
|
credentials: ctx.credentials,
|
|
@@ -80,9 +80,10 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
80
80
|
return state;
|
|
81
81
|
};
|
|
82
82
|
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
83
|
-
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
|
|
83
|
+
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
|
|
84
84
|
controlExecutor: internals.controlExecutor,
|
|
85
85
|
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
86
|
+
fileIngressExecutor: internals.fileIngressExecutor,
|
|
86
87
|
});
|
|
87
88
|
const harness = new Harness({
|
|
88
89
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
@@ -92,6 +93,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
92
93
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
93
94
|
...(controlExecutor ? { controlExecutor } : {}),
|
|
94
95
|
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
96
|
+
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
95
97
|
});
|
|
96
98
|
const coreController = new Controller({
|
|
97
99
|
qrAuth,
|
|
@@ -80,9 +80,10 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
80
80
|
return state;
|
|
81
81
|
};
|
|
82
82
|
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
83
|
-
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
|
|
83
|
+
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
|
|
84
84
|
controlExecutor: internals.controlExecutor,
|
|
85
85
|
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
86
|
+
fileIngressExecutor: internals.fileIngressExecutor,
|
|
86
87
|
});
|
|
87
88
|
const harness = new Harness({
|
|
88
89
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
@@ -92,6 +93,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
92
93
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
93
94
|
...(controlExecutor ? { controlExecutor } : {}),
|
|
94
95
|
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
96
|
+
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
95
97
|
});
|
|
96
98
|
const coreController = new Controller({
|
|
97
99
|
api,
|
|
@@ -83,9 +83,10 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
83
83
|
return state;
|
|
84
84
|
};
|
|
85
85
|
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
86
|
-
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
|
|
86
|
+
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
|
|
87
87
|
controlExecutor: internals.controlExecutor,
|
|
88
88
|
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
89
|
+
fileIngressExecutor: internals.fileIngressExecutor,
|
|
89
90
|
});
|
|
90
91
|
const harness = new Harness({
|
|
91
92
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
@@ -95,6 +96,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
95
96
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
96
97
|
...(controlExecutor ? { controlExecutor } : {}),
|
|
97
98
|
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
99
|
+
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
98
100
|
});
|
|
99
101
|
const coreController = new Controller({
|
|
100
102
|
configStore: observedConfigStore,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import QRCode from 'qrcode';
|
|
2
2
|
|
|
3
3
|
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
|
4
|
+
import { normalizeWhatsappAccessPolicy } from '../../../../src/channels/whatsapp/config-store.mjs';
|
|
4
5
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
5
6
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
|
6
7
|
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
|
@@ -13,6 +14,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|
|
13
14
|
cancelProvisioning: 'provision.cancel',
|
|
14
15
|
reconnectBot: 'bot.reconnect',
|
|
15
16
|
deleteBot: 'bot.delete',
|
|
17
|
+
setAccessPolicy: 'bot.access-policy.set',
|
|
16
18
|
setWorkspace: SET_WORKSPACE_ENDPOINT,
|
|
17
19
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
18
20
|
});
|
|
@@ -50,6 +52,17 @@ function payloadFailure(endpoint, payload) {
|
|
|
50
52
|
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId)
|
|
51
53
|
&& payload.confirm === true ? null : 'bot.delete requires a botId and confirm=true.';
|
|
52
54
|
}
|
|
55
|
+
if (endpoint === WHATSAPP_ENDPOINTS.setAccessPolicy) {
|
|
56
|
+
if (!exactKeys(payload, ['botId', 'accessMode', 'allowedNumbers'])
|
|
57
|
+
|| Object.keys(payload).length !== 3
|
|
58
|
+
|| !validId(payload.botId)) return '请输入有效的 WhatsApp 访问模式和电话号码。';
|
|
59
|
+
try {
|
|
60
|
+
normalizeWhatsappAccessPolicy(payload);
|
|
61
|
+
return null;
|
|
62
|
+
} catch {
|
|
63
|
+
return '请输入有效的 WhatsApp 访问模式和电话号码。';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
53
66
|
if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
|
|
54
67
|
return validWorkspacePayload(payload)
|
|
55
68
|
? null : '请输入工作区绝对路径。';
|
|
@@ -92,7 +105,7 @@ async function publicStatus(value, encodeQr) {
|
|
|
92
105
|
}
|
|
93
106
|
|
|
94
107
|
export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
|
|
95
|
-
for (const method of ['status', 'startProvisioning', 'registrationStatus', 'cancelProvisioning', 'reconnectBot', 'deleteBot']) {
|
|
108
|
+
for (const method of ['status', 'startProvisioning', 'registrationStatus', 'cancelProvisioning', 'reconnectBot', 'deleteBot', 'setAccessPolicy']) {
|
|
96
109
|
if (typeof controller?.[method] !== 'function') {
|
|
97
110
|
throw new TypeError(`A complete WhatsApp controller is required (${method})`);
|
|
98
111
|
}
|
|
@@ -166,6 +179,11 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|
|
166
179
|
await controller.updateAgentPreset(payload.botId, payload.agentPreset),
|
|
167
180
|
cachedEncode,
|
|
168
181
|
);
|
|
182
|
+
} else if (endpoint === WHATSAPP_ENDPOINTS.setAccessPolicy) {
|
|
183
|
+
value = await publicStatus(
|
|
184
|
+
await controller.setAccessPolicy(payload.botId, normalizeWhatsappAccessPolicy(payload)),
|
|
185
|
+
cachedEncode,
|
|
186
|
+
);
|
|
169
187
|
} else {
|
|
170
188
|
value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
|
|
171
189
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
InboundFileError,
|
|
5
|
+
stageInboundFiles,
|
|
6
|
+
} from '../../src/channels/shared/inbound-file.mjs';
|
|
7
|
+
|
|
3
8
|
function agentsFromContext(ctx) {
|
|
4
9
|
if (typeof ctx?.get !== 'function') return undefined;
|
|
5
10
|
let agents;
|
|
@@ -108,13 +113,30 @@ function createSessionMaintenanceExecutor(agents) {
|
|
|
108
113
|
};
|
|
109
114
|
}
|
|
110
115
|
|
|
116
|
+
function createFileIngressExecutor(agents) {
|
|
117
|
+
return ({ sessionId, workspace, files, signal }) => {
|
|
118
|
+
const agent = agents.get(sessionId);
|
|
119
|
+
const attachedWorkspace = agent?.session?.header?.cwd;
|
|
120
|
+
const exactWorkspace = typeof attachedWorkspace === 'string' && attachedWorkspace
|
|
121
|
+
? attachedWorkspace
|
|
122
|
+
: workspace;
|
|
123
|
+
if (typeof exactWorkspace !== 'string' || !exactWorkspace) {
|
|
124
|
+
throw new InboundFileError(
|
|
125
|
+
'inbound-file-workspace-unavailable',
|
|
126
|
+
'The Harness Session workspace is unavailable for inbound files.',
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return stageInboundFiles({ files }, { workspace: exactWorkspace, signal });
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
111
133
|
/**
|
|
112
|
-
* Build optional same-process Session executors
|
|
113
|
-
*
|
|
114
|
-
*
|
|
134
|
+
* Build optional same-process Session executors. File ingress also works for
|
|
135
|
+
* cold Sessions by using the authoritative cwd returned by session.list;
|
|
136
|
+
* when an Agent is attached, its live Session header remains authoritative.
|
|
115
137
|
*/
|
|
116
138
|
export function createHarnessSessionExecutors(ctx, provided = {}) {
|
|
117
|
-
const { controlExecutor, sessionMaintenanceExecutor } = provided;
|
|
139
|
+
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = provided;
|
|
118
140
|
if (controlExecutor !== undefined && typeof controlExecutor !== 'function') {
|
|
119
141
|
throw new TypeError('controlExecutor must be a function');
|
|
120
142
|
}
|
|
@@ -122,13 +144,18 @@ export function createHarnessSessionExecutors(ctx, provided = {}) {
|
|
|
122
144
|
&& typeof sessionMaintenanceExecutor !== 'function') {
|
|
123
145
|
throw new TypeError('sessionMaintenanceExecutor must be a function');
|
|
124
146
|
}
|
|
147
|
+
if (fileIngressExecutor !== undefined && typeof fileIngressExecutor !== 'function') {
|
|
148
|
+
throw new TypeError('fileIngressExecutor must be a function');
|
|
149
|
+
}
|
|
125
150
|
|
|
126
|
-
const agents = controlExecutor && sessionMaintenanceExecutor
|
|
151
|
+
const agents = controlExecutor && sessionMaintenanceExecutor && fileIngressExecutor
|
|
127
152
|
? undefined
|
|
128
153
|
: agentsFromContext(ctx);
|
|
129
154
|
return {
|
|
130
155
|
controlExecutor: controlExecutor ?? (agents ? createControlExecutor(agents) : undefined),
|
|
131
156
|
sessionMaintenanceExecutor: sessionMaintenanceExecutor
|
|
132
157
|
?? (agents ? createSessionMaintenanceExecutor(agents) : undefined),
|
|
158
|
+
fileIngressExecutor: fileIngressExecutor
|
|
159
|
+
?? createFileIngressExecutor(agents ?? { get: () => undefined }),
|
|
133
160
|
};
|
|
134
161
|
}
|