@xmanrui/dsh-im 4.14.0 → 4.16.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 +10 -2
- package/README.md +17 -6
- package/lib/client.js +1980 -616
- package/lib/index.js +303 -293
- package/package.json +6 -2
- package/plugin-src/client/access-policy-settings.js +6 -0
- package/plugin-src/client/channels/feishu/index.js +16 -4
- package/plugin-src/client/channels/wecom-app/api.js +124 -0
- package/plugin-src/client/channels/wecom-app/index.js +612 -0
- package/plugin-src/client/channels/wecom-app/styles.js +25 -0
- package/plugin-src/client/delivery-settings.js +7 -0
- package/plugin-src/client/i18n.js +64 -1
- package/plugin-src/client/index.js +21 -0
- package/plugin-src/client/model-setting.js +135 -69
- package/plugin-src/client/session-channel-logos.js +241 -0
- package/plugin-src/client/styles.js +22 -7
- package/plugin-src/host/channels/dingtalk/index.mjs +8 -15
- package/plugin-src/host/channels/discord/index.mjs +8 -10
- package/plugin-src/host/channels/feishu/index.mjs +8 -15
- package/plugin-src/host/channels/office/index.mjs +9 -5
- package/plugin-src/host/channels/qq/index.mjs +8 -15
- package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
- package/plugin-src/host/channels/shared/model-setting-rpc.mjs +1 -2
- package/plugin-src/host/channels/shared/startup-error.mjs +53 -0
- package/plugin-src/host/channels/shared/startup.mjs +47 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
- package/plugin-src/host/channels/slack/index.mjs +8 -10
- package/plugin-src/host/channels/telegram/index.mjs +8 -10
- package/plugin-src/host/channels/wecom/index.mjs +8 -15
- package/plugin-src/host/channels/wecom/rpc.mjs +6 -1
- package/plugin-src/host/channels/wecom-app/index.mjs +33 -0
- package/plugin-src/host/channels/wecom-app/production.mjs +217 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +225 -0
- package/plugin-src/host/channels/weixin/index.mjs +8 -15
- package/plugin-src/host/channels/whatsapp/index.mjs +8 -15
- package/plugin-src/host/delivery-adapter.mjs +4 -0
- package/plugin-src/host/delivery-suggestions.mjs +4 -0
- package/plugin-src/host/index.mjs +21 -2
- package/plugin-src/host/session-title-prefix.mjs +122 -0
- package/scripts/verify-model-setting.mjs +54 -0
- package/scripts/verify-session-channel-logos.mjs +59 -0
- package/scripts/verify-session-title-prefix.mjs +149 -0
- package/src/channels/feishu/bridge.mjs +305 -12
- package/src/channels/feishu/feishu-cards.mjs +38 -0
- package/src/channels/feishu/feishu-channel.mjs +88 -20
- package/src/channels/feishu/plugin-controller.mjs +1 -0
- package/src/channels/feishu/repair-manager.mjs +3 -2
- package/src/channels/shared/bot-workspace-store.mjs +11 -3
- package/src/channels/shared/command-catalog.mjs +102 -0
- package/src/channels/shared/i18n-en/feishu.mjs +14 -2
- package/src/channels/shared/i18n-en/shared-a.mjs +3 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +9 -0
- package/src/channels/shared/i18n-en/wecom-app.mjs +33 -0
- package/src/channels/shared/i18n-en.mjs +2 -0
- package/src/channels/shared/model-setting.mjs +43 -8
- package/src/channels/shared/session-channel-labels.mjs +26 -0
- package/src/channels/shared/text-harness-bridge.mjs +2 -26
- package/src/channels/telegram/telegram-api.mjs +18 -6
- package/src/channels/telegram/telegram-runtime.mjs +34 -32
- package/src/channels/wecom/send-error.mjs +31 -0
- package/src/channels/wecom/wecom-bridge.mjs +59 -17
- package/src/channels/wecom-app/callback-server.mjs +471 -0
- package/src/channels/wecom-app/config-store.mjs +240 -0
- package/src/channels/wecom-app/harness-client.mjs +11 -0
- package/src/channels/wecom-app/state-store.mjs +107 -0
- package/src/channels/wecom-app/wecom-app-api.mjs +432 -0
- package/src/channels/wecom-app/wecom-app-bridge.mjs +1059 -0
- package/src/channels/wecom-app/wecom-app-controller.mjs +440 -0
- package/src/channels/wecom-app/wecom-app-runtime.mjs +221 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { parseSessionChannelTitle } from '../../src/channels/shared/session-channel-labels.mjs';
|
|
2
|
+
import {
|
|
3
|
+
WeixinLogoGlyph, FeishuLogoGlyph, DingtalkLogoGlyph, WecomLogoGlyph, QqLogoGlyph,
|
|
4
|
+
SlackLogoGlyph, TelegramLogoGlyph, DiscordLogoGlyph, WhatsappLogoGlyph, OfficeLogoGlyph,
|
|
5
|
+
} from './channel-logos.js';
|
|
6
|
+
|
|
7
|
+
const GLYPHS = Object.freeze({
|
|
8
|
+
weixin: [WeixinLogoGlyph, '#07c160'],
|
|
9
|
+
feishu: [FeishuLogoGlyph, '#3370ff'],
|
|
10
|
+
dingtalk: [DingtalkLogoGlyph, '#1677ff'],
|
|
11
|
+
wecom: [WecomLogoGlyph, '#3370ff'],
|
|
12
|
+
qq: [QqLogoGlyph, '#12b7f5'],
|
|
13
|
+
slack: [SlackLogoGlyph, '#e01e5a'],
|
|
14
|
+
telegram: [TelegramLogoGlyph, '#229ed9'],
|
|
15
|
+
discord: [DiscordLogoGlyph, '#5865f2'],
|
|
16
|
+
whatsapp: [WhatsappLogoGlyph, '#25d366'],
|
|
17
|
+
office: [OfficeLogoGlyph, '#f97316'],
|
|
18
|
+
});
|
|
19
|
+
const CHANNEL_ATTR = 'data-dsh-im-session-channel';
|
|
20
|
+
const TITLE_ATTR = 'data-dsh-im-session-text';
|
|
21
|
+
const MARKED = `[${CHANNEL_ATTR}]`;
|
|
22
|
+
const ROW = '[role="treeitem"][aria-selected]';
|
|
23
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
24
|
+
const installations = new WeakMap();
|
|
25
|
+
|
|
26
|
+
function hasClassPart(element, part) {
|
|
27
|
+
const match = new RegExp(`(?:^|[_-])${part}(?:$|[_-])`, 'u');
|
|
28
|
+
return [...element.classList].some((token) => match.test(token));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function titleOf(row) {
|
|
32
|
+
if (!row.matches(ROW) || row.closest('input, textarea, [contenteditable]:not([contenteditable="false"])')) return null;
|
|
33
|
+
let title;
|
|
34
|
+
if (row.tagName === 'DIV' && hasClassPart(row, 'sessionRow')) {
|
|
35
|
+
title = [...row.children].find((child) => child.tagName === 'SPAN' && hasClassPart(child, 'title'));
|
|
36
|
+
} else if (row.tagName === 'BUTTON' && hasClassPart(row, 'searchResultRow')) {
|
|
37
|
+
const heading = [...row.children].find((child) => hasClassPart(child, 'searchResultHeading'));
|
|
38
|
+
title = heading && [...heading.children].find((child) => child.tagName === 'SPAN' && hasClassPart(child, 'searchResultTitle'));
|
|
39
|
+
}
|
|
40
|
+
// Never cover inline rename inputs, highlighted search markup, or a future
|
|
41
|
+
// row structure that this adapter does not understand.
|
|
42
|
+
return title?.childNodes.length === 1 && title.firstChild.nodeType === 3 ? title : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Serialize the existing, static SVG glyphs without mounting React nodes. */
|
|
46
|
+
function logoUri(document, channel) {
|
|
47
|
+
const [Glyph, color] = GLYPHS[channel];
|
|
48
|
+
const render = (node) => {
|
|
49
|
+
if (!['svg', 'path'].includes(node.type)) throw new Error('Unsupported channel logo element');
|
|
50
|
+
const element = document.createElementNS(SVG_NS, node.type);
|
|
51
|
+
for (const [name, value] of Object.entries(node.props)) {
|
|
52
|
+
if (name === 'children') continue;
|
|
53
|
+
const attribute = name.startsWith('stroke')
|
|
54
|
+
? name.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`)
|
|
55
|
+
: name;
|
|
56
|
+
element.setAttribute(attribute, String(value));
|
|
57
|
+
}
|
|
58
|
+
for (const child of [node.props.children].flat().filter(Boolean)) element.appendChild(render(child));
|
|
59
|
+
return element;
|
|
60
|
+
};
|
|
61
|
+
const svg = render(Glyph({ size: 16 }));
|
|
62
|
+
svg.setAttribute('xmlns', SVG_NS);
|
|
63
|
+
svg.setAttribute('color', color);
|
|
64
|
+
return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createInstallation(document) {
|
|
68
|
+
const view = document.defaultView;
|
|
69
|
+
const ready = new Set();
|
|
70
|
+
const owned = new Map();
|
|
71
|
+
const queued = new Set();
|
|
72
|
+
const images = [];
|
|
73
|
+
let closed = false;
|
|
74
|
+
let scheduled = false;
|
|
75
|
+
const style = document.createElement('style');
|
|
76
|
+
style.dataset.pluginCss = 'dsh-im-session-channel-logos';
|
|
77
|
+
const uris = Object.fromEntries(Object.keys(GLYPHS).map((channel) => [channel, logoUri(document, channel)]));
|
|
78
|
+
style.textContent = `
|
|
79
|
+
${MARKED}[${TITLE_ATTR}] {
|
|
80
|
+
position: relative;
|
|
81
|
+
-webkit-text-fill-color: transparent;
|
|
82
|
+
text-overflow: clip !important;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
}
|
|
85
|
+
${MARKED}[${TITLE_ATTR}]::before {
|
|
86
|
+
content: "";
|
|
87
|
+
position: absolute;
|
|
88
|
+
inset-inline-start: 0;
|
|
89
|
+
top: 50%;
|
|
90
|
+
transform: translateY(-50%);
|
|
91
|
+
width: 16px;
|
|
92
|
+
height: 16px;
|
|
93
|
+
background: var(--dsh-im-session-logo) center / contain no-repeat;
|
|
94
|
+
text-indent: 0;
|
|
95
|
+
pointer-events: none;
|
|
96
|
+
}
|
|
97
|
+
${MARKED}[${TITLE_ATTR}]::after {
|
|
98
|
+
content: attr(${TITLE_ATTR}) / "";
|
|
99
|
+
position: absolute;
|
|
100
|
+
inset: 0;
|
|
101
|
+
inset-inline-start: 22px;
|
|
102
|
+
-webkit-text-fill-color: currentColor;
|
|
103
|
+
text-indent: 0;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
white-space: nowrap;
|
|
106
|
+
text-overflow: ellipsis;
|
|
107
|
+
pointer-events: none;
|
|
108
|
+
}
|
|
109
|
+
${Object.entries(uris).map(([channel, uri]) => `[${CHANNEL_ATTR}="${channel}"] { --dsh-im-session-logo: url("${uri}"); }`).join('\n')}
|
|
110
|
+
`;
|
|
111
|
+
document.head.appendChild(style);
|
|
112
|
+
|
|
113
|
+
const restore = (title) => {
|
|
114
|
+
const previous = owned.get(title);
|
|
115
|
+
if (!previous) return;
|
|
116
|
+
owned.delete(title);
|
|
117
|
+
for (const [attribute, value] of previous) {
|
|
118
|
+
if (value === null) title.removeAttribute(attribute);
|
|
119
|
+
else title.setAttribute(attribute, value);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const update = (row) => {
|
|
123
|
+
const title = row.isConnected ? titleOf(row) : null;
|
|
124
|
+
for (const marked of row.querySelectorAll(MARKED)) {
|
|
125
|
+
if (marked !== title) restore(marked);
|
|
126
|
+
}
|
|
127
|
+
if (!title) return;
|
|
128
|
+
const parsed = parseSessionChannelTitle(title.textContent);
|
|
129
|
+
if (!parsed || !ready.has(parsed.channel)) {
|
|
130
|
+
restore(title);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!owned.has(title)) {
|
|
134
|
+
owned.set(title, [CHANNEL_ATTR, TITLE_ATTR].map((attribute) => [attribute, title.getAttribute(attribute)]));
|
|
135
|
+
}
|
|
136
|
+
// React retains its original text node, content, row, and handlers. Only
|
|
137
|
+
// plugin-owned attributes drive the visual replacement via pseudo-elements.
|
|
138
|
+
title.setAttribute(TITLE_ATTR, parsed.title);
|
|
139
|
+
title.setAttribute(CHANNEL_ATTR, parsed.channel);
|
|
140
|
+
};
|
|
141
|
+
const schedule = () => {
|
|
142
|
+
if (closed || scheduled || !queued.size) return;
|
|
143
|
+
scheduled = true;
|
|
144
|
+
queueMicrotask(() => {
|
|
145
|
+
scheduled = false;
|
|
146
|
+
if (closed) return;
|
|
147
|
+
const rows = [...queued];
|
|
148
|
+
queued.clear();
|
|
149
|
+
for (const row of rows) {
|
|
150
|
+
try {
|
|
151
|
+
update(row);
|
|
152
|
+
} catch {
|
|
153
|
+
for (const marked of row.querySelectorAll(MARKED)) restore(marked);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
const collect = (node, descendants = false) => {
|
|
159
|
+
const element = node.nodeType === 1 ? node : node.parentElement;
|
|
160
|
+
if (!element) return;
|
|
161
|
+
const row = element.closest(ROW);
|
|
162
|
+
if (row) queued.add(row);
|
|
163
|
+
if (descendants) {
|
|
164
|
+
for (const child of element.querySelectorAll(ROW)) queued.add(child);
|
|
165
|
+
// Also restore an already decorated title moved outside a recognized row.
|
|
166
|
+
const marked = [...element.querySelectorAll(MARKED)];
|
|
167
|
+
if (owned.has(element)) marked.push(element);
|
|
168
|
+
for (const title of marked) {
|
|
169
|
+
const owner = title.closest(ROW);
|
|
170
|
+
if (owner) queued.add(owner);
|
|
171
|
+
else restore(title);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
schedule();
|
|
175
|
+
};
|
|
176
|
+
const observer = new view.MutationObserver((records) => {
|
|
177
|
+
if (closed) return;
|
|
178
|
+
for (const record of records) {
|
|
179
|
+
collect(record.target, record.type === 'attributes');
|
|
180
|
+
if (record.type !== 'childList') continue;
|
|
181
|
+
for (const node of record.addedNodes) collect(node, true);
|
|
182
|
+
for (const node of record.removedNodes) {
|
|
183
|
+
if (node.nodeType !== 1 || node.isConnected) continue;
|
|
184
|
+
if (owned.has(node)) restore(node);
|
|
185
|
+
for (const title of node.querySelectorAll(MARKED)) restore(title);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
observer.observe(document.body, {
|
|
190
|
+
subtree: true, childList: true, characterData: true,
|
|
191
|
+
attributes: true, attributeFilter: ['class', 'role', 'aria-selected', 'contenteditable'],
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// A CSP or an image-loading failure leaves that channel's readable text
|
|
195
|
+
// prefix in place. No title is hidden until its actual SVG has loaded.
|
|
196
|
+
for (const [channel, uri] of Object.entries(uris)) {
|
|
197
|
+
const image = new view.Image();
|
|
198
|
+
images.push(image);
|
|
199
|
+
image.onload = () => {
|
|
200
|
+
if (closed) return;
|
|
201
|
+
ready.add(channel);
|
|
202
|
+
collect(document.body, true);
|
|
203
|
+
};
|
|
204
|
+
image.src = uri;
|
|
205
|
+
}
|
|
206
|
+
return () => {
|
|
207
|
+
closed = true;
|
|
208
|
+
observer.disconnect();
|
|
209
|
+
queued.clear();
|
|
210
|
+
for (const image of images) image.onload = null;
|
|
211
|
+
for (const title of [...owned.keys()]) restore(title);
|
|
212
|
+
style.remove();
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Browser-only enhancement; unknown/unsupported surfaces keep text prefixes. */
|
|
217
|
+
export function installSessionChannelLogos(document = globalThis.document) {
|
|
218
|
+
const view = document?.defaultView;
|
|
219
|
+
if (!document?.body || !document?.head || !view?.MutationObserver || !view?.Image
|
|
220
|
+
|| !view.CSS?.supports?.('content', `attr(${TITLE_ATTR}) / ""`)
|
|
221
|
+
|| !view.CSS.supports('-webkit-text-fill-color', 'transparent')) return () => {};
|
|
222
|
+
let entry = installations.get(document);
|
|
223
|
+
if (!entry) {
|
|
224
|
+
try {
|
|
225
|
+
entry = { references: 0, close: createInstallation(document) };
|
|
226
|
+
} catch {
|
|
227
|
+
return () => {};
|
|
228
|
+
}
|
|
229
|
+
installations.set(document, entry);
|
|
230
|
+
}
|
|
231
|
+
entry.references += 1;
|
|
232
|
+
let released = false;
|
|
233
|
+
return () => {
|
|
234
|
+
if (released) return;
|
|
235
|
+
released = true;
|
|
236
|
+
entry.references -= 1;
|
|
237
|
+
if (entry.references > 0) return;
|
|
238
|
+
entry.close();
|
|
239
|
+
installations.delete(document);
|
|
240
|
+
};
|
|
241
|
+
}
|
|
@@ -71,9 +71,7 @@ const CSS = String.raw`
|
|
|
71
71
|
.dim-generalSettingsAction:hover .dim-generalSettingsTooltip, .dim-generalSettingsButton:focus-visible + .dim-generalSettingsTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
|
|
72
72
|
.dim-generalSettingsButton[aria-current="page"] + .dim-generalSettingsTooltip { opacity: 0; visibility: hidden; transform: translateY(-3px); }
|
|
73
73
|
.dim-layout { display: grid; grid-template-columns: 174px 1px minmax(0, 1fr); gap: 24px; align-items: start; }
|
|
74
|
-
.dim-rail {
|
|
75
|
-
.dim-rail::-webkit-scrollbar { width: 4px; }
|
|
76
|
-
.dim-rail::-webkit-scrollbar-thumb { border-radius: 99px; background: var(--dsw-alias-border-l2, #dfe1e5); }
|
|
74
|
+
.dim-rail { display: grid; align-content: start; gap: 8px; padding: 0 4px 1px 1px; }
|
|
77
75
|
.dim-channel { width: 100%; min-height: 48px; display: grid; grid-template-columns: 30px minmax(0, 1fr); align-items: center; gap: 10px; padding: 8px 12px; border: 1px solid var(--dsw-alias-border-l2, #eef0f3); border-radius: 14px; color: inherit; background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 2px 8px rgb(31 35 41 / 3%); font: inherit; text-align: left; cursor: pointer; transition: border-color .16s ease, background .16s ease, box-shadow .16s ease; }
|
|
78
76
|
.dim-channel:hover { border-color: color-mix(in srgb, var(--dim-blue) 25%, var(--dsw-alias-border-l2, #eef0f3)); background: color-mix(in srgb, var(--dim-blue) 2%, var(--dsw-alias-bg-layer-3, #fff)); box-shadow: 0 5px 16px rgb(31 35 41 / 5%); }
|
|
79
77
|
.dim-channel[aria-selected="true"] { border-color: color-mix(in srgb, var(--dim-blue) 43%, var(--dsw-alias-border-l2, #dfe1e5)); color: var(--dim-blue); background: color-mix(in srgb, var(--dim-blue) 12%, var(--dsw-alias-bg-layer-3, #fff)); box-shadow: 0 3px 12px rgb(51 112 255 / 7%); }
|
|
@@ -103,7 +101,7 @@ const CSS = String.raw`
|
|
|
103
101
|
.dim-channelCopy { min-width: 0; display: grid; }
|
|
104
102
|
.dim-channelCopy strong { overflow: hidden; color: inherit; font-size: 14px; line-height: 20px; font-weight: 680; text-overflow: ellipsis; white-space: nowrap; }
|
|
105
103
|
.dim-channelNote { overflow: hidden; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 10px; line-height: 13px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
|
106
|
-
.dim-divider { width: 1px;
|
|
104
|
+
.dim-divider { width: 1px; align-self: stretch; background: var(--dsw-alias-border-l1, #eef0f3); }
|
|
107
105
|
.dim-panel { min-width: 0; container-type: inline-size; }
|
|
108
106
|
.dim-loopbackRecovery { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 14px; padding: 14px 16px; border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #d97706) 30%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 12px; color: var(--dsw-alias-label-primary, #1f2329); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #d97706) 8%, var(--dsw-alias-bg-layer-1, #fff)); }
|
|
109
107
|
.dim-loopbackRecoveryCopy { min-width: 0; }
|
|
@@ -219,6 +217,24 @@ const CSS = String.raw`
|
|
|
219
217
|
.dim-panel .dim-presetStatus { grid-column: 2; grid-row: 1; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; white-space: nowrap; }
|
|
220
218
|
.dim-panel .dim-presetSelect { min-width: 0; max-width: 100%; grid-column: 1 / -1; grid-row: 2; height: 30px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; }
|
|
221
219
|
.dim-panel .dim-presetSelect:disabled { cursor: not-allowed; opacity: .55; }
|
|
220
|
+
.dim-panel .dim-modelSetting { display: block; padding: 8px 4px 4px; }
|
|
221
|
+
.dim-modelSetting > .dim-presetHeader { padding: 0 6px 6px; }
|
|
222
|
+
.dim-modelRow { display: flex; align-items: center; gap: 8px; width: 100%; min-height: 40px; padding: 6px 10px; border: 0; border-radius: 10px; background: transparent; color: var(--dsw-alias-label-primary, #1f2329); font: inherit; font-size: 14px; line-height: 22px; text-align: left; cursor: pointer; }
|
|
223
|
+
.dim-modelRowLabel { flex: none; }
|
|
224
|
+
.dim-modelValue { flex: 1; min-width: 0; overflow: hidden; color: var(--dsw-alias-label-tertiary, #81858c); text-overflow: ellipsis; white-space: nowrap; text-align: right; }
|
|
225
|
+
.dim-modelChevron { flex: none; color: var(--dsw-alias-label-tertiary, #81858c); }
|
|
226
|
+
.dim-modelRow:hover:not(:disabled), .dim-modelOption:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, #eceef1); }
|
|
227
|
+
.dim-modelRow:focus-visible, .dim-modelOption:focus-visible { outline: 2px solid var(--dsw-alias-border-l3, #1677ff); outline-offset: -2px; }
|
|
228
|
+
.dim-modelRow:disabled, .dim-modelOption:disabled { opacity: .55; cursor: default; }
|
|
229
|
+
/* In-flow lists avoid clipping by collapsed-card and settings scroll containers. */
|
|
230
|
+
.dim-modelMenu { max-height: 280px; overflow-y: auto; margin: 4px; padding: 4px; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 12px; background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3, #fff)); color: var(--dsw-alias-label-primary, #1f2329); scrollbar-width: thin; }
|
|
231
|
+
.dim-modelGroupTitle { padding: 7px 8px 3px; color: var(--dsw-alias-label-tertiary, #81858c); font-size: 12px; line-height: 18px; font-weight: 500; }
|
|
232
|
+
.dim-modelOption { display: flex; align-items: center; gap: 8px; width: 100%; min-height: 38px; padding: 6px 8px; border: 0; border-radius: 10px; background: transparent; color: inherit; text-align: left; font: inherit; cursor: pointer; }
|
|
233
|
+
.dim-modelOptionCopy { display: flex; flex: 1; min-width: 0; flex-direction: column; gap: 2px; }
|
|
234
|
+
.dim-modelOptionName { font-size: 14px; line-height: 20px; font-weight: 500; overflow-wrap: anywhere; }
|
|
235
|
+
.dim-modelDescription { color: var(--dsw-alias-label-tertiary, #81858c); font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
|
|
236
|
+
.dim-modelCheck { flex: 0 0 18px; text-align: center; }
|
|
237
|
+
.dim-modelHint { margin: 4px 10px 6px; color: var(--dsw-alias-label-tertiary, #81858c); font-size: 11px; line-height: 17px; }
|
|
222
238
|
.dim-panel .dim-presetError { grid-column: 1 / -1; grid-row: 3; margin: 0; color: var(--dsw-alias-state-error-primary, #d54941); font-size: 12px; line-height: 1.4; }
|
|
223
239
|
.dim-contextEntry { width: 100%; min-height: 40px; display: grid; grid-template-columns: 16px minmax(0, 1fr) max-content 16px; align-items: center; gap: 9px; margin: 10px 0; padding: 8px 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-module-platform, #f7f8fa); font: inherit; font-size: 13px; line-height: 20px; text-align: left; cursor: pointer; }
|
|
224
240
|
.dim-contextEntry:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, #eef0f3); }
|
|
@@ -406,7 +422,7 @@ const CSS = String.raw`
|
|
|
406
422
|
.dim-feishuGroupSettings { min-width: 0; display: grid; gap: 14px; }
|
|
407
423
|
.dim-feishuGroupControls { min-width: 0; display: grid; gap: 12px; }
|
|
408
424
|
.dim-feishuGroupControl { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; padding: 16px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 12px; background: var(--dsw-alias-bg-layer-3, #fff); }
|
|
409
|
-
.dim-feishuGroupControlHeader { min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
425
|
+
.dim-feishuGroupControlHeader { position: relative; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
410
426
|
.dim-feishuGroupControlHeader h3 { min-width: 0; margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 21px; font-weight: 650; }
|
|
411
427
|
.dim-feishuGroupControlStatus { flex: none; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 17px; }
|
|
412
428
|
.dim-feishuGroupSelect { min-width: 0; width: 100%; height: 38px; padding: 0 34px 0 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 13px; line-height: 38px; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease; }
|
|
@@ -608,9 +624,8 @@ const CSS = String.raw`
|
|
|
608
624
|
@media (max-width: 840px) {
|
|
609
625
|
.dim-title { align-items: flex-start; }
|
|
610
626
|
.dim-layout { grid-template-columns: minmax(0, 1fr); gap: 18px; }
|
|
611
|
-
.dim-rail { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
627
|
+
.dim-rail { grid-template-columns: repeat(2, minmax(0, 1fr)); padding-right: 1px; }
|
|
612
628
|
.dim-divider { display: none; }
|
|
613
|
-
.dim-rail { max-height: none; overflow: visible; padding-right: 1px; }
|
|
614
629
|
.dim-channel { min-height: 48px; }
|
|
615
630
|
}
|
|
616
631
|
@media (max-width: 560px) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createProductionController } from './production.mjs';
|
|
2
|
-
import { installDingtalkRpc } from './rpc.mjs';
|
|
2
|
+
import { createDingtalkRpcHandler, installDingtalkRpc, DINGTALK_RPC_CHANNEL } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
3
4
|
|
|
4
5
|
export const name = 'dsh-dingtalk-host';
|
|
5
6
|
export const inject = ['connection', 'credentials', 'typertGateway'];
|
|
@@ -9,20 +10,12 @@ export async function apply(ctx, config = {}) {
|
|
|
9
10
|
return installDingtalkRpc(ctx, config.controller, config.rpcOptions, config.rpcAuthority);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
config.rpcOptions,
|
|
19
|
-
config.rpcAuthority,
|
|
20
|
-
);
|
|
21
|
-
ctx.effect(() => async () => {
|
|
22
|
-
await unregisterDelivery?.();
|
|
23
|
-
await production.close();
|
|
24
|
-
}, 'dsh-dingtalk: close bot connections');
|
|
25
|
-
return disposeRpc;
|
|
13
|
+
return installProductionChannel(ctx, config, {
|
|
14
|
+
channel: 'dingtalk',
|
|
15
|
+
rpcChannel: DINGTALK_RPC_CHANNEL,
|
|
16
|
+
createProduction: () => createProductionController(ctx, config, config.internals),
|
|
17
|
+
createHandler: controller => createDingtalkRpcHandler(controller, config.rpcOptions),
|
|
18
|
+
});
|
|
26
19
|
}
|
|
27
20
|
|
|
28
21
|
export function createDingtalkHostPlugin(config) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createProductionController } from './production.mjs';
|
|
2
|
-
import { installDiscordRpc } from './rpc.mjs';
|
|
2
|
+
import { createDiscordRpcHandler, installDiscordRpc, DISCORD_RPC_CHANNEL } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
3
4
|
|
|
4
5
|
export const name = 'dsh-im-discord-host';
|
|
5
6
|
export const inject = ['connection', 'credentials', 'typertGateway'];
|
|
@@ -8,15 +9,12 @@ export async function apply(ctx, config = {}) {
|
|
|
8
9
|
if (config?.controller) {
|
|
9
10
|
return installDiscordRpc(ctx, config.controller, config.rpcAuthority);
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
await production.close();
|
|
18
|
-
}, 'dsh-im: close Discord bot connections');
|
|
19
|
-
return disposeRpc;
|
|
12
|
+
return installProductionChannel(ctx, config, {
|
|
13
|
+
channel: 'discord',
|
|
14
|
+
rpcChannel: DISCORD_RPC_CHANNEL,
|
|
15
|
+
createProduction: () => createProductionController(ctx, config, config.internals ?? {}),
|
|
16
|
+
createHandler: controller => createDiscordRpcHandler(controller),
|
|
17
|
+
});
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
export function createDiscordHostPlugin(config) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createProvisioningBackedController } from './controller.mjs';
|
|
2
2
|
import { createProductionController } from './production.mjs';
|
|
3
|
-
import { installFeishuRpc } from './rpc.mjs';
|
|
3
|
+
import { createFeishuRpcHandler, installFeishuRpc, FEISHU_RPC_CHANNEL } from './rpc.mjs';
|
|
4
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
4
5
|
|
|
5
6
|
export const name = 'dsh-feishu-host';
|
|
6
7
|
export const inject = ['connection', 'credentials', 'typertGateway'];
|
|
@@ -28,20 +29,12 @@ export async function apply(ctx, config = {}) {
|
|
|
28
29
|
return installFeishuRpc(ctx, controller, config.rpcOptions, config.rpcAuthority);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
config.rpcOptions,
|
|
38
|
-
config.rpcAuthority,
|
|
39
|
-
);
|
|
40
|
-
ctx.effect(() => async () => {
|
|
41
|
-
await unregisterDelivery?.();
|
|
42
|
-
await production.close();
|
|
43
|
-
}, 'dsh-feishu: close controller and live connection');
|
|
44
|
-
return disposeRpc;
|
|
32
|
+
return installProductionChannel(ctx, config, {
|
|
33
|
+
channel: 'feishu',
|
|
34
|
+
rpcChannel: FEISHU_RPC_CHANNEL,
|
|
35
|
+
createProduction: () => createProductionController(ctx, config, config.internals),
|
|
36
|
+
createHandler: controller => createFeishuRpcHandler(controller, config.rpcOptions),
|
|
37
|
+
});
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
/** Create a programmatic plugin module with dependencies closed over. */
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { createProductionController } from './production.mjs';
|
|
2
|
-
import { installOfficeRpc } from './rpc.mjs';
|
|
2
|
+
import { createOfficeRpcHandler, installOfficeRpc } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
4
|
+
import { OFFICE_RPC_CHANNEL } from '../../../../src/channels/office/protocol.mjs';
|
|
3
5
|
|
|
4
6
|
export async function apply(ctx, config = {}) {
|
|
5
7
|
if (config.controller) return installOfficeRpc(ctx, config.controller, config.rpcAuthority);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
return installProductionChannel(ctx, config, {
|
|
9
|
+
channel: 'office',
|
|
10
|
+
rpcChannel: OFFICE_RPC_CHANNEL,
|
|
11
|
+
createProduction: () => createProductionController(ctx, config, config.internals ?? {}),
|
|
12
|
+
createHandler: controller => createOfficeRpcHandler(controller),
|
|
13
|
+
});
|
|
10
14
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createProductionController } from './production.mjs';
|
|
2
|
-
import { installQqRpc } from './rpc.mjs';
|
|
2
|
+
import { createQqRpcHandler, installQqRpc, QQ_RPC_CHANNEL } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
3
4
|
|
|
4
5
|
export const name = 'dsh-im-qq-host';
|
|
5
6
|
export const inject = ['connection', 'credentials', 'typertGateway'];
|
|
@@ -8,20 +9,12 @@ export async function apply(ctx, config = {}) {
|
|
|
8
9
|
if (config?.controller) {
|
|
9
10
|
return installQqRpc(ctx, config.controller, config.rpcOptions, config.rpcAuthority);
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
config.rpcOptions,
|
|
18
|
-
config.rpcAuthority,
|
|
19
|
-
);
|
|
20
|
-
ctx.effect(() => async () => {
|
|
21
|
-
await unregisterDelivery?.();
|
|
22
|
-
await production.close();
|
|
23
|
-
}, 'dsh-im: close QQ bot connections');
|
|
24
|
-
return disposeRpc;
|
|
12
|
+
return installProductionChannel(ctx, config, {
|
|
13
|
+
channel: 'qq',
|
|
14
|
+
rpcChannel: QQ_RPC_CHANNEL,
|
|
15
|
+
createProduction: () => createProductionController(ctx, config, config.internals),
|
|
16
|
+
createHandler: controller => createQqRpcHandler(controller, config.rpcOptions),
|
|
17
|
+
});
|
|
25
18
|
}
|
|
26
19
|
|
|
27
20
|
export function createQqHostPlugin(config) {
|
|
@@ -91,7 +91,7 @@ export function initialAccessPolicyFor(channel, config = {}) {
|
|
|
91
91
|
group: allowlistScope(),
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
|
-
if (['dingtalk', 'wecom', 'slack', 'discord'].includes(key)) {
|
|
94
|
+
if (['dingtalk', 'wecom', 'wecom-app', 'slack', 'discord'].includes(key)) {
|
|
95
95
|
return createAccessPolicy({ direct: openScope(), group: openScope() });
|
|
96
96
|
}
|
|
97
97
|
throw new TypeError(`Unsupported access-policy channel: ${channel}`);
|
|
@@ -13,7 +13,6 @@ export function validModelPayload(payload) {
|
|
|
13
13
|
|| !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) return false;
|
|
14
14
|
if (payload.model === null) return true;
|
|
15
15
|
return isRecord(payload.model)
|
|
16
|
-
&& Object.keys(payload.model).
|
|
17
|
-
&& Object.keys(payload.model).every((key) => ['provider', 'model'].includes(key))
|
|
16
|
+
&& Object.keys(payload.model).every((key) => ['provider', 'model', 'reasoningEffort'].includes(key))
|
|
18
17
|
&& normalizeModelSelection(payload.model) !== null;
|
|
19
18
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { t } from '../../../../src/channels/shared/i18n.mjs';
|
|
2
|
+
|
|
3
|
+
const CHANNEL_NAMES = {
|
|
4
|
+
weixin: '微信', feishu: '飞书', dingtalk: '钉钉', wecom: '企业微信',
|
|
5
|
+
'wecom-app': '企业微信应用', qq: 'QQ', slack: 'Slack', telegram: 'Telegram',
|
|
6
|
+
discord: 'Discord', whatsapp: 'WhatsApp', office: 'AI Office',
|
|
7
|
+
};
|
|
8
|
+
const INVALID_CONFIG_MESSAGES = new Set([
|
|
9
|
+
'dsh-weixin config contains invalid account data',
|
|
10
|
+
'dsh-feishu config contains an invalid bot entry',
|
|
11
|
+
'dsh-feishu config contains duplicate bot identities',
|
|
12
|
+
'dsh-feishu config is incomplete or invalid',
|
|
13
|
+
'dsh-dingtalk config contains invalid bot data',
|
|
14
|
+
...['Enterprise WeChat', 'Enterprise WeChat app', 'QQ', 'Slack', 'Telegram', 'Discord']
|
|
15
|
+
.map(channel => `dsh-im ${channel} config contains invalid bot data`),
|
|
16
|
+
'dsh-im WhatsApp config contains invalid account data',
|
|
17
|
+
'dsh-im AI Office config is invalid',
|
|
18
|
+
'dsh-im workspace config is invalid',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function publicChannelInitializing(channel) {
|
|
22
|
+
return {
|
|
23
|
+
code: `${channel}-initializing`,
|
|
24
|
+
message: t('{channel}正在初始化,请稍后重新读取。', { channel: t(CHANNEL_NAMES[channel]) }),
|
|
25
|
+
details: {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Only public guidance crosses RPC; parser excerpts, paths and secrets stay in logs. */
|
|
30
|
+
export function publicChannelStartupError(channel, error) {
|
|
31
|
+
const params = { channel: t(CHANNEL_NAMES[channel]), id: channel };
|
|
32
|
+
if (error instanceof SyntaxError || INVALID_CONFIG_MESSAGES.has(error?.message)) {
|
|
33
|
+
return {
|
|
34
|
+
code: `${channel}-startup-config-invalid`,
|
|
35
|
+
message: channel === 'office'
|
|
36
|
+
? t('{channel}配置格式错误。请检查其数据目录中的 config.json,修复后重启 DSH。详细原因请查看启动日志。', params)
|
|
37
|
+
: t('{channel}配置格式错误。请检查其数据目录中的 config.json 和 workspaces.json,修复后重启 DSH。详细原因请查看启动日志。', params),
|
|
38
|
+
details: {},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (error?.code === 'EACCES' || error?.code === 'EPERM') {
|
|
42
|
+
return {
|
|
43
|
+
code: `${channel}-startup-permission-denied`,
|
|
44
|
+
message: t('无法读取或写入{channel}配置。请检查其数据目录的访问权限,修复后重启 DSH。详细原因请查看启动日志。', params),
|
|
45
|
+
details: {},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
code: `${channel}-startup-failed`,
|
|
50
|
+
message: t('{channel}初始化失败。请查看 DSH 启动日志中 failed to activate {id} 后的错误,修复后重启 DSH。', params),
|
|
51
|
+
details: {},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
2
|
+
import { publicChannelInitializing, publicChannelStartupError } from './startup-error.mjs';
|
|
3
|
+
|
|
4
|
+
/** Mount the native management RPC before any fallible production initialization. */
|
|
5
|
+
export async function installProductionChannel(ctx, config, {
|
|
6
|
+
channel, rpcChannel, createProduction, createHandler,
|
|
7
|
+
}) {
|
|
8
|
+
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
|
9
|
+
throw new TypeError('DSH Host Connection RPC is required');
|
|
10
|
+
}
|
|
11
|
+
let startupError = publicChannelInitializing(channel);
|
|
12
|
+
let handler = async () => ({ ok: false, error: startupError });
|
|
13
|
+
const disposeRpc = ctx.connection.rpc.handle(rpcChannel, (endpoint, payload, signal) => {
|
|
14
|
+
if (signal?.aborted) {
|
|
15
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } };
|
|
16
|
+
}
|
|
17
|
+
return handler(endpoint, payload, signal);
|
|
18
|
+
}, { authority: resolveRpcAuthority(config.rpcAuthority) });
|
|
19
|
+
const logger = typeof ctx.logger === 'function' ? ctx.logger(`dsh-im:${channel}`) : (ctx.logger ?? console);
|
|
20
|
+
let production;
|
|
21
|
+
let unregisterDelivery;
|
|
22
|
+
let closing;
|
|
23
|
+
const closeProduction = () => (closing ??= (async () => {
|
|
24
|
+
try {
|
|
25
|
+
await unregisterDelivery?.();
|
|
26
|
+
} finally {
|
|
27
|
+
await production?.close();
|
|
28
|
+
}
|
|
29
|
+
})());
|
|
30
|
+
try {
|
|
31
|
+
production = await createProduction();
|
|
32
|
+
unregisterDelivery = config.deliveryService && production.deliveryAdapter
|
|
33
|
+
? config.deliveryService.registerAdapter(production.deliveryAdapter) : undefined;
|
|
34
|
+
const readyHandler = createHandler(production.controller);
|
|
35
|
+
ctx.effect(() => closeProduction, `dsh-im: close ${channel} connections`);
|
|
36
|
+
handler = readyHandler;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
startupError = publicChannelStartupError(channel, error);
|
|
39
|
+
logger.error?.(`[dsh-im] failed to activate ${channel}; management RPC remains available`, error);
|
|
40
|
+
try {
|
|
41
|
+
await closeProduction();
|
|
42
|
+
} catch (cleanupError) {
|
|
43
|
+
logger.error?.(`[dsh-im] failed to close partially initialized ${channel} resources`, cleanupError);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return disposeRpc;
|
|
47
|
+
}
|
|
@@ -24,6 +24,7 @@ export function publicWorkspaceError(error) {
|
|
|
24
24
|
'agent-preset-unavailable',
|
|
25
25
|
'model-selection-invalid',
|
|
26
26
|
'model-selection-unavailable',
|
|
27
|
+
'model-reasoning-unavailable',
|
|
27
28
|
'context-enhancement-invalid',
|
|
28
29
|
].includes(error?.code)) return null;
|
|
29
30
|
return { code: error.code, message: error.message };
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { createProductionController } from './production.mjs';
|
|
2
|
-
import { installSlackRpc } from './rpc.mjs';
|
|
2
|
+
import { createSlackRpcHandler, installSlackRpc, SLACK_RPC_CHANNEL } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
3
4
|
|
|
4
5
|
export const name = 'dsh-im-slack-host';
|
|
5
6
|
export const inject = ['connection', 'credentials', 'typertGateway'];
|
|
6
7
|
|
|
7
8
|
export async function apply(ctx, config = {}) {
|
|
8
9
|
if (config?.controller) return installSlackRpc(ctx, config.controller, config.rpcAuthority);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
await production.close();
|
|
16
|
-
}, 'dsh-im: close Slack bot connections');
|
|
17
|
-
return disposeRpc;
|
|
10
|
+
return installProductionChannel(ctx, config, {
|
|
11
|
+
channel: 'slack',
|
|
12
|
+
rpcChannel: SLACK_RPC_CHANNEL,
|
|
13
|
+
createProduction: () => createProductionController(ctx, config, config.internals ?? {}),
|
|
14
|
+
createHandler: controller => createSlackRpcHandler(controller),
|
|
15
|
+
});
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
export function createSlackHostPlugin(config) {
|