@xmanrui/dsh-im 0.1.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/LICENSE +21 -0
- package/README.md +143 -0
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/bin/dsh-im.mjs +99 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +6211 -0
- package/lib/index.js +13339 -0
- package/package.json +73 -0
- package/plugin-src/client/build.mjs +38 -0
- package/plugin-src/client/channel-logos.js +107 -0
- package/plugin-src/client/channels/dingtalk/api.js +203 -0
- package/plugin-src/client/channels/dingtalk/index.js +709 -0
- package/plugin-src/client/channels/dingtalk/styles.js +140 -0
- package/plugin-src/client/channels/discord/api.js +11 -0
- package/plugin-src/client/channels/discord/index.js +25 -0
- package/plugin-src/client/channels/discord/styles.js +19 -0
- package/plugin-src/client/channels/feishu/api.js +325 -0
- package/plugin-src/client/channels/feishu/index.js +986 -0
- package/plugin-src/client/channels/feishu/styles.js +505 -0
- package/plugin-src/client/channels/qq/api.js +119 -0
- package/plugin-src/client/channels/qq/index.js +402 -0
- package/plugin-src/client/channels/qq/styles.js +19 -0
- package/plugin-src/client/channels/shared/token-api.js +88 -0
- package/plugin-src/client/channels/shared/token-channel.js +277 -0
- package/plugin-src/client/channels/telegram/api.js +11 -0
- package/plugin-src/client/channels/telegram/index.js +25 -0
- package/plugin-src/client/channels/telegram/styles.js +19 -0
- package/plugin-src/client/channels/wecom/api.js +119 -0
- package/plugin-src/client/channels/wecom/index.js +402 -0
- package/plugin-src/client/channels/wecom/styles.js +19 -0
- package/plugin-src/client/channels/weixin/api.js +156 -0
- package/plugin-src/client/channels/weixin/index.js +577 -0
- package/plugin-src/client/channels/weixin/styles.js +120 -0
- package/plugin-src/client/credential-binding.js +117 -0
- package/plugin-src/client/index.js +192 -0
- package/plugin-src/client/lifecycle.js +86 -0
- package/plugin-src/client/styles.js +192 -0
- package/plugin-src/host/build.mjs +24 -0
- package/plugin-src/host/channels/dingtalk/connection-supervisor.mjs +130 -0
- package/plugin-src/host/channels/dingtalk/index.mjs +39 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +122 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +237 -0
- package/plugin-src/host/channels/discord/index.mjs +30 -0
- package/plugin-src/host/channels/discord/production.mjs +17 -0
- package/plugin-src/host/channels/discord/rpc.mjs +21 -0
- package/plugin-src/host/channels/feishu/connection-supervisor.mjs +167 -0
- package/plugin-src/host/channels/feishu/controller.mjs +172 -0
- package/plugin-src/host/channels/feishu/credential-store.mjs +84 -0
- package/plugin-src/host/channels/feishu/index.mjs +73 -0
- package/plugin-src/host/channels/feishu/production.mjs +138 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +454 -0
- package/plugin-src/host/channels/qq/connection-supervisor.mjs +124 -0
- package/plugin-src/host/channels/qq/index.mjs +30 -0
- package/plugin-src/host/channels/qq/production.mjs +109 -0
- package/plugin-src/host/channels/qq/rpc.mjs +146 -0
- package/plugin-src/host/channels/shared/connection-supervisor.mjs +133 -0
- package/plugin-src/host/channels/shared/production.mjs +104 -0
- package/plugin-src/host/channels/shared/rpc.mjs +119 -0
- package/plugin-src/host/channels/telegram/index.mjs +30 -0
- package/plugin-src/host/channels/telegram/production.mjs +17 -0
- package/plugin-src/host/channels/telegram/rpc.mjs +21 -0
- package/plugin-src/host/channels/wecom/connection-supervisor.mjs +124 -0
- package/plugin-src/host/channels/wecom/index.mjs +36 -0
- package/plugin-src/host/channels/wecom/production.mjs +113 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +151 -0
- package/plugin-src/host/channels/weixin/connection-supervisor.mjs +127 -0
- package/plugin-src/host/channels/weixin/index.mjs +39 -0
- package/plugin-src/host/channels/weixin/production.mjs +119 -0
- package/plugin-src/host/channels/weixin/rpc.mjs +178 -0
- package/plugin-src/host/index.mjs +44 -0
- package/plugin-src/host/rpc-authority.mjs +11 -0
- package/scripts/verify-package.mjs +97 -0
- package/src/channels/dingtalk/config-store.mjs +282 -0
- package/src/channels/dingtalk/device-auth.mjs +237 -0
- package/src/channels/dingtalk/dingtalk-api.mjs +579 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +281 -0
- package/src/channels/dingtalk/dingtalk-card-stream.mjs +233 -0
- package/src/channels/dingtalk/dingtalk-controller.mjs +752 -0
- package/src/channels/dingtalk/dingtalk-runtime.mjs +358 -0
- package/src/channels/dingtalk/harness-client.mjs +299 -0
- package/src/channels/dingtalk/state-store.mjs +212 -0
- package/src/channels/discord/config-store.mjs +24 -0
- package/src/channels/discord/discord-api.mjs +153 -0
- package/src/channels/discord/discord-bridge.mjs +15 -0
- package/src/channels/discord/discord-controller.mjs +16 -0
- package/src/channels/discord/discord-runtime.mjs +460 -0
- package/src/channels/discord/harness-client.mjs +3 -0
- package/src/channels/discord/state-store.mjs +3 -0
- package/src/channels/feishu/bridge.mjs +216 -0
- package/src/channels/feishu/config.mjs +71 -0
- package/src/channels/feishu/feishu-app.mjs +51 -0
- package/src/channels/feishu/feishu-channel.mjs +153 -0
- package/src/channels/feishu/feishu-runtime.mjs +222 -0
- package/src/channels/feishu/harness-client.mjs +268 -0
- package/src/channels/feishu/message-utils.mjs +51 -0
- package/src/channels/feishu/multi-bot-controller.mjs +686 -0
- package/src/channels/feishu/plugin-config-store.mjs +204 -0
- package/src/channels/feishu/plugin-controller.mjs +248 -0
- package/src/channels/feishu/registration-manager.mjs +345 -0
- package/src/channels/feishu/state-store.mjs +71 -0
- package/src/channels/qq/config-store.mjs +171 -0
- package/src/channels/qq/harness-client.mjs +3 -0
- package/src/channels/qq/qq-bridge.mjs +177 -0
- package/src/channels/qq/qq-controller.mjs +452 -0
- package/src/channels/qq/qq-runtime.mjs +208 -0
- package/src/channels/qq/qr-auth.mjs +24 -0
- package/src/channels/qq/state-store.mjs +96 -0
- package/src/channels/shared/conversation-state-store.mjs +107 -0
- package/src/channels/shared/editable-message-stream.mjs +81 -0
- package/src/channels/shared/text-harness-bridge.mjs +180 -0
- package/src/channels/shared/token-bot-controller.mjs +298 -0
- package/src/channels/shared/token-config-store.mjs +180 -0
- package/src/channels/telegram/config-store.mjs +24 -0
- package/src/channels/telegram/harness-client.mjs +3 -0
- package/src/channels/telegram/state-store.mjs +3 -0
- package/src/channels/telegram/telegram-api.mjs +120 -0
- package/src/channels/telegram/telegram-bridge.mjs +15 -0
- package/src/channels/telegram/telegram-controller.mjs +16 -0
- package/src/channels/telegram/telegram-runtime.mjs +278 -0
- package/src/channels/wecom/config-store.mjs +170 -0
- package/src/channels/wecom/harness-client.mjs +3 -0
- package/src/channels/wecom/qr-auth.mjs +95 -0
- package/src/channels/wecom/state-store.mjs +90 -0
- package/src/channels/wecom/wecom-bridge.mjs +243 -0
- package/src/channels/wecom/wecom-controller.mjs +474 -0
- package/src/channels/wecom/wecom-runtime.mjs +209 -0
- package/src/channels/weixin/config-store.mjs +182 -0
- package/src/channels/weixin/harness-client.mjs +259 -0
- package/src/channels/weixin/state-store.mjs +112 -0
- package/src/channels/weixin/weixin-api.mjs +319 -0
- package/src/channels/weixin/weixin-bridge.mjs +173 -0
- package/src/channels/weixin/weixin-controller.mjs +545 -0
- package/src/channels/weixin/weixin-runtime.mjs +204 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { unlink } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { QqConfigStore } from '../../../../src/channels/qq/config-store.mjs';
|
|
6
|
+
import { QqHarnessClient } from '../../../../src/channels/qq/harness-client.mjs';
|
|
7
|
+
import { QqController } from '../../../../src/channels/qq/qq-controller.mjs';
|
|
8
|
+
import { QqRuntime } from '../../../../src/channels/qq/qq-runtime.mjs';
|
|
9
|
+
import { QqQrAuth } from '../../../../src/channels/qq/qr-auth.mjs';
|
|
10
|
+
import { QqStateStore } from '../../../../src/channels/qq/state-store.mjs';
|
|
11
|
+
import { createConnectionSupervisor } from './connection-supervisor.mjs';
|
|
12
|
+
|
|
13
|
+
function harnessOrigin(webServer, configured) {
|
|
14
|
+
if (configured !== undefined) return new URL(configured);
|
|
15
|
+
const port = webServer?.port;
|
|
16
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
17
|
+
throw new Error('dsh-im QQ requires an initialized DSH webServer port');
|
|
18
|
+
}
|
|
19
|
+
return new URL(`http://127.0.0.1:${port}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function pluginPaths(config) {
|
|
23
|
+
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
|
|
24
|
+
const root = resolve(config.dataDir ?? join(dshHome, 'integrations', 'dsh-qq'));
|
|
25
|
+
return {
|
|
26
|
+
config: resolve(config.configPath ?? join(root, 'config.json')),
|
|
27
|
+
bots: resolve(config.botsDir ?? join(root, 'bots')),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
32
|
+
if (!ctx?.credentials) throw new TypeError('dsh-im QQ requires ctx.credentials');
|
|
33
|
+
if (!ctx?.webServer) throw new TypeError('dsh-im QQ requires ctx.webServer');
|
|
34
|
+
|
|
35
|
+
const ConfigStore = internals.ConfigStore ?? QqConfigStore;
|
|
36
|
+
const StateStore = internals.StateStore ?? QqStateStore;
|
|
37
|
+
const Harness = internals.HarnessClient ?? QqHarnessClient;
|
|
38
|
+
const Controller = internals.Controller ?? QqController;
|
|
39
|
+
const Runtime = internals.Runtime ?? QqRuntime;
|
|
40
|
+
const QrAuth = internals.QrAuth ?? QqQrAuth;
|
|
41
|
+
const createSupervisor = internals.createConnectionSupervisor ?? createConnectionSupervisor;
|
|
42
|
+
const logger = typeof ctx.logger === 'function' ? ctx.logger('dsh-im:qq') : (ctx.logger ?? console);
|
|
43
|
+
const paths = pluginPaths(config);
|
|
44
|
+
const configStore = await new ConfigStore(paths.config).load();
|
|
45
|
+
const qrAuth = internals.qrAuth ?? new QrAuth({ source: config.qrSource ?? 'deepseek-harness' });
|
|
46
|
+
const stateStores = new Map();
|
|
47
|
+
const statePath = (botId) => resolve(paths.bots, botId, 'state.json');
|
|
48
|
+
const stateFor = async (botId) => {
|
|
49
|
+
let state = stateStores.get(botId);
|
|
50
|
+
if (!state) {
|
|
51
|
+
state = await new StateStore(statePath(botId)).load();
|
|
52
|
+
stateStores.set(botId, state);
|
|
53
|
+
}
|
|
54
|
+
return state;
|
|
55
|
+
};
|
|
56
|
+
const harness = new Harness({
|
|
57
|
+
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
58
|
+
workspace: resolve(config.workspace ?? process.cwd()),
|
|
59
|
+
agentPreset: config.agentPreset ?? 'standard',
|
|
60
|
+
autostart: false,
|
|
61
|
+
dshBin: config.dshBin ?? 'dsh',
|
|
62
|
+
});
|
|
63
|
+
const controller = new Controller({
|
|
64
|
+
qrAuth,
|
|
65
|
+
credentials: ctx.credentials,
|
|
66
|
+
configStore,
|
|
67
|
+
logger,
|
|
68
|
+
createRuntime: async ({ botId, config: botConfig, appSecret }) => new Runtime({
|
|
69
|
+
config: botConfig,
|
|
70
|
+
appSecret,
|
|
71
|
+
harness,
|
|
72
|
+
state: await stateFor(botId),
|
|
73
|
+
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
|
74
|
+
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
|
|
75
|
+
logger: {
|
|
76
|
+
error: (...args) => logger.error?.(`[${botId}]`, ...args),
|
|
77
|
+
warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
|
|
78
|
+
info: (...args) => logger.info?.(`[${botId}]`, ...args),
|
|
79
|
+
debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
deleteState: async ({ botId }) => {
|
|
83
|
+
const state = stateStores.get(botId);
|
|
84
|
+
stateStores.delete(botId);
|
|
85
|
+
if (state && typeof state.remove === 'function') return state.remove();
|
|
86
|
+
try {
|
|
87
|
+
await unlink(statePath(botId));
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
const supervisor = createSupervisor({
|
|
94
|
+
controller,
|
|
95
|
+
harness,
|
|
96
|
+
logger,
|
|
97
|
+
retryDelaysMs: config.retryDelaysMs,
|
|
98
|
+
healthyIntervalMs: config.healthyIntervalMs,
|
|
99
|
+
}).start();
|
|
100
|
+
return {
|
|
101
|
+
controller,
|
|
102
|
+
ready: supervisor.ready,
|
|
103
|
+
async close() {
|
|
104
|
+
await supervisor.close();
|
|
105
|
+
await controller.close();
|
|
106
|
+
harness.stopManagedProcess();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import QRCode from 'qrcode';
|
|
2
|
+
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
3
|
+
|
|
4
|
+
export const QQ_RPC_CHANNEL = '/qq';
|
|
5
|
+
export const QQ_ENDPOINTS = Object.freeze({
|
|
6
|
+
status: 'connection.status',
|
|
7
|
+
beginProvisioning: 'provision.begin',
|
|
8
|
+
pollProvisioning: 'provision.poll',
|
|
9
|
+
cancelProvisioning: 'provision.cancel',
|
|
10
|
+
bindCredentials: 'bot.bind-credentials',
|
|
11
|
+
reconnectBot: 'bot.reconnect',
|
|
12
|
+
deleteBot: 'bot.delete',
|
|
13
|
+
});
|
|
14
|
+
export const QQ_RPC_ENDPOINTS = Object.freeze(Object.values(QQ_ENDPOINTS));
|
|
15
|
+
|
|
16
|
+
const FORBIDDEN_PUBLIC_KEYS = new Set([
|
|
17
|
+
'appSecret', 'app_secret', 'secretRef', 'ownerUserOpenid', 'userOpenid', 'verificationUrl',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function exactKeys(value, allowed) {
|
|
25
|
+
return isRecord(value) && Object.keys(value).every((key) => allowed.includes(key));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validId(value) {
|
|
29
|
+
return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validCredential(value, maxLength) {
|
|
33
|
+
return typeof value === 'string' && value.trim().length > 0 && value.length <= maxLength;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function payloadFailure(endpoint, payload) {
|
|
37
|
+
if (!isRecord(payload)) return 'Payload must be an object.';
|
|
38
|
+
if (endpoint === QQ_ENDPOINTS.status) return exactKeys(payload, []) ? null : 'connection.status does not accept fields.';
|
|
39
|
+
if (endpoint === QQ_ENDPOINTS.beginProvisioning) {
|
|
40
|
+
return exactKeys(payload, ['locale']) && (payload.locale === undefined || payload.locale === 'zh-CN')
|
|
41
|
+
? null : 'provision.begin received unsupported fields.';
|
|
42
|
+
}
|
|
43
|
+
if ([QQ_ENDPOINTS.pollProvisioning, QQ_ENDPOINTS.cancelProvisioning].includes(endpoint)) {
|
|
44
|
+
return exactKeys(payload, ['attemptId']) && validId(payload.attemptId)
|
|
45
|
+
? null : `${endpoint} requires an attemptId.`;
|
|
46
|
+
}
|
|
47
|
+
if (endpoint === QQ_ENDPOINTS.bindCredentials) {
|
|
48
|
+
return exactKeys(payload, ['appId', 'appSecret'])
|
|
49
|
+
&& validCredential(payload.appId, 256)
|
|
50
|
+
&& validCredential(payload.appSecret, 1024)
|
|
51
|
+
? null : 'bot.bind-credentials requires AppID and AppSecret.';
|
|
52
|
+
}
|
|
53
|
+
if (endpoint === QQ_ENDPOINTS.reconnectBot) {
|
|
54
|
+
return exactKeys(payload, ['botId']) && validId(payload.botId) ? null : 'bot.reconnect requires a botId.';
|
|
55
|
+
}
|
|
56
|
+
if (endpoint === QQ_ENDPOINTS.deleteBot) {
|
|
57
|
+
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
|
|
58
|
+
? null : 'bot.delete requires a botId and confirm=true.';
|
|
59
|
+
}
|
|
60
|
+
return 'Unknown QQ endpoint.';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sanitizePublic(value) {
|
|
64
|
+
if (Array.isArray(value)) return value.map(sanitizePublic);
|
|
65
|
+
if (!isRecord(value)) return value;
|
|
66
|
+
const safe = {};
|
|
67
|
+
for (const [key, child] of Object.entries(value)) {
|
|
68
|
+
if (!FORBIDDEN_PUBLIC_KEYS.has(key)) safe[key] = sanitizePublic(child);
|
|
69
|
+
}
|
|
70
|
+
return safe;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function qrDataUrl(value) {
|
|
74
|
+
return QRCode.toDataURL(value, {
|
|
75
|
+
type: 'image/png', errorCorrectionLevel: 'M', margin: 2, width: 320,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function withEncodedQr(value, encodeQr) {
|
|
80
|
+
if (!value || typeof value.verificationUrl !== 'string') return sanitizePublic(value);
|
|
81
|
+
return sanitizePublic({ ...value, qrCodeDataUrl: await encodeQr(value.verificationUrl) });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function publicStatus(status, encodeQr) {
|
|
85
|
+
const value = structuredClone(status);
|
|
86
|
+
if (value?.provisioning) value.provisioning = await withEncodedQr(value.provisioning, encodeQr);
|
|
87
|
+
return sanitizePublic(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
|
|
91
|
+
for (const method of ['status', 'startProvisioning', 'registrationStatus', 'cancelProvisioning', 'bindCredentials', 'reconnectBot', 'deleteBot']) {
|
|
92
|
+
if (typeof controller?.[method] !== 'function') throw new TypeError(`A complete QQ controller is required (${method})`);
|
|
93
|
+
}
|
|
94
|
+
const qrCache = new Map();
|
|
95
|
+
const cachedEncode = (url) => {
|
|
96
|
+
let encoded = qrCache.get(url);
|
|
97
|
+
if (!encoded) {
|
|
98
|
+
if (qrCache.size >= 16) qrCache.delete(qrCache.keys().next().value);
|
|
99
|
+
encoded = Promise.resolve().then(() => encodeQr(url));
|
|
100
|
+
qrCache.set(url, encoded);
|
|
101
|
+
}
|
|
102
|
+
return encoded;
|
|
103
|
+
};
|
|
104
|
+
return async (endpoint, payload, signal) => {
|
|
105
|
+
if (signal?.aborted) return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
|
|
106
|
+
if (!QQ_RPC_ENDPOINTS.includes(endpoint)) return { ok: false, error: { code: 'bad-request', message: 'Unknown QQ endpoint.' } };
|
|
107
|
+
const invalid = payloadFailure(endpoint, payload);
|
|
108
|
+
if (invalid) return { ok: false, error: { code: 'bad-request', message: invalid } };
|
|
109
|
+
try {
|
|
110
|
+
let value;
|
|
111
|
+
if (endpoint === QQ_ENDPOINTS.status) value = await publicStatus(await controller.status(), cachedEncode);
|
|
112
|
+
else if (endpoint === QQ_ENDPOINTS.beginProvisioning) value = await withEncodedQr(await controller.startProvisioning(), cachedEncode);
|
|
113
|
+
else if (endpoint === QQ_ENDPOINTS.pollProvisioning) {
|
|
114
|
+
const current = await controller.registrationStatus(payload.attemptId);
|
|
115
|
+
if (!current) return { ok: false, error: { code: 'bad-request', message: 'The provisioning attempt no longer exists.' } };
|
|
116
|
+
value = await withEncodedQr(current, cachedEncode);
|
|
117
|
+
} else if (endpoint === QQ_ENDPOINTS.cancelProvisioning) {
|
|
118
|
+
value = sanitizePublic(await controller.cancelProvisioning(payload.attemptId));
|
|
119
|
+
} else if (endpoint === QQ_ENDPOINTS.bindCredentials) {
|
|
120
|
+
value = await publicStatus(await controller.bindCredentials(payload), cachedEncode);
|
|
121
|
+
} else if (endpoint === QQ_ENDPOINTS.reconnectBot) {
|
|
122
|
+
value = await publicStatus(await controller.reconnectBot(payload.botId), cachedEncode);
|
|
123
|
+
} else {
|
|
124
|
+
value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
|
|
125
|
+
}
|
|
126
|
+
return signal?.aborted
|
|
127
|
+
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
128
|
+
: { ok: true, value };
|
|
129
|
+
} catch {
|
|
130
|
+
return signal?.aborted
|
|
131
|
+
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
132
|
+
: { ok: false, error: { code: 'qq-operation-failed', message: 'QQ 操作失败,请稍后重试。' } };
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function installQqRpc(ctx, controller, options, authority) {
|
|
138
|
+
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
|
139
|
+
throw new TypeError('DSH Host Connection RPC is required');
|
|
140
|
+
}
|
|
141
|
+
return ctx.connection.rpc.handle(
|
|
142
|
+
QQ_RPC_CHANNEL,
|
|
143
|
+
createQqRpcHandler(controller, options),
|
|
144
|
+
{ authority: resolveRpcAuthority(authority) },
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const DEFAULT_RETRY_DELAYS_MS = Object.freeze([250, 1_000, 3_000, 5_000, 10_000, 30_000]);
|
|
2
|
+
|
|
3
|
+
function retryDelays(value) {
|
|
4
|
+
if (!Array.isArray(value) || value.length === 0) return [...DEFAULT_RETRY_DELAYS_MS];
|
|
5
|
+
const valid = value.filter((delay) => Number.isFinite(delay) && delay >= 0);
|
|
6
|
+
return valid.length > 0 ? valid : [...DEFAULT_RETRY_DELAYS_MS];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class TokenConnectionSupervisor {
|
|
10
|
+
#channel;
|
|
11
|
+
#controller;
|
|
12
|
+
#harness;
|
|
13
|
+
#logger;
|
|
14
|
+
#retryDelays;
|
|
15
|
+
#healthyIntervalMs;
|
|
16
|
+
#setTimeout;
|
|
17
|
+
#clearTimeout;
|
|
18
|
+
#timer = null;
|
|
19
|
+
#running = null;
|
|
20
|
+
#retryIndex = 0;
|
|
21
|
+
#closed = false;
|
|
22
|
+
#started = false;
|
|
23
|
+
#ready;
|
|
24
|
+
#resolveReady;
|
|
25
|
+
|
|
26
|
+
constructor({
|
|
27
|
+
channel,
|
|
28
|
+
controller,
|
|
29
|
+
harness,
|
|
30
|
+
logger = console,
|
|
31
|
+
retryDelaysMs,
|
|
32
|
+
healthyIntervalMs = 15_000,
|
|
33
|
+
setTimeoutImpl = setTimeout,
|
|
34
|
+
clearTimeoutImpl = clearTimeout,
|
|
35
|
+
}) {
|
|
36
|
+
if (!channel) throw new TypeError('TokenConnectionSupervisor requires a channel');
|
|
37
|
+
if (!controller || typeof controller.initialize !== 'function' || typeof controller.status !== 'function') {
|
|
38
|
+
throw new TypeError('TokenConnectionSupervisor requires a controller');
|
|
39
|
+
}
|
|
40
|
+
if (!harness || typeof harness.ensureRunning !== 'function') {
|
|
41
|
+
throw new TypeError('TokenConnectionSupervisor requires a Harness client');
|
|
42
|
+
}
|
|
43
|
+
this.#channel = channel;
|
|
44
|
+
this.#controller = controller;
|
|
45
|
+
this.#harness = harness;
|
|
46
|
+
this.#logger = logger;
|
|
47
|
+
this.#retryDelays = retryDelays(retryDelaysMs);
|
|
48
|
+
this.#healthyIntervalMs = Number.isFinite(healthyIntervalMs) && healthyIntervalMs >= 0
|
|
49
|
+
? healthyIntervalMs : 15_000;
|
|
50
|
+
this.#setTimeout = setTimeoutImpl;
|
|
51
|
+
this.#clearTimeout = clearTimeoutImpl;
|
|
52
|
+
this.#ready = new Promise((resolve) => {
|
|
53
|
+
this.#resolveReady = resolve;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get ready() {
|
|
58
|
+
return this.#ready;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
start() {
|
|
62
|
+
if (this.#started || this.#closed) return this;
|
|
63
|
+
this.#started = true;
|
|
64
|
+
this.#schedule(0);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async close() {
|
|
69
|
+
if (this.#closed) return;
|
|
70
|
+
this.#closed = true;
|
|
71
|
+
if (this.#timer !== null) this.#clearTimeout(this.#timer);
|
|
72
|
+
this.#timer = null;
|
|
73
|
+
await this.#running?.catch(() => undefined);
|
|
74
|
+
this.#resolveReady?.(null);
|
|
75
|
+
this.#resolveReady = null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#schedule(delayMs) {
|
|
79
|
+
if (this.#closed) return;
|
|
80
|
+
this.#timer = this.#setTimeout(() => {
|
|
81
|
+
this.#timer = null;
|
|
82
|
+
void this.#run();
|
|
83
|
+
}, delayMs);
|
|
84
|
+
this.#timer?.unref?.();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async #run() {
|
|
88
|
+
if (this.#closed || this.#running) return;
|
|
89
|
+
const operation = this.#reconcile();
|
|
90
|
+
this.#running = operation;
|
|
91
|
+
try {
|
|
92
|
+
await operation;
|
|
93
|
+
} finally {
|
|
94
|
+
if (this.#running === operation) this.#running = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async #reconcile() {
|
|
99
|
+
try {
|
|
100
|
+
await this.#harness.ensureRunning();
|
|
101
|
+
if (this.#closed) return;
|
|
102
|
+
const status = await this.#controller.initialize();
|
|
103
|
+
if (this.#closed) return;
|
|
104
|
+
this.#resolveReady?.(status);
|
|
105
|
+
this.#resolveReady = null;
|
|
106
|
+
const { configured, connected } = status.totals;
|
|
107
|
+
if (connected < configured) {
|
|
108
|
+
const delay = this.#retryDelays[Math.min(this.#retryIndex, this.#retryDelays.length - 1)];
|
|
109
|
+
this.#retryIndex += 1;
|
|
110
|
+
this.#logger.warn?.(
|
|
111
|
+
`[dsh-im:${this.#channel}] ${connected}/${configured} bots connected; retrying in ${delay}ms`,
|
|
112
|
+
);
|
|
113
|
+
this.#schedule(delay);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.#retryIndex = 0;
|
|
117
|
+
this.#schedule(this.#healthyIntervalMs);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (this.#closed) return;
|
|
120
|
+
const delay = this.#retryDelays[Math.min(this.#retryIndex, this.#retryDelays.length - 1)];
|
|
121
|
+
this.#retryIndex += 1;
|
|
122
|
+
this.#logger.warn?.(
|
|
123
|
+
`[dsh-im:${this.#channel}] connection reconciliation failed; retrying in ${delay}ms`,
|
|
124
|
+
error,
|
|
125
|
+
);
|
|
126
|
+
this.#schedule(delay);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createTokenConnectionSupervisor(options) {
|
|
132
|
+
return new TokenConnectionSupervisor(options);
|
|
133
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { unlink } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { createTokenConnectionSupervisor } from './connection-supervisor.mjs';
|
|
6
|
+
|
|
7
|
+
function harnessOrigin(webServer, configured) {
|
|
8
|
+
if (configured !== undefined) return new URL(configured);
|
|
9
|
+
const port = webServer?.port;
|
|
10
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
11
|
+
throw new Error('dsh-im token channel requires an initialized DSH webServer port');
|
|
12
|
+
}
|
|
13
|
+
return new URL(`http://127.0.0.1:${port}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function pluginPaths(config, channel) {
|
|
17
|
+
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
|
|
18
|
+
const root = resolve(config.dataDir ?? join(dshHome, 'integrations', `dsh-${channel}`));
|
|
19
|
+
return {
|
|
20
|
+
config: resolve(config.configPath ?? join(root, 'config.json')),
|
|
21
|
+
bots: resolve(config.botsDir ?? join(root, 'bots')),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function createTokenProductionController(ctx, config, internals, definitions) {
|
|
26
|
+
const { channel, ConfigStore, StateStore, HarnessClient, Controller, Runtime } = definitions;
|
|
27
|
+
if (!ctx?.credentials) throw new TypeError(`dsh-im ${channel} requires ctx.credentials`);
|
|
28
|
+
if (!ctx?.webServer) throw new TypeError(`dsh-im ${channel} requires ctx.webServer`);
|
|
29
|
+
|
|
30
|
+
const ResolvedConfigStore = internals.ConfigStore ?? ConfigStore;
|
|
31
|
+
const ResolvedStateStore = internals.StateStore ?? StateStore;
|
|
32
|
+
const ResolvedHarness = internals.HarnessClient ?? HarnessClient;
|
|
33
|
+
const ResolvedController = internals.Controller ?? Controller;
|
|
34
|
+
const ResolvedRuntime = internals.Runtime ?? Runtime;
|
|
35
|
+
const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor;
|
|
36
|
+
const logger = typeof ctx.logger === 'function'
|
|
37
|
+
? ctx.logger(`dsh-im:${channel}`) : (ctx.logger ?? console);
|
|
38
|
+
const paths = pluginPaths(config, channel);
|
|
39
|
+
const configStore = await new ResolvedConfigStore(paths.config).load();
|
|
40
|
+
const stateStores = new Map();
|
|
41
|
+
const statePath = (botId) => resolve(paths.bots, botId, 'state.json');
|
|
42
|
+
const stateFor = async (botId) => {
|
|
43
|
+
let state = stateStores.get(botId);
|
|
44
|
+
if (!state) {
|
|
45
|
+
state = await new ResolvedStateStore(statePath(botId)).load();
|
|
46
|
+
stateStores.set(botId, state);
|
|
47
|
+
}
|
|
48
|
+
return state;
|
|
49
|
+
};
|
|
50
|
+
const harness = new ResolvedHarness({
|
|
51
|
+
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
52
|
+
workspace: resolve(config.workspace ?? process.cwd()),
|
|
53
|
+
agentPreset: config.agentPreset ?? 'standard',
|
|
54
|
+
autostart: false,
|
|
55
|
+
dshBin: config.dshBin ?? 'dsh',
|
|
56
|
+
});
|
|
57
|
+
const controller = new ResolvedController({
|
|
58
|
+
credentials: ctx.credentials,
|
|
59
|
+
configStore,
|
|
60
|
+
logger,
|
|
61
|
+
...(internals.inspectToken ? { inspectToken: internals.inspectToken } : {}),
|
|
62
|
+
createRuntime: async ({ botId, config: botConfig, token }) => new ResolvedRuntime({
|
|
63
|
+
config: botConfig,
|
|
64
|
+
token,
|
|
65
|
+
harness,
|
|
66
|
+
state: await stateFor(botId),
|
|
67
|
+
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
|
68
|
+
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
|
|
69
|
+
logger: {
|
|
70
|
+
error: (...args) => logger.error?.(`[${botId}]`, ...args),
|
|
71
|
+
warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
|
|
72
|
+
info: (...args) => logger.info?.(`[${botId}]`, ...args),
|
|
73
|
+
debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
deleteState: async ({ botId }) => {
|
|
77
|
+
const state = stateStores.get(botId);
|
|
78
|
+
stateStores.delete(botId);
|
|
79
|
+
if (state && typeof state.remove === 'function') return state.remove();
|
|
80
|
+
try {
|
|
81
|
+
await unlink(statePath(botId));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
const supervisor = createSupervisor({
|
|
88
|
+
channel,
|
|
89
|
+
controller,
|
|
90
|
+
harness,
|
|
91
|
+
logger,
|
|
92
|
+
retryDelaysMs: config.retryDelaysMs,
|
|
93
|
+
healthyIntervalMs: config.healthyIntervalMs,
|
|
94
|
+
}).start();
|
|
95
|
+
return {
|
|
96
|
+
controller,
|
|
97
|
+
ready: supervisor.ready,
|
|
98
|
+
async close() {
|
|
99
|
+
await supervisor.close();
|
|
100
|
+
await controller.close();
|
|
101
|
+
harness.stopManagedProcess();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
2
|
+
|
|
3
|
+
export const TOKEN_BOT_ENDPOINTS = Object.freeze({
|
|
4
|
+
status: 'connection.status',
|
|
5
|
+
bindCredentials: 'bot.bind-credentials',
|
|
6
|
+
reconnectBot: 'bot.reconnect',
|
|
7
|
+
deleteBot: 'bot.delete',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const ENDPOINTS = Object.freeze(Object.values(TOKEN_BOT_ENDPOINTS));
|
|
11
|
+
const FORBIDDEN_PUBLIC_KEYS = new Set([
|
|
12
|
+
'token', 'botToken', 'tokenRef', 'platformId', 'secret', 'secretRef',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function exactKeys(value, allowed) {
|
|
20
|
+
return isRecord(value) && Object.keys(value).every((key) => allowed.includes(key));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validId(value) {
|
|
24
|
+
return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validToken(value) {
|
|
28
|
+
return typeof value === 'string' && value.trim().length >= 20 && value.length <= 4_096;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function payloadFailure(endpoint, payload) {
|
|
32
|
+
if (!isRecord(payload)) return 'Payload must be an object.';
|
|
33
|
+
if (endpoint === TOKEN_BOT_ENDPOINTS.status) {
|
|
34
|
+
return exactKeys(payload, []) ? null : 'connection.status does not accept fields.';
|
|
35
|
+
}
|
|
36
|
+
if (endpoint === TOKEN_BOT_ENDPOINTS.bindCredentials) {
|
|
37
|
+
return exactKeys(payload, ['token']) && validToken(payload.token)
|
|
38
|
+
? null : 'bot.bind-credentials requires a Bot Token.';
|
|
39
|
+
}
|
|
40
|
+
if (endpoint === TOKEN_BOT_ENDPOINTS.reconnectBot) {
|
|
41
|
+
return exactKeys(payload, ['botId']) && validId(payload.botId)
|
|
42
|
+
? null : 'bot.reconnect requires a botId.';
|
|
43
|
+
}
|
|
44
|
+
if (endpoint === TOKEN_BOT_ENDPOINTS.deleteBot) {
|
|
45
|
+
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
|
|
46
|
+
? null : 'bot.delete requires a botId and confirm=true.';
|
|
47
|
+
}
|
|
48
|
+
return 'Unknown bot endpoint.';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sanitizePublic(value) {
|
|
52
|
+
if (Array.isArray(value)) return value.map(sanitizePublic);
|
|
53
|
+
if (!isRecord(value)) return value;
|
|
54
|
+
const safe = {};
|
|
55
|
+
for (const [key, child] of Object.entries(value)) {
|
|
56
|
+
if (!FORBIDDEN_PUBLIC_KEYS.has(key)) safe[key] = sanitizePublic(child);
|
|
57
|
+
}
|
|
58
|
+
return safe;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function operationError(channel, error) {
|
|
62
|
+
if (error?.code === 'webhook-configured') {
|
|
63
|
+
return { code: 'webhook-configured', message: error.message };
|
|
64
|
+
}
|
|
65
|
+
if (error?.code === 'telegram-401' || error?.code === 'discord-401') {
|
|
66
|
+
return { code: 'invalid-token', message: `${channel} Bot Token 无效,请重新填写。` };
|
|
67
|
+
}
|
|
68
|
+
if (error?.code === 'discord-intents') {
|
|
69
|
+
return { code: 'discord-intents', message: error.message };
|
|
70
|
+
}
|
|
71
|
+
return { code: `${channel.toLowerCase()}-operation-failed`, message: `${channel} 操作失败,请稍后重试。` };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createTokenBotRpcHandler(controller, { channel }) {
|
|
75
|
+
for (const method of ['status', 'bindCredentials', 'reconnectBot', 'deleteBot']) {
|
|
76
|
+
if (typeof controller?.[method] !== 'function') {
|
|
77
|
+
throw new TypeError(`A complete ${channel} controller is required (${method})`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return async (endpoint, payload, signal) => {
|
|
81
|
+
if (signal?.aborted) {
|
|
82
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
|
|
83
|
+
}
|
|
84
|
+
if (!ENDPOINTS.includes(endpoint)) {
|
|
85
|
+
return { ok: false, error: { code: 'bad-request', message: `Unknown ${channel} endpoint.` } };
|
|
86
|
+
}
|
|
87
|
+
const invalid = payloadFailure(endpoint, payload);
|
|
88
|
+
if (invalid) return { ok: false, error: { code: 'bad-request', message: invalid } };
|
|
89
|
+
try {
|
|
90
|
+
let value;
|
|
91
|
+
if (endpoint === TOKEN_BOT_ENDPOINTS.status) value = await controller.status();
|
|
92
|
+
else if (endpoint === TOKEN_BOT_ENDPOINTS.bindCredentials) {
|
|
93
|
+
value = await controller.bindCredentials(payload);
|
|
94
|
+
} else if (endpoint === TOKEN_BOT_ENDPOINTS.reconnectBot) {
|
|
95
|
+
value = await controller.reconnectBot(payload.botId);
|
|
96
|
+
} else {
|
|
97
|
+
value = await controller.deleteBot(payload.botId);
|
|
98
|
+
}
|
|
99
|
+
return signal?.aborted
|
|
100
|
+
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
101
|
+
: { ok: true, value: sanitizePublic(value) };
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return signal?.aborted
|
|
104
|
+
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
105
|
+
: { ok: false, error: operationError(channel, error) };
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function installTokenBotRpc(ctx, controller, { channel, rpcChannel, authority }) {
|
|
111
|
+
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
|
112
|
+
throw new TypeError('DSH Host Connection RPC is required');
|
|
113
|
+
}
|
|
114
|
+
return ctx.connection.rpc.handle(
|
|
115
|
+
rpcChannel,
|
|
116
|
+
createTokenBotRpcHandler(controller, { channel }),
|
|
117
|
+
{ authority: resolveRpcAuthority(authority) },
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createProductionController } from './production.mjs';
|
|
2
|
+
import { installTelegramRpc } from './rpc.mjs';
|
|
3
|
+
|
|
4
|
+
export const name = 'dsh-im-telegram-host';
|
|
5
|
+
export const inject = ['connection', 'credentials', 'webServer'];
|
|
6
|
+
|
|
7
|
+
export async function apply(ctx, config = {}) {
|
|
8
|
+
if (config?.controller) {
|
|
9
|
+
return installTelegramRpc(ctx, config.controller, config.rpcAuthority);
|
|
10
|
+
}
|
|
11
|
+
const production = await createProductionController(ctx, config, config.internals ?? {});
|
|
12
|
+
const disposeRpc = installTelegramRpc(ctx, production.controller, config.rpcAuthority);
|
|
13
|
+
ctx.effect(() => async () => production.close(), 'dsh-im: close Telegram bot connections');
|
|
14
|
+
return disposeRpc;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createTelegramHostPlugin(config) {
|
|
18
|
+
return Object.freeze({ name, inject, apply: (ctx) => apply(ctx, config) });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { createProductionController } from './production.mjs';
|
|
22
|
+
export {
|
|
23
|
+
TELEGRAM_ENDPOINTS,
|
|
24
|
+
TELEGRAM_RPC_CHANNEL,
|
|
25
|
+
TELEGRAM_RPC_ENDPOINTS,
|
|
26
|
+
createTelegramRpcHandler,
|
|
27
|
+
installTelegramRpc,
|
|
28
|
+
} from './rpc.mjs';
|
|
29
|
+
export { TelegramController } from '../../../../src/channels/telegram/telegram-controller.mjs';
|
|
30
|
+
export { TelegramRuntime } from '../../../../src/channels/telegram/telegram-runtime.mjs';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TelegramConfigStore } from '../../../../src/channels/telegram/config-store.mjs';
|
|
2
|
+
import { TelegramHarnessClient } from '../../../../src/channels/telegram/harness-client.mjs';
|
|
3
|
+
import { TelegramController } from '../../../../src/channels/telegram/telegram-controller.mjs';
|
|
4
|
+
import { TelegramRuntime } from '../../../../src/channels/telegram/telegram-runtime.mjs';
|
|
5
|
+
import { TelegramStateStore } from '../../../../src/channels/telegram/state-store.mjs';
|
|
6
|
+
import { createTokenProductionController } from '../shared/production.mjs';
|
|
7
|
+
|
|
8
|
+
export function createProductionController(ctx, config = {}, internals = {}) {
|
|
9
|
+
return createTokenProductionController(ctx, config, internals, {
|
|
10
|
+
channel: 'telegram',
|
|
11
|
+
ConfigStore: TelegramConfigStore,
|
|
12
|
+
StateStore: TelegramStateStore,
|
|
13
|
+
HarnessClient: TelegramHarnessClient,
|
|
14
|
+
Controller: TelegramController,
|
|
15
|
+
Runtime: TelegramRuntime,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TOKEN_BOT_ENDPOINTS,
|
|
3
|
+
createTokenBotRpcHandler,
|
|
4
|
+
installTokenBotRpc,
|
|
5
|
+
} from '../shared/rpc.mjs';
|
|
6
|
+
|
|
7
|
+
export const TELEGRAM_RPC_CHANNEL = '/telegram';
|
|
8
|
+
export const TELEGRAM_ENDPOINTS = TOKEN_BOT_ENDPOINTS;
|
|
9
|
+
export const TELEGRAM_RPC_ENDPOINTS = Object.freeze(Object.values(TELEGRAM_ENDPOINTS));
|
|
10
|
+
|
|
11
|
+
export function createTelegramRpcHandler(controller) {
|
|
12
|
+
return createTokenBotRpcHandler(controller, { channel: 'Telegram' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function installTelegramRpc(ctx, controller, authority) {
|
|
16
|
+
return installTokenBotRpc(ctx, controller, {
|
|
17
|
+
channel: 'Telegram',
|
|
18
|
+
rpcChannel: TELEGRAM_RPC_CHANNEL,
|
|
19
|
+
authority,
|
|
20
|
+
});
|
|
21
|
+
}
|