mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 04:13:17 +08:00
172 lines
5.4 KiB
JavaScript
172 lines
5.4 KiB
JavaScript
const ACTIVE_REGISTRATION_STATES = new Set([
|
|
'starting',
|
|
'qr_ready',
|
|
'polling',
|
|
'slow_down',
|
|
'domain_switched',
|
|
]);
|
|
|
|
function credentialResult(result) {
|
|
const appId = result?.client_id ?? result?.appId;
|
|
const appSecret = result?.client_secret ?? result?.appSecret;
|
|
if (typeof appId !== 'string' || appId.length === 0
|
|
|| typeof appSecret !== 'string' || appSecret.length === 0) {
|
|
throw new TypeError('Feishu registration returned invalid credentials');
|
|
}
|
|
return {
|
|
appId,
|
|
appSecret,
|
|
userInfo: result?.user_info ?? result?.userInfo,
|
|
};
|
|
}
|
|
|
|
async function readConnectionStatus(connectionManager) {
|
|
if (typeof connectionManager.status !== 'function') return {};
|
|
return await connectionManager.status();
|
|
}
|
|
|
|
function isConnected(status) {
|
|
if (status?.connected === true) return true;
|
|
return status?.ready === true
|
|
&& status?.feishuLongConnectionState === 'connected'
|
|
&& status?.harnessReachable === true;
|
|
}
|
|
|
|
/**
|
|
* Minimal orchestration boundary between QR provisioning and the live bot.
|
|
*
|
|
* `createProvisioningManager` receives the only callback that can observe the
|
|
* App Secret. The callback persists credentials and starts the long-lived
|
|
* connection before the provisioning manager may report `succeeded`.
|
|
*/
|
|
export class ProvisioningBackedController {
|
|
#credentialStore;
|
|
#connectionManager;
|
|
#registrationOptions;
|
|
#manager;
|
|
#knownConfigured = false;
|
|
#lastError = null;
|
|
|
|
constructor({
|
|
createProvisioningManager,
|
|
credentialStore,
|
|
connectionManager,
|
|
registrationOptions = {},
|
|
} = {}) {
|
|
if (typeof createProvisioningManager !== 'function') {
|
|
throw new TypeError('createProvisioningManager is required');
|
|
}
|
|
if (!credentialStore
|
|
|| typeof credentialStore.save !== 'function'
|
|
|| typeof credentialStore.clear !== 'function') {
|
|
throw new TypeError('credentialStore.save/clear are required');
|
|
}
|
|
if (!connectionManager
|
|
|| typeof connectionManager.connect !== 'function'
|
|
|| typeof connectionManager.disconnect !== 'function') {
|
|
throw new TypeError('connectionManager.connect/disconnect are required');
|
|
}
|
|
if (registrationOptions === null
|
|
|| typeof registrationOptions !== 'object'
|
|
|| Array.isArray(registrationOptions)) {
|
|
throw new TypeError('registrationOptions must be an object');
|
|
}
|
|
|
|
this.#credentialStore = credentialStore;
|
|
this.#connectionManager = connectionManager;
|
|
this.#registrationOptions = structuredClone(registrationOptions);
|
|
this.#manager = createProvisioningManager({
|
|
onCredentials: (result) => this.#acceptCredentials(result),
|
|
});
|
|
if (!this.#manager
|
|
|| typeof this.#manager.start !== 'function'
|
|
|| typeof this.#manager.status !== 'function'
|
|
|| typeof this.#manager.cancel !== 'function') {
|
|
throw new TypeError('The provisioning manager must implement start/status/cancel');
|
|
}
|
|
}
|
|
|
|
async startRegistration() {
|
|
this.#lastError = null;
|
|
await this.#manager.start(structuredClone(this.#registrationOptions));
|
|
return this.status();
|
|
}
|
|
|
|
async cancelRegistration() {
|
|
await this.#manager.cancel();
|
|
return this.status();
|
|
}
|
|
|
|
async disconnect() {
|
|
await this.#manager.cancel();
|
|
await this.#connectionManager.disconnect();
|
|
try {
|
|
await this.#credentialStore.clear();
|
|
this.#knownConfigured = false;
|
|
this.#lastError = null;
|
|
} catch {
|
|
// Do not reflect a credential provider's error text across the RPC
|
|
// boundary. It may include a backend path or secret reference detail.
|
|
this.#lastError = {
|
|
code: 'credential_removal_failed',
|
|
message: 'Unable to remove the Feishu credentials.',
|
|
};
|
|
}
|
|
return this.status();
|
|
}
|
|
|
|
async status() {
|
|
const registration = await this.#manager.status();
|
|
const connection = await readConnectionStatus(this.#connectionManager);
|
|
const connected = isConnected(connection);
|
|
let configured = this.#knownConfigured;
|
|
if (typeof this.#credentialStore.configured === 'function') {
|
|
try {
|
|
configured = await this.#credentialStore.configured();
|
|
} catch {
|
|
configured = this.#knownConfigured;
|
|
}
|
|
}
|
|
|
|
let phase = 'unconfigured';
|
|
if (connected) phase = 'connected';
|
|
else if (ACTIVE_REGISTRATION_STATES.has(registration?.state)) phase = 'registering';
|
|
else if (registration?.state === 'saving') phase = 'connecting';
|
|
else if (this.#lastError || registration?.state === 'error') phase = 'error';
|
|
else if (configured) phase = 'disconnected';
|
|
|
|
return {
|
|
phase,
|
|
connected,
|
|
configured,
|
|
registration,
|
|
connection,
|
|
error: this.#lastError ?? registration?.error ?? null,
|
|
};
|
|
}
|
|
|
|
async close() {
|
|
await this.#manager.cancel();
|
|
await this.#connectionManager.disconnect();
|
|
}
|
|
|
|
async #acceptCredentials(result) {
|
|
const credentials = credentialResult(result);
|
|
try {
|
|
await this.#credentialStore.save(credentials);
|
|
this.#knownConfigured = true;
|
|
await this.#connectionManager.connect(credentials);
|
|
this.#lastError = null;
|
|
} catch {
|
|
this.#lastError = {
|
|
code: 'connection_failed',
|
|
message: 'The bot was created, but its connection could not be started.',
|
|
};
|
|
throw new Error('Unable to activate the Feishu connection.');
|
|
}
|
|
}
|
|
}
|
|
|
|
export function createProvisioningBackedController(options) {
|
|
return new ProvisioningBackedController(options);
|
|
}
|