// plugin-src/host/channels/dingtalk/production.mjs import { unlink as unlink3 } from "node:fs/promises"; import { homedir } from "node:os"; import { join, resolve } from "node:path"; // src/channels/dingtalk/config-store.mjs import { createHash, randomUUID } from "node:crypto"; import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; var EMPTY_DOCUMENT = Object.freeze({ version: 1, bots: Object.freeze([]) }); var STORED_BOT_KEYS = /* @__PURE__ */ new Set(["clientId", "secretRef", "approvedSenders"]); function cleanString(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function digest(value) { return createHash("sha256").update(value).digest("hex"); } function safeBotId(value) { const id = cleanString(value); return id && /^dt_[a-f0-9]{24}$/.test(id) ? id : null; } function safeSecretRef(value) { const ref = cleanString(value); return ref && /^DSH_DINGTALK_BOT_SECRET_[A-F0-9]{24}$/.test(ref) ? ref : null; } function safeSenderKey(value) { const key = cleanString(value); return key && /^dt_sender_[a-f0-9]{32}$/.test(key) ? key : null; } function normalizeApprovedSender(value) { const record = typeof value === "string" ? { staffId: value } : value; if (!record || typeof record !== "object" || Array.isArray(record)) return null; const senderKey = safeSenderKey(record.senderKey); const staffId = cleanString(record.staffId); if (!senderKey || !staffId) return null; return Object.freeze({ senderKey, staffId, displayName: cleanString(record.displayName), approvedAt: cleanString(record.approvedAt) }); } function normalizeApprovedSenders(value) { if (!Array.isArray(value)) return null; const senders = value.map(normalizeApprovedSender); if (senders.some((sender) => sender === null)) return null; const ids = /* @__PURE__ */ new Set(); const keys = /* @__PURE__ */ new Set(); for (const sender of senders) { if (ids.has(sender.staffId) || keys.has(sender.senderKey)) return null; ids.add(sender.staffId); keys.add(sender.senderKey); } return Object.freeze(senders); } function deriveDingtalkBotIdentity(clientId) { const value = cleanString(clientId); if (!value) throw new TypeError("clientId is required"); const valueDigest = digest(value).slice(0, 24); return Object.freeze({ botId: `dt_${valueDigest}`, secretRef: `DSH_DINGTALK_BOT_SECRET_${valueDigest.toUpperCase()}` }); } function deriveDingtalkSenderKey() { return `dt_sender_${randomUUID().replaceAll("-", "")}`; } function maskDingtalkSenderId(staffId) { const value = cleanString(staffId); if (!value) return "\u9489\u9489\u7528\u6237"; return "\u8EAB\u4EFD\u5DF2\u9690\u85CF"; } function maskDingtalkClientId(clientId) { const value = cleanString(clientId); if (!value) return "\u9489\u9489\u673A\u5668\u4EBA"; if (value.length <= 8) return `${value.slice(0, 2)}\u2022\u2022\u2022\u2022`; return `${value.slice(0, 4)}\u2022\u2022\u2022\u2022${value.slice(-4)}`; } function normalizeBot(value, { stored = false } = {}) { if (!value || typeof value !== "object" || Array.isArray(value)) return null; if ("clientSecret" in value || "client_secret" in value || "deviceCode" in value) return null; if (stored && Object.keys(value).some((key) => !STORED_BOT_KEYS.has(key))) return null; const clientId = cleanString(value.clientId); const secretRef = safeSecretRef(value.secretRef); const approvedSenders = normalizeApprovedSenders(value.approvedSenders ?? []); if (!clientId || !secretRef || !approvedSenders) return null; const identity = deriveDingtalkBotIdentity(clientId); if (identity.secretRef !== secretRef) return null; const suppliedBotId = value.botId === void 0 ? identity.botId : safeBotId(value.botId); if (suppliedBotId !== identity.botId) return null; return Object.freeze({ botId: identity.botId, clientId, secretRef, approvedSenders }); } function normalizeDocument(value) { if (!value || value.version !== 1 || !Array.isArray(value.bots)) return null; const bots = value.bots.map((bot) => normalizeBot(bot, { stored: true })); if (bots.some((bot) => bot === null)) return null; const botIds = /* @__PURE__ */ new Set(); const clientIds = /* @__PURE__ */ new Set(); const secretRefs = /* @__PURE__ */ new Set(); for (const bot of bots) { if (botIds.has(bot.botId) || clientIds.has(bot.clientId) || secretRefs.has(bot.secretRef)) { return null; } botIds.add(bot.botId); clientIds.add(bot.clientId); secretRefs.add(bot.secretRef); } return Object.freeze({ version: 1, bots: Object.freeze(bots) }); } function storedDocument(document) { return { version: 1, bots: document.bots.map((bot) => ({ clientId: bot.clientId, secretRef: bot.secretRef, approvedSenders: bot.approvedSenders.map((sender) => ({ senderKey: sender.senderKey, staffId: sender.staffId, displayName: sender.displayName, approvedAt: sender.approvedAt })) })) }; } var DingtalkConfigStore = class { #path; #value = EMPTY_DOCUMENT; #writeQueue = Promise.resolve(); /** @param {string} path Absolute or process-relative configuration file path. */ constructor(path) { if (!cleanString(path)) throw new TypeError("config path is required"); this.#path = path; } /** @returns {Promise} Loaded store. */ async load() { try { const normalized = normalizeDocument(JSON.parse(await readFile(this.#path, "utf8"))); if (!normalized) throw new Error("dsh-dingtalk config contains invalid bot data"); this.#value = normalized; } catch (error) { if (error?.code !== "ENOENT") throw error; this.#value = EMPTY_DOCUMENT; } return this; } /** @returns {Array} Cloned bot configurations with derived bot IDs. */ list() { return structuredClone(this.#value.bots); } /** @param {string} botId Derived bot ID. @returns {object|null} Bot configuration. */ get(botId) { const found = this.#value.bots.find((bot) => bot.botId === botId); return found ? structuredClone(found) : null; } /** @param {string} clientId DingTalk client ID. @returns {object|null} Bot configuration. */ getByClientId(clientId) { const found = this.#value.bots.find((bot) => bot.clientId === clientId); return found ? structuredClone(found) : null; } /** @param {object} value Bot configuration without a client secret. @returns {Promise} Saved config. */ async save(value) { const normalized = normalizeBot(value); if (!normalized) throw new Error("Refusing to persist invalid dsh-dingtalk bot data"); return this.#mutate((bots) => { const collision = bots.find( (bot) => (bot.clientId === normalized.clientId || bot.secretRef === normalized.secretRef) && bot.botId !== normalized.botId ); if (collision) throw new Error("Duplicate DingTalk bot identity"); const index = bots.findIndex((bot) => bot.botId === normalized.botId); if (index === -1) bots.push(normalized); else bots[index] = normalized; return structuredClone(normalized); }); } /** @param {string} botId Derived bot ID. @returns {Promise} Removed config. */ async remove(botId) { if (!safeBotId(botId)) throw new TypeError("Invalid DingTalk bot id"); return this.#mutate((bots) => { const index = bots.findIndex((bot) => bot.botId === botId); if (index === -1) return null; const [removed] = bots.splice(index, 1); return structuredClone(removed); }); } /** Removes the configuration file and resets the in-memory store. */ async clear() { const operation = this.#writeQueue.then(async () => { try { await unlink(this.#path); } catch (error) { if (error?.code !== "ENOENT") throw error; } this.#value = EMPTY_DOCUMENT; }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; } async #mutate(mutator) { let result; const operation = this.#writeQueue.then(async () => { const bots = [...this.#value.bots]; result = mutator(bots); const document = Object.freeze({ version: 1, bots: Object.freeze(bots) }); await this.#write(document); this.#value = document; }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; return result; } async #write(document) { await mkdir(dirname(this.#path), { recursive: true, mode: 448 }); const temporary = `${this.#path}.${process.pid}.${randomUUID()}.tmp`; try { await writeFile(temporary, `${JSON.stringify(storedDocument(document), null, 2)} `, { encoding: "utf8", flag: "wx", mode: 384 }); await rename(temporary, this.#path); } catch (error) { try { await unlink(temporary); } catch (cleanupError) { if (cleanupError?.code !== "ENOENT") throw new AggregateError([error, cleanupError]); } throw error; } } }; // src/channels/dingtalk/device-auth.mjs var DEFAULT_REGISTRATION_BASE_URL = "https://oapi.dingtalk.com"; var REGISTRATION_SOURCE = "DING_DWS_CLAW"; function cleanString2(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function positiveNumber(value, fallback) { const number = Number(value); return Number.isFinite(number) && number > 0 ? number : fallback; } function normalizeBaseUrl(value) { let url; try { url = new URL(cleanString2(value) ?? DEFAULT_REGISTRATION_BASE_URL); } catch { throw new TypeError("DingTalk registration base URL must be a valid HTTPS URL"); } const isDingtalkHost2 = url.hostname === "dingtalk.com" || url.hostname.endsWith(".dingtalk.com"); if (url.protocol !== "https:" || url.port || !isDingtalkHost2 || url.username || url.password || url.search || url.hash) { throw new TypeError("DingTalk registration base URL must be a valid HTTPS URL"); } url.pathname = url.pathname.replace(/\/+$/, ""); return url.href.replace(/\/$/, ""); } function readNow(clock) { const value = typeof clock?.now === "function" ? clock.now() : clock(); if (!Number.isFinite(value)) throw new TypeError("clock must return a finite timestamp"); return value; } function assertRecord(value, action) { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new DingtalkDeviceAuthError( "invalid-response", `DingTalk ${action} returned an invalid response`, action ); } if (Number(value.errcode) !== 0) { throw new DingtalkDeviceAuthError( "api-error", `DingTalk ${action} request was rejected`, action ); } return value; } var DingtalkDeviceAuthError = class extends Error { /** * @param {string} code Stable failure code. * @param {string} message Safe diagnostic that does not include response credentials. * @param {string} action Registration stage that failed. * @param {{cause?: unknown}} [options] Optional underlying error. */ constructor(code, message, action, options = {}) { super(message, options); this.name = "DingtalkDeviceAuthError"; this.code = code; this.action = action; } }; var DingtalkDeviceAuth = class { #fetch; #clock; #baseUrl; #timeoutMs; /** * @param {{fetch?: typeof globalThis.fetch, clock?: {now(): number}|(()=>number), baseUrl?: string, timeoutMs?: number}} [options] * Device-registration dependencies. */ constructor({ fetch: fetch2 = globalThis.fetch, clock = Date, baseUrl = DEFAULT_REGISTRATION_BASE_URL, timeoutMs = 15e3 } = {}) { if (typeof fetch2 !== "function") throw new TypeError("fetch is required"); if (typeof clock !== "function" && typeof clock?.now !== "function") { throw new TypeError("clock must be a function or expose now()"); } if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { throw new TypeError("timeoutMs must be a positive number"); } this.#fetch = fetch2; this.#clock = clock; this.#baseUrl = normalizeBaseUrl(baseUrl); this.#timeoutMs = timeoutMs; } /** * Starts a QR registration and returns the host-only device code with QR metadata. * @param {{signal?: AbortSignal}} [options] Optional cancellation signal. * @returns {Promise} Device registration details. */ async start({ signal } = {}) { const initialized = await this.#post( "/app/registration/init", { source: REGISTRATION_SOURCE }, "initialization", signal ); const nonce = cleanString2(initialized.nonce); if (!nonce) { throw new DingtalkDeviceAuthError( "missing-nonce", "DingTalk registration initialization did not return a nonce", "initialization" ); } const begun = await this.#post( "/app/registration/begin", { nonce }, "begin", signal ); const deviceCode = cleanString2(begun.device_code); const verificationUrl = cleanString2(begun.verification_uri_complete); if (!deviceCode || !verificationUrl) { throw new DingtalkDeviceAuthError( "incomplete-registration", "DingTalk registration did not return complete QR metadata", "begin" ); } const expiresInSeconds = positiveNumber(begun.expires_in, 7200); const pollIntervalMs = positiveNumber(begun.interval, 5) * 1e3; return Object.freeze({ deviceCode, verificationUrl, verificationUri: cleanString2(begun.verification_uri), userCode: cleanString2(begun.user_code), expiresAt: readNow(this.#clock) + expiresInSeconds * 1e3, pollIntervalMs }); } /** * Polls one registration attempt. * @param {{deviceCode: string, signal?: AbortSignal}|string} request Host-only device code. * @returns {Promise} Normalized registration state and credentials on success. */ async poll(request) { const deviceCode = cleanString2(typeof request === "string" ? request : request?.deviceCode); const signal = typeof request === "object" ? request?.signal : void 0; if (!deviceCode) throw new TypeError("deviceCode is required"); const response = await this.#post( "/app/registration/poll", { device_code: deviceCode }, "poll", signal ); const rawStatus = cleanString2(response.status)?.toUpperCase(); const status = ["WAITING", "SUCCESS", "FAIL", "EXPIRED"].includes(rawStatus) ? rawStatus : "UNKNOWN"; return Object.freeze({ status, clientId: cleanString2(response.client_id), clientSecret: cleanString2(response.client_secret), failReason: cleanString2(response.fail_reason) }); } async #post(path, body, action, signal) { let response; const timeoutSignal = AbortSignal.timeout(this.#timeoutMs); const requestSignal3 = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal; try { response = await this.#fetch(`${this.#baseUrl}${path}`, { method: "POST", headers: { accept: "application/json", "content-type": "application/json" }, body: JSON.stringify(body), redirect: "error", signal: requestSignal3 }); } catch (error) { if (signal?.aborted) throw signal.reason ?? error; if (timeoutSignal.aborted) { throw new DingtalkDeviceAuthError( "timeout", `DingTalk ${action} request timed out`, action, { cause: error } ); } if (error?.name === "AbortError") throw error; throw new DingtalkDeviceAuthError( "network-error", `DingTalk ${action} request could not be completed`, action, { cause: error } ); } if (!response || response.ok === false || typeof response.json !== "function") { throw new DingtalkDeviceAuthError( "http-error", `DingTalk ${action} request failed`, action ); } let value; try { value = await response.json(); } catch (error) { throw new DingtalkDeviceAuthError( "invalid-json", `DingTalk ${action} returned invalid JSON`, action, { cause: error } ); } return assertRecord(value, action); } }; // src/channels/dingtalk/dingtalk-controller.mjs import { randomUUID as randomUUID2 } from "node:crypto"; var ACTIVE_ATTEMPT_STATES = /* @__PURE__ */ new Set(["starting", "pending", "connecting"]); var TERMINAL_ATTEMPT_STATES = /* @__PURE__ */ new Set(["connected", "expired", "failed", "cancelled"]); function cleanString3(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function safeError(code, message) { return Object.freeze({ code, message }); } function nowFrom(clock) { return typeof clock?.now === "function" ? clock.now() : clock(); } function isoNow(clock) { return new Date(nowFrom(clock)).toISOString(); } function abortError() { return new DOMException("DingTalk provisioning was cancelled", "AbortError"); } function publicAttempt(record) { if (!record) return null; return { attemptId: record.id, status: record.state, ...record.verificationUrl ? { verificationUrl: record.verificationUrl } : {}, ...record.expiresAt ? { expiresAt: record.expiresAt } : {}, ...record.pollIntervalMs ? { pollIntervalMs: record.pollIntervalMs } : {}, ...record.botId ? { botId: record.botId } : {}, ...record.alreadyConnected ? { alreadyConnected: true } : {}, ...record.error ? { error: structuredClone(record.error) } : {} }; } function runtimeStatus(runtime) { if (!runtime) return {}; const value = typeof runtime.status === "function" ? runtime.status() : runtime.status; return value && typeof value === "object" && !Array.isArray(value) ? value : {}; } function isRuntimeConnected(runtime, status) { if (!runtime) return false; if (status.connected === false || status.ready === false) return false; const state = cleanString3( status.dingtalkStreamState ?? status.dingtalkConnectionState ?? status.connectionState ?? status.state )?.toLowerCase(); if (["failed", "error", "offline", "disconnected", "stopped"].includes(state)) return false; return status.connected === true || status.ready === true || state === "connected" || state === "ready"; } function normalizePendingSender(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return null; const staffId = cleanString3(value.staffId ?? value.senderStaffId ?? value.senderId); if (!staffId) return null; const suppliedRequestId = cleanString3(value.requestId); const opaqueRequestId = suppliedRequestId && /^ding_sender_[A-Za-z0-9_-]{1,100}$/.test(suppliedRequestId) && !suppliedRequestId.includes(staffId) ? suppliedRequestId : null; if (!opaqueRequestId) return null; return { requestId: opaqueRequestId, staffId, displayName: cleanString3(value.displayName ?? value.senderName ?? value.senderNick) ?? "\u9489\u9489\u7528\u6237", requestedAt: cleanString3(value.requestedAt) }; } function internalPendingSenders(status) { if (!Array.isArray(status.pendingSenders)) return []; const seen = /* @__PURE__ */ new Set(); const senders = []; for (const value of status.pendingSenders) { const sender = normalizePendingSender(value); if (!sender || seen.has(sender.staffId)) continue; seen.add(sender.staffId); senders.push(sender); } return senders; } function publicPendingSender(sender) { return { requestId: sender.requestId, displayName: sender.displayName, senderIdMasked: maskDingtalkSenderId(sender.staffId), requestedAt: sender.requestedAt }; } function publicApprovedSender(sender) { return { senderKey: sender.senderKey, displayName: cleanString3(sender.displayName) ?? "\u9489\u9489\u7528\u6237", senderIdMasked: maskDingtalkSenderId(sender.staffId), approvedAt: cleanString3(sender.approvedAt) }; } var DingtalkController = class { #deviceAuth; #credentials; #configStore; #createRuntime; #deleteState; #logger; #clock; #runtimes = /* @__PURE__ */ new Map(); #errors = /* @__PURE__ */ new Map(); #attempts = /* @__PURE__ */ new Map(); #activeAttemptId = null; #transitions = /* @__PURE__ */ new Map(); #revision = 0; #closed = false; /** * @param {object} options Controller dependencies. * @param {object} options.deviceAuth Host-only DingTalk device auth client. * @param {object} options.credentials DSH credential provider. * @param {object} options.configStore Loaded DingTalk config store. * @param {Function} options.createRuntime Runtime factory. * @param {Function} [options.deleteState] Per-bot state cleanup callback. * @param {Console} [options.logger] Host logger. * @param {{now(): number}|(()=>number)} [options.clock] Injectable clock. */ constructor({ deviceAuth, credentials, configStore, createRuntime, deleteState = async () => { }, logger = console, clock = Date }) { if (!deviceAuth || typeof deviceAuth.start !== "function" || typeof deviceAuth.poll !== "function") { throw new TypeError("DingtalkController requires a DingTalk device auth client"); } if (!credentials || typeof credentials.resolve !== "function" || typeof credentials.set !== "function" || typeof credentials.unset !== "function") { throw new TypeError("DingtalkController requires the DSH credential provider"); } if (!configStore || typeof configStore.list !== "function" || typeof configStore.get !== "function" || typeof configStore.getByClientId !== "function" || typeof configStore.save !== "function" || typeof configStore.remove !== "function") { throw new TypeError("DingtalkController requires a loaded config store"); } if (typeof createRuntime !== "function") throw new TypeError("createRuntime is required"); if (typeof deleteState !== "function") throw new TypeError("deleteState must be a function"); if (typeof clock !== "function" && typeof clock?.now !== "function") { throw new TypeError("clock must be a function or expose now()"); } this.#deviceAuth = deviceAuth; this.#credentials = credentials; this.#configStore = configStore; this.#createRuntime = createRuntime; this.#deleteState = deleteState; this.#logger = logger; this.#clock = clock; } /** Starts all configured DingTalk runtimes whose secrets are available. */ async initialize() { if (this.#closed) return this.status(); for (const config of this.#configStore.list()) { const current = this.#runtimes.get(config.botId); try { if (isRuntimeConnected(current, runtimeStatus(current))) continue; } catch { } await this.#withBotTransition(config.botId, async () => { const latest = this.#configStore.get(config.botId); if (!latest || this.#closed) return; const clientSecret = await this.#resolveSecret(latest.secretRef); if (!clientSecret) { this.#errors.set( latest.botId, safeError("missing-secret", "\u9489\u9489\u673A\u5668\u4EBA\u51ED\u636E\u7F3A\u5931\uFF0C\u8BF7\u79FB\u9664\u540E\u91CD\u65B0\u626B\u7801\u3002") ); this.#touch(); return; } try { await this.#startRuntime(latest, clientSecret); this.#errors.delete(latest.botId); } catch { this.#errors.set( latest.botId, safeError("connection-failed", "\u9489\u9489\u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002") ); this.#logger.warn?.(`[dsh-dingtalk] bot ${latest.botId} failed to initialize`); } this.#touch(); }); } return this.status(); } /** Starts one DingTalk QR registration, cancelling any prior active attempt. */ async startProvisioning({ signal } = {}) { if (this.#closed) throw new Error("dsh-dingtalk controller is closed"); if (this.#activeAttemptId) await this.cancelProvisioning(this.#activeAttemptId); const record = { id: randomUUID2(), state: "starting", controller: new AbortController(), deviceCode: null, verificationUrl: null, expiresAt: null, pollIntervalMs: null, pollTask: null, botId: null, alreadyConnected: false, error: null }; this.#attempts.set(record.id, record); this.#activeAttemptId = record.id; this.#touch(); const abortFromRequest = () => record.controller.abort(signal?.reason); if (signal?.aborted) abortFromRequest(); else signal?.addEventListener("abort", abortFromRequest, { once: true }); try { const begun = await this.#deviceAuth.start({ signal: record.controller.signal }); this.#assertAttemptActive(record); record.deviceCode = cleanString3(begun.deviceCode); record.verificationUrl = cleanString3(begun.verificationUrl); record.expiresAt = Number(begun.expiresAt); record.pollIntervalMs = Number(begun.pollIntervalMs); if (!record.deviceCode || !record.verificationUrl || !Number.isFinite(record.expiresAt) || !Number.isFinite(record.pollIntervalMs) || record.pollIntervalMs <= 0) { throw new Error("DingTalk device auth returned incomplete registration metadata"); } record.state = "pending"; this.#touch(); return publicAttempt(record); } catch (error) { if (record.controller.signal.aborted || error?.name === "AbortError") { record.state = "cancelled"; record.error = safeError("cancelled", "\u626B\u7801\u63A5\u5165\u5DF2\u53D6\u6D88\u3002"); } else { record.state = "failed"; record.error = safeError("qr-start-failed", "\u65E0\u6CD5\u751F\u6210\u9489\u9489\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"); } if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; this.#touch(); if (record.state === "failed") throw error; return publicAttempt(record); } finally { signal?.removeEventListener("abort", abortFromRequest); } } /** Binds one DingTalk application with an existing Client ID and Client Secret. */ async bindCredentials({ clientId, clientSecret } = {}) { if (this.#closed) throw new Error("dsh-dingtalk controller is closed"); const normalizedClientId = cleanString3(clientId); const normalizedSecret = cleanString3(clientSecret); if (!normalizedClientId || !normalizedSecret) { throw new TypeError("DingTalk Client ID and Client Secret are required"); } if (this.#activeAttemptId) await this.cancelProvisioning(this.#activeAttemptId); if (this.#closed) throw new Error("dsh-dingtalk controller is closed"); const identity = deriveDingtalkBotIdentity(normalizedClientId); await this.#withBotTransition(identity.botId, async () => { if (this.#closed) throw abortError(); const previousConfig = this.#configStore.getByClientId(normalizedClientId); const previousSecret = await this.#credentials.resolve(identity.secretRef).catch(() => void 0); if (this.#closed) throw abortError(); const config = { botId: identity.botId, clientId: normalizedClientId, secretRef: identity.secretRef, approvedSenders: previousConfig?.approvedSenders ?? [] }; await this.#credentials.set(identity.secretRef, normalizedSecret); try { await this.#configStore.save(config); } catch (error) { await this.#restoreCredential(identity.secretRef, previousSecret); throw error; } try { await this.#startRuntime(config, normalizedSecret); this.#errors.delete(identity.botId); } catch { this.#errors.set( identity.botId, safeError("connection-failed", "\u9489\u9489\u5DF2\u63A5\u5165\uFF0C\u4F46\u6D88\u606F\u8FDE\u63A5\u6682\u672A\u5C31\u7EEA\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002") ); this.#logger.warn?.("[dsh-dingtalk] credential-bound bot saved but its connection is not ready"); } this.#touch(); }); return this.status(); } /** Polls one QR registration without exposing its device code or returned secret. */ async registrationStatus(attemptId) { const record = this.#attempts.get(attemptId); if (!record) return null; if (TERMINAL_ATTEMPT_STATES.has(record.state) || record.state === "starting") { return publicAttempt(record); } if (nowFrom(this.#clock) >= record.expiresAt) { record.state = "expired"; record.error = safeError("expired", "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u3002"); if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; this.#touch(); return publicAttempt(record); } if (!record.pollTask) { const task = this.#pollRegistration(record).finally(() => { if (record.pollTask === task) record.pollTask = null; }); record.pollTask = task; } await record.pollTask; return publicAttempt(record); } /** Cancels an active QR registration. */ async cancelProvisioning(attemptId) { const record = this.#attempts.get(attemptId); if (!record) return null; if (!TERMINAL_ATTEMPT_STATES.has(record.state)) { record.controller.abort(); await record.pollTask?.catch(() => void 0); if (!TERMINAL_ATTEMPT_STATES.has(record.state)) record.state = "cancelled"; record.error ??= safeError("cancelled", "\u626B\u7801\u63A5\u5165\u5DF2\u53D6\u6D88\u3002"); } if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; this.#touch(); return publicAttempt(record); } /** Replaces one bot runtime using its stored credential. */ async reconnectBot(botId) { const config = this.#configStore.get(botId); if (!config) throw new Error("Unknown DingTalk bot"); await this.#withBotTransition(botId, async () => { const clientSecret = await this.#resolveSecret(config.secretRef); if (!clientSecret) throw new Error("The DingTalk client secret is missing"); try { await this.#startRuntime(config, clientSecret); this.#errors.delete(botId); } catch (error) { this.#errors.set( botId, safeError("connection-failed", "\u9489\u9489\u8FDE\u63A5\u4ECD\u672A\u5C31\u7EEA\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002") ); throw error; } finally { this.#touch(); } }); return this.status(); } /** Removes one bot, its secret, runtime, and local conversation state. */ async deleteBot(botId) { const config = this.#configStore.get(botId); if (!config) throw new Error("Unknown DingTalk bot"); await this.#withBotTransition(botId, async () => { const previousSecret = await this.#credentials.resolve(config.secretRef).catch(() => void 0); await this.#stopRuntime(botId); try { await this.#credentials.unset(config.secretRef); await this.#configStore.remove(botId); } catch (error) { if (cleanString3(previousSecret?.value)) { await this.#credentials.set(config.secretRef, previousSecret.value).catch(() => void 0); await this.#startRuntime(config, previousSecret.value).catch(() => void 0); } throw new Error("Unable to remove the DingTalk bot safely.", { cause: error }); } try { await this.#deleteState({ botId, config }); } catch { this.#logger.warn?.(`[dsh-dingtalk] bot ${botId} state cleanup failed`); } this.#errors.delete(botId); this.#touch(); }); return this.status(); } /** Approves one opaque pending-sender request for a bot. */ async approveSender(botId, requestId) { const config = this.#configStore.get(botId); if (!config) throw new Error("Unknown DingTalk bot"); const runtime = this.#runtimes.get(botId); const direct = typeof runtime?.pendingSender === "function" ? normalizePendingSender(runtime.pendingSender(requestId)) : null; const pending = internalPendingSenders(runtimeStatus(runtime)); const sender = direct?.requestId === requestId ? direct : pending.find((candidate) => candidate.requestId === requestId); if (!sender) throw new Error("Unknown DingTalk sender approval request"); if (config.approvedSenders.some((approved) => approved.staffId === sender.staffId)) { return this.status(); } const updated = { ...config, approvedSenders: [ ...config.approvedSenders, { senderKey: deriveDingtalkSenderKey(), staffId: sender.staffId, displayName: sender.displayName, approvedAt: isoNow(this.#clock) } ] }; await this.#saveAndRestart(config, updated); return this.status(); } /** Revokes one approved sender by its browser-safe sender key. */ async revokeSender(botId, senderKey) { const config = this.#configStore.get(botId); if (!config) throw new Error("Unknown DingTalk bot"); const index = config.approvedSenders.findIndex( (sender) => sender.senderKey === senderKey ); if (index === -1) throw new Error("Unknown approved DingTalk sender"); const approvedSenders = [...config.approvedSenders]; approvedSenders.splice(index, 1); await this.#saveAndRestart(config, { ...config, approvedSenders }); return this.status(); } /** Returns browser-safe bot, health, and sender-approval state. */ status() { const bots = this.#configStore.list().map((config) => { const runtime = this.#runtimes.get(config.botId); let currentStatus = {}; try { currentStatus = runtimeStatus(runtime); } catch { currentStatus = { state: "error" }; } const connected = isRuntimeConnected(runtime, currentStatus); const accountError = this.#errors.get(config.botId); const state = connected ? "connected" : accountError ? "error" : "offline"; const approvedIds = new Set(config.approvedSenders.map((sender) => sender.staffId)); const pending = internalPendingSenders(currentStatus).filter((sender) => !approvedIds.has(sender.staffId)).map(publicPendingSender); return { botId: config.botId, state, connected, configured: true, bot: { name: "\u9489\u9489\u673A\u5668\u4EBA", clientIdMasked: maskDingtalkClientId(config.clientId) }, health: { status: connected ? "healthy" : accountError ? "error" : "offline", summary: connected ? "\u9489\u9489 Stream \u6D88\u606F\u8FDE\u63A5\u8FD0\u884C\u6B63\u5E38" : accountError?.message ?? "\u9489\u9489\u6D88\u606F\u8FDE\u63A5\u5F53\u524D\u79BB\u7EBF", lastCheckedAt: currentStatus.lastCheckedAt ?? null }, stats: { messagesReceived: Number(currentStatus.messagesReceived) || 0, messagesReplied: Number(currentStatus.messagesReplied) || 0 }, senders: { pending, approved: config.approvedSenders.map(publicApprovedSender) }, error: accountError ? structuredClone(accountError) : null }; }); const connectedCount = bots.filter((bot) => bot.connected).length; const active = this.#activeAttemptId ? this.#attempts.get(this.#activeAttemptId) : null; return { schemaVersion: 1, revision: this.#revision, state: active && ACTIVE_ATTEMPT_STATES.has(active.state) ? "provisioning" : bots.length === 0 ? "disconnected" : connectedCount === bots.length ? "connected" : connectedCount > 0 ? "degraded" : "offline", bots, totals: { configured: bots.length, connected: connectedCount }, ...active && ACTIVE_ATTEMPT_STATES.has(active.state) ? { provisioning: publicAttempt(active) } : {} }; } /** Cancels provisioning and stops every bot runtime. */ async close() { if (this.#closed) return; this.#closed = true; if (this.#activeAttemptId) await this.cancelProvisioning(this.#activeAttemptId); await Promise.allSettled([...this.#runtimes.keys()].map((botId) => this.#stopRuntime(botId))); await Promise.allSettled([...this.#transitions.values()]); await Promise.allSettled([...this.#runtimes.keys()].map((botId) => this.#stopRuntime(botId))); } async #pollRegistration(record) { try { this.#assertAttemptActive(record); const response = await this.#deviceAuth.poll({ deviceCode: record.deviceCode, signal: record.controller.signal }); this.#assertAttemptActive(record); const state = cleanString3(response.status)?.toUpperCase(); if (state === "WAITING") { record.state = "pending"; record.error = null; } else if (state === "SUCCESS") { const clientId = cleanString3(response.clientId); const clientSecret = cleanString3(response.clientSecret); if (!clientId || !clientSecret) throw new Error("DingTalk returned incomplete credentials"); record.state = "connecting"; record.error = null; this.#touch(); const activation = await this.#activateBot(record, { clientId, clientSecret }); record.botId = activation.botId; record.alreadyConnected = activation.alreadyConnected; record.state = "connected"; if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; } else if (state === "EXPIRED") { record.state = "expired"; record.error = safeError("expired", "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u3002"); if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; } else if (state === "FAIL") { record.state = "failed"; record.error = safeError("authorization-failed", "\u9489\u9489\u672A\u5B8C\u6210\u673A\u5668\u4EBA\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u626B\u7801\u3002"); if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; } else { record.state = "pending"; record.error = safeError("poll-pending", "\u9489\u9489\u6388\u6743\u72B6\u6001\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u6B63\u5728\u91CD\u8BD5\u3002"); } } catch (error) { if (record.controller.signal.aborted || error?.name === "AbortError") { record.state = "cancelled"; record.error = safeError("cancelled", "\u626B\u7801\u63A5\u5165\u5DF2\u53D6\u6D88\u3002"); if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; } else if (record.state === "connecting") { record.state = "failed"; record.error = safeError( "activation-failed", "\u9489\u9489\u5DF2\u6388\u6743\uFF0C\u4F46\u65E0\u6CD5\u5B89\u5168\u4FDD\u5B58\u63A5\u5165\u914D\u7F6E\u3002" ); if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; this.#logger.error?.("[dsh-dingtalk] bot activation failed"); } else { record.state = "pending"; record.error = safeError("poll-failed", "\u9489\u9489\u6388\u6743\u67E5\u8BE2\u6682\u65F6\u5931\u8D25\uFF0C\u6B63\u5728\u91CD\u8BD5\u3002"); } } finally { this.#touch(); this.#pruneAttempts(); } } async #activateBot(record, { clientId, clientSecret }) { const identity = deriveDingtalkBotIdentity(clientId); const previousConfig = this.#configStore.getByClientId(clientId); const previousSecret = await this.#credentials.resolve(identity.secretRef).catch(() => void 0); const config = { botId: identity.botId, clientId, secretRef: identity.secretRef, approvedSenders: previousConfig?.approvedSenders ?? [] }; return this.#withBotTransition(identity.botId, async () => { const rollback = async () => { await this.#stopRuntime(identity.botId); if (previousConfig) await this.#configStore.save(previousConfig).catch(() => void 0); else if (this.#configStore.get(identity.botId)) { await this.#configStore.remove(identity.botId).catch(() => void 0); } await this.#restoreCredential(identity.secretRef, previousSecret); if (previousConfig && cleanString3(previousSecret?.value)) { await this.#startRuntime(previousConfig, previousSecret.value).catch(() => void 0); } }; await this.#credentials.set(identity.secretRef, clientSecret); try { this.#assertAttemptActive(record); await this.#configStore.save(config); this.#assertAttemptActive(record); } catch (error) { await rollback(); throw error; } try { await this.#startRuntime(config, clientSecret); this.#assertAttemptActive(record); this.#errors.delete(identity.botId); } catch (error) { if (record.controller.signal.aborted || this.#activeAttemptId !== record.id) { await rollback(); throw abortError(); } this.#errors.set( identity.botId, safeError("connection-failed", "\u9489\u9489\u5DF2\u63A5\u5165\uFF0C\u4F46\u6D88\u606F\u8FDE\u63A5\u6682\u672A\u5C31\u7EEA\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002") ); this.#logger.warn?.("[dsh-dingtalk] authorized bot saved but its connection is not ready"); } return { botId: identity.botId, alreadyConnected: Boolean(previousConfig) }; }); } async #saveAndRestart(previousConfig, nextConfig) { return this.#withBotTransition(previousConfig.botId, async () => { const clientSecret = await this.#resolveSecret(previousConfig.secretRef); if (!clientSecret) throw new Error("The DingTalk client secret is missing"); await this.#configStore.save(nextConfig); try { await this.#startRuntime(nextConfig, clientSecret); this.#errors.delete(previousConfig.botId); } catch (error) { await this.#configStore.save(previousConfig).catch(() => void 0); await this.#startRuntime(previousConfig, clientSecret).catch(() => void 0); this.#errors.set( previousConfig.botId, safeError("connection-failed", "\u9489\u9489\u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002") ); throw error; } finally { this.#touch(); } }); } async #startRuntime(config, clientSecret) { if (this.#closed) throw abortError(); await this.#stopRuntime(config.botId); if (this.#closed) throw abortError(); const runtime = await this.#createRuntime({ botId: config.botId, config: structuredClone(config), clientSecret }); if (!runtime || typeof runtime.start !== "function" || typeof runtime.stop !== "function") { throw new TypeError("createRuntime returned an invalid DingTalk runtime"); } if (this.#closed) { await runtime.stop().catch(() => void 0); throw abortError(); } this.#runtimes.set(config.botId, runtime); try { await runtime.start(); if (this.#closed) { await runtime.stop().catch(() => void 0); throw abortError(); } } catch (error) { if (this.#runtimes.get(config.botId) === runtime) this.#runtimes.delete(config.botId); await runtime.stop().catch(() => void 0); throw error; } } async #stopRuntime(botId) { const runtime = this.#runtimes.get(botId); this.#runtimes.delete(botId); await runtime?.stop().catch(() => { this.#logger.warn?.(`[dsh-dingtalk] bot ${botId} failed to stop cleanly`); }); } async #resolveSecret(secretRef) { const result = await this.#credentials.resolve(secretRef).catch(() => void 0); return cleanString3(result?.value); } async #restoreCredential(secretRef, previous) { try { if (cleanString3(previous?.value)) await this.#credentials.set(secretRef, previous.value); else await this.#credentials.unset(secretRef); } catch { this.#logger.error?.(`[dsh-dingtalk] failed to restore credential ${secretRef}`); } } #assertAttemptActive(record) { if (record.controller.signal.aborted || this.#activeAttemptId !== record.id) throw abortError(); } #withBotTransition(botId, operation) { if (this.#closed) return Promise.reject(new Error("dsh-dingtalk controller is closed")); const previous = this.#transitions.get(botId) ?? Promise.resolve(); const current = previous.catch(() => void 0).then(operation); const settled = current.finally(() => { if (this.#transitions.get(botId) === settled) this.#transitions.delete(botId); }); this.#transitions.set(botId, settled); return settled; } #pruneAttempts() { for (const [id, record] of this.#attempts) { if (id !== this.#activeAttemptId && TERMINAL_ATTEMPT_STATES.has(record.state) && this.#attempts.size > 16) { this.#attempts.delete(id); } } } #touch() { this.#revision += 1; } }; // src/channels/dingtalk/dingtalk-api.mjs import { randomUUID as randomUUID3 } from "node:crypto"; var DINGTALK_REGISTRATION_BASE_URL = "https://oapi.dingtalk.com/"; var DINGTALK_API_BASE_URL = "https://api.dingtalk.com/"; var DINGTALK_REGISTRATION_SOURCE = "DING_DWS_CLAW"; var DINGTALK_AI_CARD_TEMPLATE_ID = "02fcf2f4-5e02-4a85-b672-46d1f715543e.schema"; var DEFAULT_TIMEOUT_MS = 15e3; var REGISTRATION_STATUSES = /* @__PURE__ */ new Set(["WAITING", "SUCCESS", "FAIL", "EXPIRED"]); var DingtalkApiError = class extends Error { constructor(code, message, options = {}) { super(message, options); this.name = "DingtalkApiError"; this.code = code; this.status = options.status; } }; function nonEmptyString(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function isDingtalkHost(hostname) { const normalized = hostname.toLowerCase().replace(/\.$/, ""); return normalized === "dingtalk.com" || normalized.endsWith(".dingtalk.com"); } function normalizeTrustedUrl(value, { label, requireSubdomain = true } = {}) { let url; try { url = new URL(value); } catch { throw new DingtalkApiError("invalid-url", `${label ?? "\u9489\u9489\u670D\u52A1"}\u8FD4\u56DE\u4E86\u65E0\u6548\u5730\u5740\u3002`); } const normalizedHost = url.hostname.toLowerCase().replace(/\.$/, ""); const trustedHost = requireSubdomain ? normalizedHost !== "dingtalk.com" && isDingtalkHost(normalizedHost) : isDingtalkHost(normalizedHost); if (url.protocol !== "https:" || !trustedHost || url.port && url.port !== "443") { throw new DingtalkApiError("untrusted-url", `${label ?? "\u9489\u9489\u670D\u52A1"}\u5730\u5740\u4E0D\u53D7\u4FE1\u4EFB\u3002`); } if (url.username || url.password) { throw new DingtalkApiError("untrusted-url", `${label ?? "\u9489\u9489\u670D\u52A1"}\u5730\u5740\u4E0D\u53D7\u4FE1\u4EFB\u3002`); } return url; } function normalizeDingtalkSessionWebhook(value) { const text = nonEmptyString(value); if (!text) throw new DingtalkApiError("invalid-session-webhook", "\u9489\u9489\u6D88\u606F\u6CA1\u6709\u53EF\u7528\u7684\u56DE\u590D\u5730\u5740\u3002"); const url = normalizeTrustedUrl(text, { label: "\u9489\u9489\u56DE\u590D", requireSubdomain: false }); url.hash = ""; return url.toString(); } function splitDingtalkText(value, maxChars = 4e3) { const text = typeof value === "string" ? value.trim() : ""; if (!text) return []; if (!Number.isInteger(maxChars) || maxChars < 1) throw new TypeError("maxChars must be a positive integer"); if (text.length <= maxChars) return [text]; const chunks = []; let remaining = text; while (remaining.length > maxChars) { let splitAt = remaining.lastIndexOf("\n", maxChars); if (splitAt < Math.floor(maxChars * 0.6)) splitAt = maxChars; chunks.push(remaining.slice(0, splitAt)); remaining = remaining.slice(splitAt).replace(/^\n+/, ""); } if (remaining) chunks.push(remaining); return chunks; } function abortError2(signal) { if (signal?.reason instanceof Error) return signal.reason; return new DOMException("The operation was aborted", "AbortError"); } function abortableDelay(ms, signal) { if (ms <= 0) return Promise.resolve(); return new Promise((resolve7, reject) => { if (signal?.aborted) { reject(abortError2(signal)); return; } const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve7(); }, ms); const onAbort = () => { clearTimeout(timer); reject(abortError2(signal)); }; signal?.addEventListener("abort", onAbort, { once: true }); }); } async function requestJson(fetchImpl, url, { body, signal, timeoutMs = DEFAULT_TIMEOUT_MS, headers = {}, method = "POST", action = "request" } = {}) { const controller = new AbortController(); let timedOut = false; const onAbort = () => controller.abort(signal?.reason); if (signal?.aborted) throw abortError2(signal); signal?.addEventListener("abort", onAbort, { once: true }); const timer = timeoutMs > 0 ? setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs) : null; try { const response = await fetchImpl(url, { method, redirect: "error", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify(body ?? {}), signal: controller.signal }); if (!response.ok) { throw new DingtalkApiError( "http-error", `\u9489\u9489\u670D\u52A1\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${response.status}\uFF09\u3002`, { status: response.status } ); } try { return await response.json(); } catch (error) { throw new DingtalkApiError("invalid-response", "\u9489\u9489\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\u3002", { cause: error }); } } catch (error) { if (signal?.aborted) throw abortError2(signal); if (timedOut) throw new DingtalkApiError("timeout", "\u9489\u9489\u670D\u52A1\u8BF7\u6C42\u8D85\u65F6\u3002", { cause: error }); if (error instanceof DingtalkApiError) throw error; throw new DingtalkApiError("network-error", `\u6682\u65F6\u65E0\u6CD5\u5B8C\u6210\u9489\u9489${action}\u8BF7\u6C42\u3002`, { cause: error }); } finally { if (timer) clearTimeout(timer); signal?.removeEventListener("abort", onAbort); } } function normalizeCardTarget(target) { if (target?.type === "user") { const userId = nonEmptyString(target.userId); if (userId) return { type: "user", userId }; } if (target?.type === "group") { const openConversationId = nonEmptyString(target.openConversationId); if (openConversationId) return { type: "group", openConversationId }; } throw new TypeError("DingTalk AI Card target is invalid"); } function cardData(text, flowStatus) { return { cardParamMap: { flowStatus, msgContent: normalizeDingtalkCardMarkdown(text), staticMsgContent: "", sys_full_json_obj: JSON.stringify({ order: ["msgContent"] }), config: JSON.stringify({ autoLayout: true }) } }; } function cardDeliverBody(cardInstanceId, target, robotCode) { const base = { outTrackId: cardInstanceId, userIdType: 1 }; if (target.type === "group") { return { ...base, openSpaceId: `dtv1.card//IM_GROUP.${target.openConversationId}`, imGroupOpenDeliverModel: { robotCode } }; } return { ...base, openSpaceId: `dtv1.card//IM_ROBOT.${target.userId}`, imRobotOpenDeliverModel: { spaceType: "IM_ROBOT", robotCode, extension: { dynamicSummary: "true" } } }; } function normalizeDingtalkCardMarkdown(value) { const text = typeof value === "string" ? value.replace(/\r\n?/g, "\n") : ""; const lines = text.split("\n"); let inCodeBlock = false; return lines.map((line, index) => { const fenced = /^\s{0,3}```/.test(line); const currentInCodeBlock = inCodeBlock; if (fenced) inCodeBlock = !inCodeBlock; if (index === lines.length - 1) return line; if (currentInCodeBlock || fenced || inCodeBlock || !line || !lines[index + 1]) return `${line} `; if (/^\s{0,3}(?:[-*+] |\d+[.)] |#{1,6} |\||> )/.test(lines[index + 1])) return `${line} `; return `${line}
`; }).join(""); } function assertRegistrationOk(value, action) { if (!value || typeof value !== "object" || value.errcode !== 0) { throw new DingtalkApiError( "registration-rejected", `\u9489\u9489\u626B\u7801${action}\u5931\u8D25\u3002` ); } return value; } function positiveNumber2(value, fallback) { const number = Number(value); return Number.isFinite(number) && number > 0 ? number : fallback; } function createDingtalkApi({ fetchImpl = fetch, registrationBaseUrl = process.env.DINGTALK_REGISTRATION_BASE_URL || DINGTALK_REGISTRATION_BASE_URL, registrationSource = process.env.DINGTALK_REGISTRATION_SOURCE || DINGTALK_REGISTRATION_SOURCE, now = () => Date.now(), cardMinIntervalMs = 50, cardBackoffMs = 1e3, delay: delay3 = abortableDelay } = {}) { if (typeof fetchImpl !== "function") throw new TypeError("fetchImpl must be a function"); if (typeof now !== "function") throw new TypeError("now must be a function"); if (!Number.isFinite(cardMinIntervalMs) || cardMinIntervalMs < 0) { throw new TypeError("cardMinIntervalMs must be a non-negative number"); } if (!Number.isFinite(cardBackoffMs) || cardBackoffMs < 0) { throw new TypeError("cardBackoffMs must be a non-negative number"); } if (typeof delay3 !== "function") throw new TypeError("delay must be a function"); const registrationBase = normalizeTrustedUrl(registrationBaseUrl, { label: "\u9489\u9489\u6CE8\u518C\u670D\u52A1", requireSubdomain: false }); const apiBase = new URL(DINGTALK_API_BASE_URL); const source = nonEmptyString(registrationSource); if (!source) throw new TypeError("registrationSource is required"); const tokenCache = /* @__PURE__ */ new Map(); const tokenRequests = /* @__PURE__ */ new Map(); let cardSlotTail = Promise.resolve(); let nextCardRequestAt = 0; const endpoint = (base, pathname) => new URL(pathname.replace(/^\//, ""), base); async function accessToken({ clientId, clientSecret, signal }) { const appKey = nonEmptyString(clientId); const appSecret = nonEmptyString(clientSecret); if (!appKey || !appSecret) throw new TypeError("clientId and clientSecret are required"); const cached = tokenCache.get(appKey); if (cached && cached.expiresAt > now()) return cached.token; if (tokenRequests.has(appKey)) return tokenRequests.get(appKey); const request = (async () => { const value = await requestJson(fetchImpl, endpoint(apiBase, "v1.0/oauth2/accessToken"), { body: { appKey, appSecret }, signal, action: "\u9274\u6743" }); const token = nonEmptyString(value?.accessToken); if (!token) throw new DingtalkApiError("invalid-access-token", "\u9489\u9489\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u8BBF\u95EE\u4EE4\u724C\u3002"); const expiresInSeconds = positiveNumber2(value?.expireIn ?? value?.expiresIn, 7200); const refreshAfterMs = Math.max(1e3, (expiresInSeconds - 60) * 1e3); tokenCache.set(appKey, { token, expiresAt: now() + refreshAfterMs }); return token; })().finally(() => tokenRequests.delete(appKey)); tokenRequests.set(appKey, request); return request; } function acquireCardRequestSlot(signal) { const acquire = async () => { const waitMs = Math.max(0, nextCardRequestAt - now()); if (waitMs > 0) await delay3(waitMs, signal); nextCardRequestAt = Math.max(nextCardRequestAt, now()) + cardMinIntervalMs; }; const slot = cardSlotTail.then(acquire, acquire); cardSlotTail = slot.catch(() => void 0); return slot; } async function cardRequest(pathname, options) { await acquireCardRequestSlot(options.signal); try { return await requestJson(fetchImpl, endpoint(apiBase, pathname), options); } catch (error) { if (!(error instanceof DingtalkApiError) || error.status !== 403) throw error; await delay3(cardBackoffMs, options.signal); await acquireCardRequestSlot(options.signal); return requestJson(fetchImpl, endpoint(apiBase, pathname), options); } } async function failCard({ clientId, clientSecret, cardInstanceId, text, signal }) { const instanceId = nonEmptyString(cardInstanceId); const content = nonEmptyString(text); if (!instanceId) throw new TypeError("cardInstanceId is required"); if (!content) throw new TypeError("text is required"); const token = await accessToken({ clientId, clientSecret, signal }); const headers = { "x-acs-dingtalk-access-token": token }; const requests = [ cardRequest("v1.0/card/streaming", { method: "PUT", body: { outTrackId: instanceId, guid: randomUUID3(), key: "msgContent", content: normalizeDingtalkCardMarkdown(content), isFull: true, isFinalize: false, isError: true }, headers, signal, action: "AI Card \u5931\u8D25\u6536\u53E3" }), cardRequest("v1.0/card/instances", { method: "PUT", body: { outTrackId: instanceId, cardData: cardData(content, "5"), cardUpdateOptions: { updateCardDataByKey: true } }, headers, signal, action: "AI Card \u5931\u8D25\u72B6\u6001" }) ]; const results = await Promise.allSettled(requests); if (results.every(({ status }) => status === "rejected")) throw results[0].reason; return true; } return Object.freeze({ async beginRegistration({ signal } = {}) { const initialized = assertRegistrationOk(await requestJson( fetchImpl, endpoint(registrationBase, "app/registration/init"), { body: { source }, signal, action: "\u521D\u59CB\u5316" } ), "\u521D\u59CB\u5316"); const nonce = nonEmptyString(initialized.nonce); if (!nonce) throw new DingtalkApiError("invalid-registration", "\u9489\u9489\u626B\u7801\u521D\u59CB\u5316\u7F3A\u5C11 nonce\u3002"); const begun = assertRegistrationOk(await requestJson( fetchImpl, endpoint(registrationBase, "app/registration/begin"), { body: { nonce }, signal, action: "\u521B\u5EFA" } ), "\u521B\u5EFA"); const deviceCode = nonEmptyString(begun.device_code); const verificationUriComplete = nonEmptyString(begun.verification_uri_complete); if (!deviceCode || !verificationUriComplete) { throw new DingtalkApiError("invalid-registration", "\u9489\u9489\u626B\u7801\u670D\u52A1\u8FD4\u56DE\u7684\u4FE1\u606F\u4E0D\u5B8C\u6574\u3002"); } const verificationUrl = normalizeTrustedUrl(verificationUriComplete, { label: "\u9489\u9489\u626B\u7801", requireSubdomain: false }).toString(); return { deviceCode, userCode: nonEmptyString(begun.user_code) ?? void 0, verificationUri: nonEmptyString(begun.verification_uri) ?? void 0, verificationUriComplete: verificationUrl, expiresInSeconds: positiveNumber2(begun.expires_in, 7200), intervalSeconds: positiveNumber2(begun.interval, 5) }; }, async pollRegistration({ deviceCode, signal } = {}) { const code = nonEmptyString(deviceCode); if (!code) throw new TypeError("deviceCode is required"); const polled = assertRegistrationOk(await requestJson( fetchImpl, endpoint(registrationBase, "app/registration/poll"), { body: { device_code: code }, signal, action: "\u72B6\u6001\u67E5\u8BE2" } ), "\u72B6\u6001\u67E5\u8BE2"); const status = nonEmptyString(polled.status)?.toUpperCase(); if (!status || !REGISTRATION_STATUSES.has(status)) { throw new DingtalkApiError("invalid-registration-status", "\u9489\u9489\u626B\u7801\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u72B6\u6001\u3002"); } const result = { status, failReason: nonEmptyString(polled.fail_reason) ?? void 0 }; if (status === "SUCCESS") { result.clientId = nonEmptyString(polled.client_id) ?? void 0; result.clientSecret = nonEmptyString(polled.client_secret) ?? void 0; if (!result.clientId || !result.clientSecret) { throw new DingtalkApiError("missing-credentials", "\u9489\u9489\u626B\u7801\u5DF2\u786E\u8BA4\uFF0C\u4F46\u6CA1\u6709\u8FD4\u56DE\u673A\u5668\u4EBA\u51ED\u636E\u3002"); } } return result; }, accessToken, async createAiCard({ clientId, clientSecret, target, initialText, signal }) { const appKey = nonEmptyString(clientId); const appSecret = nonEmptyString(clientSecret); const content = nonEmptyString(initialText); if (!appKey || !appSecret) throw new TypeError("clientId and clientSecret are required"); if (!content) throw new TypeError("initialText is required"); const normalizedTarget = normalizeCardTarget(target); const token = await accessToken({ clientId: appKey, clientSecret: appSecret, signal }); const cardInstanceId = `dsh_${randomUUID3()}`; const headers = { "x-acs-dingtalk-access-token": token }; let delivered = false; try { await cardRequest("v1.0/card/instances", { body: { cardTemplateId: DINGTALK_AI_CARD_TEMPLATE_ID, outTrackId: cardInstanceId, cardData: { cardParamMap: { config: JSON.stringify({ autoLayout: true }) } }, callbackType: "STREAM", imGroupOpenSpaceModel: { supportForward: true }, imRobotOpenSpaceModel: { supportForward: true } }, headers, signal, action: "AI Card \u521B\u5EFA" }); await cardRequest("v1.0/card/instances/deliver", { body: cardDeliverBody(cardInstanceId, normalizedTarget, appKey), headers, signal, action: "AI Card \u6295\u653E" }); delivered = true; await cardRequest("v1.0/card/instances", { method: "PUT", body: { outTrackId: cardInstanceId, cardData: cardData(content, "2") }, headers, signal, action: "AI Card \u542F\u52A8" }); await cardRequest("v1.0/card/streaming", { method: "PUT", body: { outTrackId: cardInstanceId, guid: randomUUID3(), key: "msgContent", content: normalizeDingtalkCardMarkdown(content).replace(/\n+$/, ""), isFull: true, isFinalize: false, isError: false }, headers, signal, action: "AI Card \u542F\u52A8" }); } catch (error) { if (delivered) { const cleanupSignal = AbortSignal.timeout(5e3); await failCard({ clientId: appKey, clientSecret: appSecret, cardInstanceId, text: "\u6D88\u606F\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", signal: cleanupSignal }).catch(() => void 0); } throw error; } return { cardInstanceId }; }, async updateAiCard({ clientId, clientSecret, cardInstanceId, text, signal }) { const instanceId = nonEmptyString(cardInstanceId); const content = nonEmptyString(text); if (!instanceId) throw new TypeError("cardInstanceId is required"); if (!content) throw new TypeError("text is required"); const token = await accessToken({ clientId, clientSecret, signal }); await cardRequest("v1.0/card/streaming", { method: "PUT", body: { outTrackId: instanceId, guid: randomUUID3(), key: "msgContent", content: normalizeDingtalkCardMarkdown(content).replace(/\n+$/, ""), isFull: true, isFinalize: false, isError: false }, headers: { "x-acs-dingtalk-access-token": token }, signal, action: "AI Card \u66F4\u65B0" }); return true; }, async finishAiCard({ clientId, clientSecret, cardInstanceId, text, signal }) { const instanceId = nonEmptyString(cardInstanceId); const content = nonEmptyString(text); if (!instanceId) throw new TypeError("cardInstanceId is required"); if (!content) throw new TypeError("text is required"); const token = await accessToken({ clientId, clientSecret, signal }); const headers = { "x-acs-dingtalk-access-token": token }; const normalizedContent = normalizeDingtalkCardMarkdown(content); await cardRequest("v1.0/card/streaming", { method: "PUT", body: { outTrackId: instanceId, guid: randomUUID3(), key: "msgContent", content: normalizedContent, isFull: true, isFinalize: true, isError: false }, headers, signal, action: "AI Card \u5B8C\u6210" }); let completed = true; const completionRequest = { method: "PUT", body: { outTrackId: instanceId, cardData: cardData(content, "3"), cardUpdateOptions: { updateCardDataByKey: true } }, headers, signal, action: "AI Card \u6536\u53E3" }; try { await cardRequest("v1.0/card/instances", completionRequest); } catch { try { await cardRequest("v1.0/card/instances", completionRequest); } catch { completed = false; } } return { delivered: true, completed }; }, failAiCard: failCard, async sendText({ clientId, clientSecret, sessionWebhook, text, signal }) { const content = nonEmptyString(text); if (!content) throw new TypeError("text is required"); const webhook = normalizeDingtalkSessionWebhook(sessionWebhook); const token = await accessToken({ clientId, clientSecret, signal }); const response = await requestJson(fetchImpl, webhook, { body: { msgtype: "text", text: { content } }, headers: { "x-acs-dingtalk-access-token": token }, signal, action: "\u6D88\u606F\u56DE\u590D" }); if (response?.errcode !== void 0 && response.errcode !== 0 || response?.code !== void 0 && response.code !== 0) { throw new DingtalkApiError("send-rejected", "\u9489\u9489\u670D\u52A1\u62D2\u7EDD\u4E86\u56DE\u590D\u6D88\u606F\u3002"); } return true; }, clearAccessToken(clientId) { const appKey = nonEmptyString(clientId); if (appKey) tokenCache.delete(appKey); } }); } // src/channels/dingtalk/dingtalk-card-stream.mjs var DEFAULT_UPDATE_INTERVAL_MS = 500; var FAILURE_TEXT = "\u6D88\u606F\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"; function requiredText(value, name2) { if (typeof value !== "string") throw new TypeError(`${name2} must be a string`); return value; } function requiredCredential(value, name2) { if (typeof value !== "string" || !value.trim()) { throw new TypeError(`${name2} is required`); } return value.trim(); } function createDingTalkCardStream({ api, clientId, clientSecret, target, signal, logger = console, updateIntervalMs = DEFAULT_UPDATE_INTERVAL_MS, clock = () => Date.now(), timer = { setTimeout: (callback, delay3) => globalThis.setTimeout(callback, delay3), clearTimeout: (handle) => globalThis.clearTimeout(handle) } } = {}) { if (!api || typeof api.createAiCard !== "function" || typeof api.updateAiCard !== "function" || typeof api.finishAiCard !== "function") { throw new TypeError("DingTalk AI Card API is required"); } const normalizedClientId = requiredCredential(clientId, "clientId"); const normalizedClientSecret = requiredCredential(clientSecret, "clientSecret"); if (target === void 0 || target === null) throw new TypeError("target is required"); if (!Number.isFinite(updateIntervalMs) || updateIntervalMs < 0) { throw new TypeError("updateIntervalMs must be a non-negative number"); } if (typeof clock !== "function") throw new TypeError("clock must be a function"); if (typeof timer?.setTimeout !== "function" || typeof timer?.clearTimeout !== "function") { throw new TypeError("timer must provide setTimeout and clearTimeout"); } const readClock = () => { const value = clock(); if (!Number.isFinite(value)) throw new TypeError("clock must return a finite timestamp"); return value; }; readClock(); let phase = signal?.aborted ? "aborted" : "idle"; let cardRequest = null; let pendingText = null; let scheduledUpdate = null; let updateWorker = null; let finishPromise = null; let cleanupPromise = null; let lastUpdateAt = 0; const clearScheduledUpdate = () => { if (scheduledUpdate === null) return; timer.clearTimeout(scheduledUpdate); scheduledUpdate = null; }; const removeAbortListener = () => signal?.removeEventListener("abort", onAbort); const close = (nextPhase) => { phase = nextPhase; pendingText = null; clearScheduledUpdate(); removeAbortListener(); }; const cleanupCard = () => { if (!cardRequest || typeof api.failAiCard !== "function") return Promise.resolve(false); if (!cleanupPromise) { cleanupPromise = api.failAiCard({ ...cardRequest, text: FAILURE_TEXT, signal: AbortSignal.timeout(5e3) }).then( () => true, () => false ); } return cleanupPromise; }; const fail = (operation) => { if (phase === "failed" || phase === "finished" || phase === "aborted") return; void cleanupCard(); close("failed"); logger?.error?.(`[dsh-dingtalk] AI Card ${operation} failed`); }; function onAbort() { if (phase === "finished" || phase === "failed" || phase === "aborted") return; void cleanupCard(); close("aborted"); } if (phase !== "aborted") signal?.addEventListener("abort", onAbort, { once: true }); const launchUpdate = () => { if (phase !== "active" || updateWorker || pendingText === null) return; const delay3 = Math.max(0, lastUpdateAt + updateIntervalMs - readClock()); if (delay3 > 0) { scheduledUpdate = timer.setTimeout(() => { scheduledUpdate = null; launchUpdate(); }, delay3); return; } const text = pendingText; pendingText = null; updateWorker = (async () => { try { await api.updateAiCard({ ...cardRequest, text, finished: false }); lastUpdateAt = readClock(); } catch { if (signal?.aborted || phase === "aborted") return; fail("update"); } })().finally(() => { updateWorker = null; if (phase === "active" && pendingText !== null) launchUpdate(); }); }; const start = async (initialText) => { requiredText(initialText, "initialText"); if (phase !== "idle") return false; phase = "starting"; try { const created = await api.createAiCard({ clientId: normalizedClientId, clientSecret: normalizedClientSecret, target, initialText, signal }); const cardInstanceId = typeof created?.cardInstanceId === "string" ? created.cardInstanceId.trim() : ""; if (!cardInstanceId) throw new TypeError("DingTalk did not return a card instance id"); cardRequest = Object.freeze({ clientId: normalizedClientId, clientSecret: normalizedClientSecret, target, cardInstanceId, signal }); if (phase !== "starting") { void cleanupCard(); return false; } lastUpdateAt = readClock(); phase = "active"; return true; } catch { if (signal?.aborted || phase === "aborted") { close("aborted"); return false; } fail("creation"); return false; } }; const push = (progressText3) => { requiredText(progressText3, "progressText"); if (phase !== "active") return; pendingText = progressText3; if (!scheduledUpdate && !updateWorker) launchUpdate(); }; const finish = (finalText) => { requiredText(finalText, "finalText"); if (phase === "finished") return Promise.resolve(true); if (phase === "finishing") return finishPromise; if (phase !== "active") return Promise.resolve(false); phase = "finishing"; pendingText = null; clearScheduledUpdate(); const activeUpdate = updateWorker; finishPromise = (async () => { if (activeUpdate) await activeUpdate; if (phase !== "finishing") return false; try { await api.finishAiCard({ ...cardRequest, text: finalText }); if (phase !== "finishing") return false; close("finished"); return true; } catch { if (signal?.aborted || phase === "aborted") { close("aborted"); return false; } fail("finish"); return false; } })(); return finishPromise; }; return Object.freeze({ start, push, finish }); } // src/channels/dingtalk/dingtalk-bridge.mjs var CARD_INITIAL_TEXT = "\u5DF2\u8FDE\u63A5 DeepSeek Harness\uFF0C\u6B63\u5728\u601D\u8003\u2026"; var CARD_ERROR_TEXT = "\u6D88\u606F\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"; var HELP_TEXT = [ "\u9489\u9489\u673A\u5668\u4EBA\u5DF2\u8FDE\u63A5 DeepSeek Harness\u3002", "", "\u76F4\u63A5\u53D1\u9001\u6587\u5B57\u5373\u53EF\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\u3002", "/new \u5F00\u542F\u4E00\u4E2A\u5168\u65B0\u4F1A\u8BDD", "/status \u68C0\u67E5\u8FDE\u63A5\u72B6\u6001", "/help \u663E\u793A\u672C\u5E2E\u52A9" ].join("\n"); function nonEmptyString2(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function senderStaffId(message) { return nonEmptyString2(message?.senderStaffId) ?? nonEmptyString2(message?.senderId); } function conversationKey(message, sender) { if (String(message?.conversationType) === "2") { const conversationId = nonEmptyString2(message?.conversationId); if (!conversationId) throw new Error("DingTalk group message has no conversation id"); return `group:${conversationId}`; } return `p2p:${sender}`; } function cardTarget(message, sender) { if (String(message?.conversationType) === "2") { return { type: "group", openConversationId: nonEmptyString2(message?.conversationId) }; } return { type: "user", userId: sender }; } function progressText(update) { if (update?.type === "text" && nonEmptyString2(update.text)) return update.text; if (update?.type === "tool") { if (update.name === "web_search") return "_\u6B63\u5728\u641C\u7D22\u7F51\u7EDC\u5E76\u6574\u7406\u4FE1\u606F\u2026_"; return `_\u6B63\u5728\u4F7F\u7528 ${nonEmptyString2(update.name) ?? "\u5DE5\u5177"}\u2026_`; } return `_${nonEmptyString2(update?.text) ?? "\u6B63\u5728\u5904\u7406\u2026"}_`; } function ensureStats(status) { status.stats ??= {}; for (const key of ["messagesReceived", "messagesReplied", "messagesRejected", "messagesIgnored"]) { status[key] ??= 0; status.stats[key] = status[key]; } status.pendingSenders ??= []; } function increment(status, key) { status[key] = (status[key] ?? 0) + 1; status.stats ??= {}; status.stats[key] = status[key]; } function createDingtalkBridgeStatus({ pendingSenders = [] } = {}) { return { messagesReceived: 0, messagesReplied: 0, messagesRejected: 0, messagesIgnored: 0, lastMessageAt: null, lastReplyAt: null, lastRejectedAt: null, lastError: null, pendingSenders: structuredClone(pendingSenders), stats: { messagesReceived: 0, messagesReplied: 0, messagesRejected: 0, messagesIgnored: 0 } }; } var DingtalkHarnessBridge = class { #api; #clientId; #clientSecret; #harness; #state; #status; #logger; #replyTimeoutMs; #maxMessageChars; #signal; #queues = /* @__PURE__ */ new Map(); #acceptedMessageIds = /* @__PURE__ */ new Set(); constructor({ api, clientId, clientSecret, harness, state, status = createDingtalkBridgeStatus(), logger = console, replyTimeoutMs = 6e5, maxMessageChars = 4e3, signal }) { if (!api || typeof api.sendText !== "function") throw new TypeError("DingTalk API is required"); if (!nonEmptyString2(clientId) || !nonEmptyString2(clientSecret)) { throw new TypeError("DingTalk app credentials are required"); } if (!harness || !state) throw new TypeError("Harness client and state store are required"); this.#api = api; this.#clientId = clientId.trim(); this.#clientSecret = clientSecret.trim(); this.#harness = harness; this.#state = state; this.#status = status; this.#logger = logger; this.#replyTimeoutMs = replyTimeoutMs; this.#maxMessageChars = maxMessageChars; this.#signal = signal; ensureStats(this.#status); this.#refreshPendingSenders(); } get status() { this.#refreshPendingSenders(); return structuredClone(this.#status); } accept(message) { if (this.#signal?.aborted) return Promise.resolve(); const messageId = nonEmptyString2(message?.msgId); const sender = senderStaffId(message); if (!messageId || !sender || this.#state.hasSeen(messageId) || this.#acceptedMessageIds.has(messageId)) return Promise.resolve(); this.#acceptedMessageIds.add(messageId); let key; try { key = conversationKey(message, sender); } catch { this.#acceptedMessageIds.delete(messageId); increment(this.#status, "messagesRejected"); this.#status.lastRejectedAt = (/* @__PURE__ */ new Date()).toISOString(); return Promise.resolve(); } const previous = this.#queues.get(key) ?? Promise.resolve(); const current = previous.catch(() => void 0).then(() => this.#process(message, messageId, sender, key)).finally(() => { this.#acceptedMessageIds.delete(messageId); if (this.#queues.get(key) === current) this.#queues.delete(key); }); this.#queues.set(key, current); return current; } async waitForIdle() { await Promise.allSettled([...this.#queues.values()]); } async #process(message, messageId, sender, key) { this.#signal?.throwIfAborted(); if (this.#state.hasSeen(messageId)) return; await this.#state.markSeen(messageId); increment(this.#status, "messagesReceived"); this.#status.lastMessageAt = (/* @__PURE__ */ new Date()).toISOString(); if (String(message.conversationType) === "2" && message.isInAtList !== true) { increment(this.#status, "messagesIgnored"); return; } let sessionWebhook; try { sessionWebhook = normalizeDingtalkSessionWebhook(message.sessionWebhook); } catch { increment(this.#status, "messagesRejected"); this.#status.lastRejectedAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.lastError = "\u9489\u9489\u6D88\u606F\u6CA1\u6709\u5B89\u5168\u7684\u56DE\u590D\u5730\u5740\u3002"; return; } const text = message?.msgtype === "text" ? nonEmptyString2(message?.text?.content) : null; let cardStream = null; let cardStarted = false; try { if (!text) { await this.#send(sessionWebhook, "\u76EE\u524D\u4EC5\u652F\u6301\u6587\u5B57\u6D88\u606F\u3002"); return; } const command = text.toLowerCase(); if (command === "/help") { await this.#send(sessionWebhook, HELP_TEXT); return; } if (command === "/status") { await this.#harness.ensureRunning({ signal: this.#signal }); await this.#send(sessionWebhook, "\u9489\u9489\u673A\u5668\u4EBA\u4E0E DeepSeek Harness \u8FDE\u63A5\u6B63\u5E38\u3002"); return; } if (command === "/new") { await this.#state.clearSession(key); await this.#send(sessionWebhook, "\u5DF2\u5F00\u542F\u65B0\u4F1A\u8BDD\u3002\u8BF7\u53D1\u9001\u4F60\u7684\u95EE\u9898\u3002"); return; } let sessionId = this.#state.sessionFor(key); if (!sessionId || !await this.#harness.sessionExists(sessionId, { signal: this.#signal })) { sessionId = await this.#harness.createSession({ signal: this.#signal }); await this.#state.setSession(key, sessionId); } if (typeof this.#api.createAiCard === "function" && typeof this.#api.updateAiCard === "function" && typeof this.#api.finishAiCard === "function") { cardStream = createDingTalkCardStream({ api: this.#api, clientId: this.#clientId, clientSecret: this.#clientSecret, target: cardTarget(message, sender), signal: this.#signal, logger: this.#logger }); cardStarted = await cardStream.start(CARD_INITIAL_TEXT); } const answer = await this.#harness.ask(sessionId, text, { timeoutMs: this.#replyTimeoutMs, signal: this.#signal, onUpdate: cardStarted ? (update) => cardStream.push(progressText(update)) : void 0 }); const streamed = cardStarted && await cardStream.finish(answer); if (!streamed) await this.#send(sessionWebhook, answer); increment(this.#status, "messagesReplied"); this.#status.lastReplyAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.lastError = null; } catch { if (this.#signal?.aborted) return; this.#status.lastError = "\u9489\u9489\u6D88\u606F\u5904\u7406\u5931\u8D25\u3002"; this.#logger.error?.("[dsh-dingtalk] failed to process an inbound message"); try { const streamed = cardStarted && await cardStream.finish(CARD_ERROR_TEXT); if (!streamed) await this.#send(sessionWebhook, CARD_ERROR_TEXT); } catch { this.#logger.error?.("[dsh-dingtalk] failed to send the safe error reply"); } } } #refreshPendingSenders() { if (typeof this.#state.pendingSenders === "function") { this.#status.pendingSenders = this.#state.pendingSenders(); } } async #send(sessionWebhook, text) { for (const chunk of splitDingtalkText(text, this.#maxMessageChars)) { this.#signal?.throwIfAborted(); await this.#api.sendText({ clientId: this.#clientId, clientSecret: this.#clientSecret, sessionWebhook, text: chunk, signal: this.#signal }); } } }; // src/channels/dingtalk/dingtalk-runtime.mjs function nonEmptyString3(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function approvedSenderIds(config) { const entries = Array.isArray(config?.approvedSenders) ? config.approvedSenders : config?.approvedSenders instanceof Set ? [...config.approvedSenders] : []; return new Set(entries.map((entry) => nonEmptyString3( typeof entry === "string" ? entry : entry?.staffId )).filter(Boolean)); } function approvedSenderCount(config) { return approvedSenderIds(config).size; } function streamIsOpen(client) { return client?.connected === true || client?.socket?.readyState === 1; } function abortable(promise, signal) { return new Promise((resolve7, reject) => { if (signal.aborted) { reject(signal.reason); return; } const onAbort = () => reject(signal.reason); signal.addEventListener("abort", onAbort, { once: true }); Promise.resolve(promise).then( (value) => { signal.removeEventListener("abort", onAbort); resolve7(value); }, (error) => { signal.removeEventListener("abort", onAbort); reject(error); } ); }); } async function waitForStreamOpen(client, pollIntervalMs, signal) { while (true) { signal?.throwIfAborted(); if (streamIsOpen(client)) return; await new Promise((resolve7, reject) => { const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve7(); }, pollIntervalMs); const onAbort = () => { clearTimeout(timer); reject(signal.reason ?? new DOMException("Aborted", "AbortError")); }; signal?.addEventListener("abort", onAbort, { once: true }); }); } } async function connectStream(client, timeoutMs, pollIntervalMs, signal) { const timeoutSignal = AbortSignal.timeout(timeoutMs); const connectSignal = AbortSignal.any([signal, timeoutSignal]); let connectSettled = false; const connectTask = Promise.resolve().then(() => client.connect()).finally(() => { connectSettled = true; }); try { await abortable(connectTask, connectSignal); await waitForStreamOpen(client, pollIntervalMs, connectSignal); } catch (error) { if (connectSignal.aborted) { if (!connectSettled) { void connectTask.then(() => client.disconnect()).catch(() => void 0); } if (signal.aborted) throw signal.reason; throw new Error(`DingTalk Stream handshake timed out after ${timeoutMs}ms`); } throw error; } } async function defaultStreamFactory({ clientId, clientSecret }) { const { DWClient, TOPIC_ROBOT } = await import("dingtalk-stream"); return { client: new DWClient({ clientId, clientSecret, endpoint: "https://api.dingtalk.com", autoReconnect: false, keepAlive: true, debug: false }), topic: TOPIC_ROBOT }; } function createDingtalkRuntimeStatus({ pendingSenders = [], approvedSenders = 0 } = {}) { return { startedAt: null, ready: false, dingtalkStreamState: "idle", harnessReachable: false, lastConnectedAt: null, lastCheckedAt: null, lastCallbackAt: null, authorizationMode: "sender-staff-id-approval", approvedSenderCount: approvedSenders, ...createDingtalkBridgeStatus({ pendingSenders }) }; } var DingtalkRuntime = class { #config; #clientSecret; #harness; #state; #logger; #replyTimeoutMs; #maxMessageChars; #connectTimeoutMs; #connectPollIntervalMs; #api; #streamFactory; #status; #client = null; #bridge = null; #topic = null; #starting = null; #connectionMonitor = null; #abortController = null; #callbackTasks = /* @__PURE__ */ new Set(); constructor({ config, clientSecret, harness, state, logger = console, replyTimeoutMs = 6e5, maxMessageChars = 4e3, connectTimeoutMs = 15e3, connectPollIntervalMs = 25, api = createDingtalkApi(), streamFactory = defaultStreamFactory }) { if (!config || !nonEmptyString3(config.clientId) || !nonEmptyString3(clientSecret)) { throw new TypeError("DingtalkRuntime requires app credentials"); } if (!harness || !state) throw new TypeError("DingtalkRuntime requires Harness and state"); if (typeof streamFactory !== "function") throw new TypeError("streamFactory must be a function"); this.#config = config; this.#clientSecret = clientSecret.trim(); this.#harness = harness; this.#state = state; this.#logger = logger; this.#replyTimeoutMs = replyTimeoutMs; this.#maxMessageChars = maxMessageChars; this.#connectTimeoutMs = connectTimeoutMs; this.#connectPollIntervalMs = connectPollIntervalMs; this.#api = api; this.#streamFactory = streamFactory; this.#status = createDingtalkRuntimeStatus({ pendingSenders: this.#pendingSenders(), approvedSenders: approvedSenderCount(config) }); } get status() { if (this.#bridge) { const bridgeStatus = this.#bridge.status; Object.assign(this.#status, bridgeStatus); } else { this.#status.pendingSenders = this.#pendingSenders(); } return structuredClone(this.#status); } pendingSender(requestId) { return typeof this.#state.pendingSender === "function" ? this.#state.pendingSender(requestId) : null; } pendingSenders() { return this.#pendingSenders(); } async start() { if (this.#client && this.#status.ready) return this.status; if (this.#starting) return this.#starting; this.#starting = this.#start().finally(() => { this.#starting = null; }); return this.#starting; } async #start() { await this.stop(); const abortController = new AbortController(); this.#abortController = abortController; const { signal } = abortController; this.#status.startedAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.dingtalkStreamState = "connecting"; this.#status.lastError = null; try { await this.#harness.ensureRunning({ signal }); this.#status.harnessReachable = true; if (typeof this.#state.removePendingSenderByStaffId === "function") { for (const staffId of approvedSenderIds(this.#config)) { await this.#state.removePendingSenderByStaffId(staffId); } this.#status.pendingSenders = this.#pendingSenders(); } this.#bridge = new DingtalkHarnessBridge({ api: this.#api, clientId: this.#config.clientId, clientSecret: this.#clientSecret, approvedSenders: this.#config.approvedSenders, harness: this.#harness, state: this.#state, status: this.#status, logger: this.#logger, replyTimeoutMs: this.#replyTimeoutMs, maxMessageChars: this.#maxMessageChars, signal }); const created = await this.#streamFactory({ clientId: this.#config.clientId, clientSecret: this.#clientSecret }); signal.throwIfAborted(); this.#client = created?.client ?? created; this.#topic = created?.topic ?? created?.TOPIC_ROBOT ?? "/v1.0/im/bot/messages/get"; if (!this.#client || typeof this.#client.registerCallbackListener !== "function" || typeof this.#client.connect !== "function" || typeof this.#client.disconnect !== "function" || typeof this.#client.socketCallBackResponse !== "function") { throw new TypeError("streamFactory returned an invalid DingTalk Stream client"); } const client = this.#client; const bridge = this.#bridge; client.registerCallbackListener(this.#topic, (response) => { if (this.#client !== client || this.#bridge !== bridge) return; const callbackMessageId = nonEmptyString3(response?.headers?.messageId); if (callbackMessageId) { try { client.socketCallBackResponse(callbackMessageId, { success: true }); } catch { this.#logger.warn?.("[dsh-dingtalk] unable to acknowledge an inbound callback"); } } const task = Promise.resolve().then(async () => { if (this.#bridge !== bridge) return; let message; try { message = typeof response?.data === "string" ? JSON.parse(response.data) : response?.data; } catch { this.#status.lastError = "\u9489\u9489\u6D88\u606F\u683C\u5F0F\u65E0\u6548\u3002"; this.#logger.warn?.("[dsh-dingtalk] ignored an invalid callback payload"); return; } if (!message || typeof message !== "object") return; this.#status.lastCallbackAt = Date.now(); await bridge.accept(message); }).catch(() => { if (signal.aborted || this.#bridge !== bridge) return; this.#status.lastError = "\u9489\u9489\u6D88\u606F\u5904\u7406\u5931\u8D25\u3002"; this.#logger.error?.("[dsh-dingtalk] callback processing failed"); }).finally(() => this.#callbackTasks.delete(task)); this.#callbackTasks.add(task); }); await connectStream( client, this.#connectTimeoutMs, this.#connectPollIntervalMs, signal ); this.#status.ready = true; this.#status.dingtalkStreamState = "connected"; this.#status.lastConnectedAt = Date.now(); this.#status.lastCheckedAt = Date.now(); this.#status.lastError = null; this.#connectionMonitor = setInterval(() => { const connected = streamIsOpen(client); this.#status.ready = connected; this.#status.dingtalkStreamState = connected ? "connected" : "reconnecting"; this.#status.lastCheckedAt = Date.now(); if (connected) this.#status.lastError = null; }, 1e3); this.#connectionMonitor.unref?.(); return this.status; } catch (error) { const aborted = signal.aborted; this.#status.ready = false; this.#status.dingtalkStreamState = aborted ? "idle" : "failed"; this.#status.lastError = aborted ? null : error?.message ?? String(error); await this.stop({ preserveError: !aborted }); throw error; } } async stop({ preserveError = false } = {}) { const lastError = preserveError ? this.#status.lastError : null; const abortController = this.#abortController; this.#abortController = null; abortController?.abort(new DOMException("DingTalk runtime stopped", "AbortError")); if (this.#connectionMonitor) clearInterval(this.#connectionMonitor); this.#connectionMonitor = null; this.#status.ready = false; const client = this.#client; this.#client = null; this.#topic = null; if (client) { try { await client.disconnect(); } catch { this.#logger.warn?.("[dsh-dingtalk] DingTalk Stream disconnect failed"); } } await Promise.allSettled([...this.#callbackTasks]); this.#callbackTasks.clear(); if (this.#bridge) await this.#bridge.waitForIdle(); this.#bridge = null; this.#status.dingtalkStreamState = preserveError ? "failed" : "idle"; this.#status.lastError = lastError; return this.status; } #pendingSenders() { return typeof this.#state.pendingSenders === "function" ? this.#state.pendingSenders() : []; } }; // src/channels/dingtalk/harness-client.mjs import { spawn } from "node:child_process"; import { randomUUID as randomUUID4 } from "node:crypto"; function sleep(ms, signal) { return new Promise((resolve7, reject) => { if (signal?.aborted) { reject(signal.reason ?? new DOMException("Aborted", "AbortError")); return; } const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve7(); }, ms); const onAbort = () => { clearTimeout(timer); reject(signal.reason ?? new DOMException("Aborted", "AbortError")); }; signal?.addEventListener("abort", onAbort, { once: true }); }); } function assistantMessageText(event) { return (event?.data?.message?.content ?? []).filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n").trim(); } var HarnessReplyTracker = class { #promptRpcId; #lastSeq; #openTurn = null; #targetTurn = null; #stepText = /* @__PURE__ */ new Map(); #latestText = ""; #finished = false; #reason = null; constructor({ promptRpcId, afterSeq = -1 }) { this.#promptRpcId = promptRpcId; this.#lastSeq = afterSeq; } get finished() { return this.#finished; } get answer() { return this.#latestText.trim(); } get reason() { return this.#reason; } consume(entries) { let update = null; const ordered = [...entries].map((entry) => entry?.event ?? entry).filter(Boolean).sort((left, right) => (left.seq ?? -1) - (right.seq ?? -1)); for (const event of ordered) { const seq = event.seq ?? -1; if (seq <= this.#lastSeq) continue; this.#lastSeq = seq; if (event.type === "turn/start") this.#openTurn = event.data?.turn ?? null; if (event.type === "user/message" && event.data?.source?.rpcId === this.#promptRpcId) { this.#targetTurn = this.#openTurn; continue; } if (this.#targetTurn === null) continue; if (event.type === "turn/end") { if (event.data?.turn !== this.#targetTurn) continue; this.#finished = true; this.#reason = event.data?.reason ?? null; this.#openTurn = null; continue; } if (event.data?.turn !== this.#targetTurn) continue; if (event.type === "assistant/chunk" && event.data?.chunk?.type === "text-delta") { const step = event.data?.step ?? 0; const index = event.data.chunk.index ?? 0; const key = `${step}:${index}`; this.#stepText.set(key, (this.#stepText.get(key) ?? "") + event.data.chunk.text); const prefix = `${step}:`; const text = [...this.#stepText.entries()].filter(([partKey]) => partKey.startsWith(prefix)).sort(([left], [right]) => Number(left.split(":")[1]) - Number(right.split(":")[1])).map(([, part]) => part).join("\n").trim(); if (text && text !== this.#latestText) { this.#latestText = text; update = { type: "text", text }; } continue; } if (event.type === "assistant/message") { const text = assistantMessageText(event); if (text && text !== this.#latestText) { this.#latestText = text; update = { type: "text", text }; } continue; } if (event.type === "tool/call") { update = { type: "tool", name: event.data?.name ?? "\u5DE5\u5177" }; } else if (event.type === "tool/result") { update = { type: "status", text: "\u6B63\u5728\u6574\u7406\u7ED3\u679C\u2026" }; } } return update; } }; var HarnessRpcError = class extends Error { constructor(method, error) { super(`${method}: ${error?.message ?? "unknown Harness RPC error"}`); this.name = "HarnessRpcError"; this.method = method; this.code = error?.code ?? "internal"; this.details = error?.details ?? {}; } }; var HarnessClient = class { #baseUrl; #workspace; #agentPreset; #autostart; #dshBin; #fetch; #managedProcess = null; constructor({ baseUrl, workspace, agentPreset = "standard", autostart = false, dshBin = "dsh", fetchImpl = fetch }) { this.#baseUrl = new URL(baseUrl); this.#workspace = workspace; this.#agentPreset = agentPreset; this.#autostart = autostart; this.#dshBin = dshBin; this.#fetch = fetchImpl; } async rpc(method, payload = {}, timeoutMs = 3e4, options = {}) { const rpcId = options.rpcId ?? `dingtalk-${randomUUID4()}`; const timeoutSignal = AbortSignal.timeout(timeoutMs); const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal; const response = await this.#fetch(new URL(`/api/${method}`, this.#baseUrl), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ type: "client-request", rpcId, method, payload }), signal }); if (!response.ok) throw new Error(`Harness transport ${method} failed: HTTP ${response.status}`); const body = await response.json(); if (body?.type !== "server-response" || body?.rpcId !== rpcId) { throw new Error(`Harness returned an invalid response for ${method}`); } if (!body.result?.ok) throw new HarnessRpcError(method, body.result?.error); return body.result.value; } async health(options = {}) { await this.rpc("host.describe", {}, 5e3, options); return true; } async ensureRunning(options = {}) { try { return await this.health(options); } catch (firstError) { if (!this.#autostart) throw firstError; } if (!this.#managedProcess || this.#managedProcess.exitCode !== null) { const port = this.#baseUrl.port || (this.#baseUrl.protocol === "https:" ? "443" : "80"); this.#managedProcess = spawn(this.#dshBin, [ "web", "--host", this.#baseUrl.hostname, "--port", port ], { cwd: this.#workspace, env: process.env, stdio: ["ignore", "inherit", "inherit"] }); this.#managedProcess.on("error", (error) => { console.error("[dsh-dingtalk] failed to start Harness:", error.message); }); } const deadline = Date.now() + 6e4; let lastError; while (Date.now() < deadline) { await sleep(1e3, options.signal); try { return await this.health(options); } catch (error) { lastError = error; } } throw new Error(`Harness did not become ready: ${lastError?.message ?? "timeout"}`); } async workspaceId(options = {}) { const { items } = await this.rpc("workspace.list", {}, 3e4, options); const existing = items.find((item) => item.path === this.#workspace); if (existing) return existing.workspaceId; const created = await this.rpc("workspace.create", { path: this.#workspace }, 3e4, options); return created.workspace.workspaceId; } async createSession(options = {}) { await this.ensureRunning(options); const workspaceId = await this.workspaceId(options); const created = await this.rpc("session.create", { workspaceId, agentPreset: this.#agentPreset }, 3e4, options); return created.sessionId; } async sessionExists(sessionId, options = {}) { try { await this.rpc("session.history", { sessionId, maxMessages: 1 }, 3e4, options); return true; } catch (error) { if (error instanceof HarnessRpcError && error.code === "session-not-found") return false; throw error; } } async ask(sessionId, text, options = {}) { if (typeof options === "number") options = { timeoutMs: options }; const timeoutMs = options.timeoutMs ?? 6e5; const signal = options.signal; const onUpdate = typeof options.onUpdate === "function" ? options.onUpdate : null; await this.ensureRunning({ signal }); const before = await this.rpc( "session.history", { sessionId, maxMessages: 1 }, 3e4, { signal } ); const baselineSeq = Math.max(-1, ...(before.events ?? []).map(({ event }) => event.seq ?? -1)); const promptRpcId = `dingtalk-${randomUUID4()}`; const tracker = new HarnessReplyTracker({ promptRpcId, afterSeq: baselineSeq }); await this.rpc("session.prompt", { sessionId, mode: "queue", content: [{ type: "text", text }], clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }, 3e4, { rpcId: promptRpcId, signal }); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { await sleep(300, signal); const history = await this.rpc( "session.history", { sessionId, maxMessages: 50 }, 3e4, { signal } ); const update = tracker.consume(history.events ?? []); if (update && onUpdate) { try { await onUpdate(update); } catch (error) { console.warn("[dsh-dingtalk] ignored a progress update failure:", error.message); } } if (!tracker.finished) continue; if (tracker.answer) return tracker.answer; throw new Error( `Harness turn ended without a text reply${tracker.reason ? ` (${JSON.stringify(tracker.reason)})` : ""}` ); } throw new Error(`Harness reply timed out after ${Math.round(timeoutMs / 1e3)} seconds`); } stopManagedProcess() { if (this.#managedProcess?.exitCode === null) this.#managedProcess.kill("SIGTERM"); } }; // src/channels/dingtalk/state-store.mjs import { randomUUID as randomUUID5 } from "node:crypto"; import { mkdir as mkdir2, readFile as readFile2, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises"; import { dirname as dirname2 } from "node:path"; var EMPTY_STATE = Object.freeze({ version: 1, sessions: {}, seenMessageIds: [], pendingSenders: {} }); function nonEmptyString4(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function displayName(value) { return (nonEmptyString4(value) ?? "\u9489\u9489\u7528\u6237").slice(0, 100); } function normalizePendingSender2(value, fallbackRequestId) { if (!value || typeof value !== "object") return null; const requestId = nonEmptyString4(value.requestId) ?? nonEmptyString4(fallbackRequestId); const staffId = nonEmptyString4(value.staffId); const requestedAt = nonEmptyString4(value.requestedAt) ?? nonEmptyString4(value.lastSeenAt); const lastSeenAt = nonEmptyString4(value.lastSeenAt) ?? requestedAt; if (!requestId || !staffId || !requestedAt || !lastSeenAt) return null; return { requestId, staffId, displayName: displayName(value.displayName ?? value.nick), requestedAt, lastSeenAt }; } function normalizeState(value) { if (!value || typeof value !== "object") return structuredClone(EMPTY_STATE); const sessions = {}; if (value.sessions && typeof value.sessions === "object" && !Array.isArray(value.sessions)) { for (const [key, sessionId] of Object.entries(value.sessions)) { const normalizedKey = nonEmptyString4(key); const normalizedSession = nonEmptyString4(sessionId); if (normalizedKey && normalizedSession) sessions[normalizedKey] = normalizedSession; } } const pendingSenders = {}; const entries = Array.isArray(value.pendingSenders) ? value.pendingSenders.map((entry) => [entry?.requestId, entry]) : Object.entries(value.pendingSenders && typeof value.pendingSenders === "object" ? value.pendingSenders : {}); for (const [key, candidate] of entries) { const pending = normalizePendingSender2(candidate, key); if (!pending) continue; const duplicate = Object.values(pendingSenders).find((entry) => entry.staffId === pending.staffId); if (!duplicate || duplicate.lastSeenAt < pending.lastSeenAt) { if (duplicate) delete pendingSenders[duplicate.requestId]; pendingSenders[pending.requestId] = pending; } } return { version: 1, sessions, seenMessageIds: Array.isArray(value.seenMessageIds) ? [...new Set(value.seenMessageIds.map(nonEmptyString4).filter(Boolean))].slice(-1e3) : [], pendingSenders }; } var DingtalkStateStore = class { #path; #state = structuredClone(EMPTY_STATE); #writeQueue = Promise.resolve(); #idFactory; #now; constructor(path, { idFactory = randomUUID5, now = () => (/* @__PURE__ */ new Date()).toISOString() } = {}) { if (!nonEmptyString4(path)) throw new TypeError("state path is required"); if (typeof idFactory !== "function" || typeof now !== "function") { throw new TypeError("idFactory and now must be functions"); } this.#path = path; this.#idFactory = idFactory; this.#now = now; } async load() { try { this.#state = normalizeState(JSON.parse(await readFile2(this.#path, "utf8"))); } catch (error) { if (error?.code !== "ENOENT") throw error; this.#state = structuredClone(EMPTY_STATE); await this.#persist(); } return this; } sessionFor(key) { return this.#state.sessions[key] ?? null; } async setSession(key, sessionId) { const normalizedKey = nonEmptyString4(key); const normalizedSession = nonEmptyString4(sessionId); if (!normalizedKey || !normalizedSession) throw new TypeError("key and sessionId are required"); this.#state.sessions[normalizedKey] = normalizedSession; await this.#persist(); } async clearSession(key) { const normalizedKey = nonEmptyString4(key); if (!normalizedKey || !(normalizedKey in this.#state.sessions)) return; delete this.#state.sessions[normalizedKey]; await this.#persist(); } hasSeen(messageId) { const id = nonEmptyString4(messageId); return Boolean(id && this.#state.seenMessageIds.includes(id)); } async markSeen(messageId) { const id = nonEmptyString4(messageId); if (!id) throw new TypeError("messageId is required"); if (this.hasSeen(id)) return; this.#state.seenMessageIds.push(id); if (this.#state.seenMessageIds.length > 1e3) { this.#state.seenMessageIds.splice(0, this.#state.seenMessageIds.length - 1e3); } await this.#persist(); } pendingSenders() { return Object.values(this.#state.pendingSenders).sort((left, right) => left.requestedAt.localeCompare(right.requestedAt)).map((entry) => structuredClone(entry)); } pendingSender(requestId) { const id = nonEmptyString4(requestId); const entry = id ? this.#state.pendingSenders[id] : null; return entry ? structuredClone(entry) : null; } async recordPendingSender(staffIdOrEntry, name2, seenAt) { const input = staffIdOrEntry && typeof staffIdOrEntry === "object" ? staffIdOrEntry : { staffId: staffIdOrEntry, displayName: name2, lastSeenAt: seenAt }; const staffId = nonEmptyString4(input.staffId); if (!staffId) throw new TypeError("staffId is required"); const timestamp = nonEmptyString4(input.lastSeenAt) ?? nonEmptyString4(input.requestedAt) ?? this.#now(); const existing = Object.values(this.#state.pendingSenders).find((entry2) => entry2.staffId === staffId); const entry = { requestId: existing?.requestId ?? `ding_sender_${this.#idFactory()}`, staffId, displayName: displayName(input.displayName ?? input.nick ?? name2), requestedAt: existing?.requestedAt ?? timestamp, lastSeenAt: timestamp }; this.#state.pendingSenders[entry.requestId] = entry; await this.#persist(); return structuredClone(entry); } async removePendingSender(requestId) { const id = nonEmptyString4(requestId); if (!id || !this.#state.pendingSenders[id]) return false; delete this.#state.pendingSenders[id]; await this.#persist(); return true; } async removePendingSenderByStaffId(staffId) { const id = nonEmptyString4(staffId); const pending = id ? Object.values(this.#state.pendingSenders).find((entry) => entry.staffId === id) : null; return pending ? this.removePendingSender(pending.requestId) : false; } snapshot() { return structuredClone(this.#state); } async remove() { await this.#writeQueue; try { await unlink2(this.#path); } catch (error) { if (error?.code !== "ENOENT") throw error; } this.#state = structuredClone(EMPTY_STATE); } async #persist() { const snapshot = `${JSON.stringify(this.#state, null, 2)} `; const operation = this.#writeQueue.then(async () => { await mkdir2(dirname2(this.#path), { recursive: true, mode: 448 }); const temporary = `${this.#path}.tmp`; await writeFile2(temporary, snapshot, { encoding: "utf8", mode: 384 }); await rename2(temporary, this.#path); }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; } }; // plugin-src/host/channels/dingtalk/connection-supervisor.mjs var DEFAULT_RETRY_DELAYS_MS = Object.freeze([250, 1e3, 3e3, 5e3, 1e4, 3e4]); function retryDelays(value) { if (!Array.isArray(value) || value.length === 0) return [...DEFAULT_RETRY_DELAYS_MS]; const valid = value.filter((delay3) => Number.isFinite(delay3) && delay3 >= 0); return valid.length > 0 ? valid : [...DEFAULT_RETRY_DELAYS_MS]; } var ConnectionSupervisor = class { #controller; #harness; #logger; #retryDelays; #healthyIntervalMs; #setTimeout; #clearTimeout; #timer = null; #running = null; #retryIndex = 0; #closed = false; #started = false; #ready; #resolveReady; constructor({ controller, harness, logger = console, retryDelaysMs, healthyIntervalMs = 15e3, setTimeoutImpl = setTimeout, clearTimeoutImpl = clearTimeout }) { if (!controller || typeof controller.initialize !== "function" || typeof controller.status !== "function") { throw new TypeError("ConnectionSupervisor requires a controller"); } if (!harness || typeof harness.ensureRunning !== "function") { throw new TypeError("ConnectionSupervisor requires a Harness client"); } this.#controller = controller; this.#harness = harness; this.#logger = logger; this.#retryDelays = retryDelays(retryDelaysMs); this.#healthyIntervalMs = Number.isFinite(healthyIntervalMs) && healthyIntervalMs >= 0 ? healthyIntervalMs : 15e3; this.#setTimeout = setTimeoutImpl; this.#clearTimeout = clearTimeoutImpl; this.#ready = new Promise((resolve7) => { this.#resolveReady = resolve7; }); } get ready() { return this.#ready; } start() { if (this.#started || this.#closed) return this; this.#started = true; this.#schedule(0); return this; } async close() { if (this.#closed) return; this.#closed = true; if (this.#timer !== null) this.#clearTimeout(this.#timer); this.#timer = null; await this.#running?.catch(() => void 0); this.#resolveReady?.(null); this.#resolveReady = null; } #schedule(delayMs) { if (this.#closed) return; this.#timer = this.#setTimeout(() => { this.#timer = null; void this.#run(); }, delayMs); this.#timer?.unref?.(); } async #run() { if (this.#closed || this.#running) return; const operation = this.#reconcile(); this.#running = operation; try { await operation; } finally { if (this.#running === operation) this.#running = null; } } async #reconcile() { try { await this.#harness.ensureRunning(); if (this.#closed) return; const status = await this.#controller.initialize(); if (this.#closed) return; this.#resolveReady?.(status); this.#resolveReady = null; const { configured, connected } = status.totals; if (connected < configured) { const delayMs = this.#retryDelays[Math.min(this.#retryIndex, this.#retryDelays.length - 1)]; this.#retryIndex += 1; this.#logger.warn?.( `[dsh-dingtalk] ${connected}/${configured} bots connected; retrying in ${delayMs}ms` ); this.#schedule(delayMs); return; } this.#retryIndex = 0; this.#schedule(this.#healthyIntervalMs); } catch (error) { if (this.#closed) return; const delayMs = this.#retryDelays[Math.min(this.#retryIndex, this.#retryDelays.length - 1)]; this.#retryIndex += 1; this.#logger.warn?.( `[dsh-dingtalk] connection reconciliation failed; retrying in ${delayMs}ms`, error ); this.#schedule(delayMs); } } }; function createConnectionSupervisor(options) { return new ConnectionSupervisor(options); } // plugin-src/host/channels/dingtalk/production.mjs function harnessOrigin(webServer, configured) { if (configured !== void 0) return new URL(configured); const port = webServer?.port; if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error("dsh-dingtalk requires an initialized DSH webServer port"); } return new URL(`http://127.0.0.1:${port}`); } function pluginPaths(config) { const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), ".dsh")); const root = resolve(config.dataDir ?? join(dshHome, "integrations", "dsh-dingtalk")); return { root, config: resolve(config.configPath ?? join(root, "config.json")), bots: resolve(config.botsDir ?? join(root, "bots")) }; } async function createProductionController(ctx, config = {}, internals = {}) { if (!ctx?.credentials) throw new TypeError("dsh-dingtalk requires ctx.credentials"); if (!ctx?.webServer) throw new TypeError("dsh-dingtalk requires ctx.webServer"); const ConfigStore = internals.ConfigStore ?? DingtalkConfigStore; const DeviceAuth = internals.DeviceAuth ?? DingtalkDeviceAuth; const StateStore2 = internals.StateStore ?? DingtalkStateStore; const Harness = internals.HarnessClient ?? HarnessClient; const Controller = internals.Controller ?? DingtalkController; const Runtime = internals.Runtime ?? DingtalkRuntime; const createSupervisor = internals.createConnectionSupervisor ?? createConnectionSupervisor; const logger = typeof ctx.logger === "function" ? ctx.logger("dsh-dingtalk") : ctx.logger ?? console; const paths = pluginPaths(config); const configStore = await new ConfigStore(paths.config).load(); const deviceAuth = internals.deviceAuth ?? new DeviceAuth({ baseUrl: config.registrationBaseUrl }); const stateStores = /* @__PURE__ */ new Map(); const statePath = (botId) => resolve(paths.bots, botId, "state.json"); const stateFor = async (botId) => { let state = stateStores.get(botId); if (!state) { state = await new StateStore2(statePath(botId)).load(); stateStores.set(botId, state); } return state; }; const harness = new Harness({ baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl), workspace: resolve(config.workspace ?? process.cwd()), agentPreset: config.agentPreset ?? "standard", autostart: false, dshBin: config.dshBin ?? "dsh" }); const controller = new Controller({ deviceAuth, credentials: ctx.credentials, configStore, logger, createRuntime: async ({ botId, config: botConfig, clientSecret }) => { const state = await stateFor(botId); return new Runtime({ config: botConfig, clientSecret, harness, state, replyTimeoutMs: config.replyTimeoutMs ?? 6e5, maxMessageChars: config.maxMessageChars ?? 4e3, connectTimeoutMs: config.connectTimeoutMs ?? 15e3, logger: { error: (...args) => logger.error?.(`[${botId}]`, ...args), warn: (...args) => logger.warn?.(`[${botId}]`, ...args), info: (...args) => logger.info?.(`[${botId}]`, ...args), debug: (...args) => logger.debug?.(`[${botId}]`, ...args) } }); }, deleteState: async ({ botId }) => { const state = stateStores.get(botId); stateStores.delete(botId); if (state && typeof state.remove === "function") { await state.remove(); return; } try { await unlink3(statePath(botId)); } catch (error) { if (error?.code !== "ENOENT") throw error; } } }); const supervisor = createSupervisor({ controller, harness, logger, retryDelaysMs: config.retryDelaysMs, healthyIntervalMs: config.healthyIntervalMs }).start(); return { controller, ready: supervisor.ready, async close() { await supervisor.close(); await controller.close(); harness.stopManagedProcess(); } }; } // plugin-src/host/channels/dingtalk/rpc.mjs import QRCode from "qrcode"; // plugin-src/host/rpc-authority.mjs var RPC_AUTHORITIES = /* @__PURE__ */ new Set(["loopback", "trusted-host"]); function resolveRpcAuthority(value) { if (value === void 0) return "loopback"; if (RPC_AUTHORITIES.has(value)) return value; throw new TypeError('dsh-im rpcAuthority must be "loopback" or "trusted-host"'); } // plugin-src/host/channels/dingtalk/rpc.mjs var DINGTALK_RPC_CHANNEL = "/dingtalk"; var DINGTALK_ENDPOINTS = Object.freeze({ status: "connection.status", beginProvisioning: "provision.begin", pollProvisioning: "provision.poll", cancelProvisioning: "provision.cancel", bindCredentials: "bot.bind-credentials", reconnectBot: "bot.reconnect", deleteBot: "bot.delete", approveSender: "bot.sender.approve", revokeSender: "bot.sender.revoke" }); var DINGTALK_RPC_ENDPOINTS = Object.freeze(Object.values(DINGTALK_ENDPOINTS)); var FORBIDDEN_PUBLIC_KEYS = /* @__PURE__ */ new Set([ "clientSecret", "client_secret", "deviceCode", "device_code", "secretRef", "staffId", "senderStaffId", "verificationUrl", "verificationUri", "userCode" ]); function isRecord(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function exactKeys(value, allowed) { return isRecord(value) && Object.keys(value).every((key) => allowed.includes(key)); } function validId(value) { return typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value); } function validCredential(value, maxLength) { return typeof value === "string" && value.trim().length > 0 && value.length <= maxLength; } function payloadFailure(endpoint, payload) { if (!isRecord(payload)) return "Payload must be an object."; if (endpoint === DINGTALK_ENDPOINTS.status) { return exactKeys(payload, []) ? null : "connection.status does not accept fields."; } if (endpoint === DINGTALK_ENDPOINTS.beginProvisioning) { return exactKeys(payload, ["locale"]) && (payload.locale === void 0 || payload.locale === "zh-CN") ? null : "provision.begin received unsupported fields."; } if ([DINGTALK_ENDPOINTS.pollProvisioning, DINGTALK_ENDPOINTS.cancelProvisioning].includes(endpoint)) { return exactKeys(payload, ["attemptId"]) && validId(payload.attemptId) ? null : `${endpoint} requires an attemptId.`; } if (endpoint === DINGTALK_ENDPOINTS.bindCredentials) { return exactKeys(payload, ["clientId", "clientSecret"]) && validCredential(payload.clientId, 256) && validCredential(payload.clientSecret, 1024) ? null : "bot.bind-credentials requires Client ID and Client Secret."; } if (endpoint === DINGTALK_ENDPOINTS.reconnectBot) { return exactKeys(payload, ["botId"]) && validId(payload.botId) ? null : "bot.reconnect requires a botId."; } if (endpoint === DINGTALK_ENDPOINTS.deleteBot) { return exactKeys(payload, ["botId", "confirm"]) && validId(payload.botId) && payload.confirm === true ? null : "bot.delete requires a botId and confirm=true."; } if (endpoint === DINGTALK_ENDPOINTS.approveSender) { return exactKeys(payload, ["botId", "requestId", "confirm"]) && validId(payload.botId) && validId(payload.requestId) && payload.confirm === true ? null : "bot.sender.approve requires botId, requestId, and confirm=true."; } if (endpoint === DINGTALK_ENDPOINTS.revokeSender) { return exactKeys(payload, ["botId", "senderKey", "confirm"]) && validId(payload.botId) && validId(payload.senderKey) && payload.confirm === true ? null : "bot.sender.revoke requires botId, senderKey, and confirm=true."; } return "Unknown DingTalk endpoint."; } function badRequest(message) { return { ok: false, error: { code: "bad-request", message } }; } function cancelled() { return { ok: false, error: { code: "cancelled", message: "The request was cancelled." } }; } function internalFailure() { return { ok: false, error: { code: "dingtalk-operation-failed", message: "\u9489\u9489\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" } }; } function sanitizePublic(value) { if (Array.isArray(value)) return value.map(sanitizePublic); if (!isRecord(value)) return value; const safe = {}; for (const [key, child] of Object.entries(value)) { if (!FORBIDDEN_PUBLIC_KEYS.has(key)) safe[key] = sanitizePublic(child); } return safe; } async function qrDataUrl(value) { return QRCode.toDataURL(value, { type: "image/png", errorCorrectionLevel: "M", margin: 2, width: 320 }); } async function withEncodedQr(value, encodeQr) { if (!value || typeof value.verificationUrl !== "string") return sanitizePublic(value); return sanitizePublic({ ...value, qrCodeDataUrl: await encodeQr(value.verificationUrl) }); } async function publicStatus(status, encodeQr) { const value = structuredClone(status); if (value?.provisioning) { value.provisioning = await withEncodedQr(value.provisioning, encodeQr); } return sanitizePublic(value); } function assertController(controller) { for (const method of [ "status", "startProvisioning", "registrationStatus", "cancelProvisioning", "bindCredentials", "reconnectBot", "deleteBot", "approveSender", "revokeSender" ]) { if (typeof controller?.[method] !== "function") { throw new TypeError(`A complete DingTalk controller is required (${method})`); } } } function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } = {}) { assertController(controller); const qrCache = /* @__PURE__ */ new Map(); const cachedEncode = (url) => { let encoded = qrCache.get(url); if (!encoded) { if (qrCache.size >= 16) qrCache.delete(qrCache.keys().next().value); encoded = Promise.resolve().then(() => encodeQr(url)); qrCache.set(url, encoded); } return encoded; }; return async (endpoint, payload, signal) => { if (signal?.aborted) return cancelled(); if (!DINGTALK_RPC_ENDPOINTS.includes(endpoint)) return badRequest("Unknown DingTalk endpoint."); const invalid = payloadFailure(endpoint, payload); if (invalid) return badRequest(invalid); try { let value; if (endpoint === DINGTALK_ENDPOINTS.status) { value = await publicStatus(await controller.status(), cachedEncode); } else if (endpoint === DINGTALK_ENDPOINTS.beginProvisioning) { const started = await controller.startProvisioning({ signal }); if (signal?.aborted) { await controller.cancelProvisioning(started.attemptId); return cancelled(); } value = await withEncodedQr(started, cachedEncode); } else if (endpoint === DINGTALK_ENDPOINTS.pollProvisioning) { const current = await controller.registrationStatus(payload.attemptId); if (!current) return badRequest("The provisioning attempt no longer exists."); value = await withEncodedQr(current, cachedEncode); } else if (endpoint === DINGTALK_ENDPOINTS.cancelProvisioning) { value = await controller.cancelProvisioning(payload.attemptId); if (!value) return badRequest("The provisioning attempt no longer exists."); value = sanitizePublic(value); } else if (endpoint === DINGTALK_ENDPOINTS.bindCredentials) { value = await publicStatus(await controller.bindCredentials(payload), cachedEncode); } else if (endpoint === DINGTALK_ENDPOINTS.reconnectBot) { value = await publicStatus(await controller.reconnectBot(payload.botId), cachedEncode); } else if (endpoint === DINGTALK_ENDPOINTS.deleteBot) { value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode); } else if (endpoint === DINGTALK_ENDPOINTS.approveSender) { value = await publicStatus( await controller.approveSender(payload.botId, payload.requestId), cachedEncode ); } else { value = await publicStatus( await controller.revokeSender(payload.botId, payload.senderKey), cachedEncode ); } return signal?.aborted ? cancelled() : { ok: true, value }; } catch { return signal?.aborted ? cancelled() : internalFailure(); } }; } function installDingtalkRpc(ctx, controller, options, authority) { if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== "function") { throw new TypeError("DSH Host Connection RPC is required"); } return ctx.connection.rpc.handle( DINGTALK_RPC_CHANNEL, createDingtalkRpcHandler(controller, options), { authority: resolveRpcAuthority(authority) } ); } // plugin-src/host/channels/dingtalk/index.mjs async function apply(ctx, config = {}) { if (config?.controller) { return installDingtalkRpc(ctx, config.controller, config.rpcOptions, config.rpcAuthority); } const production = await createProductionController(ctx, config, config.internals); const disposeRpc = installDingtalkRpc( ctx, production.controller, config.rpcOptions, config.rpcAuthority ); ctx.effect(() => async () => { await production.close(); }, "dsh-dingtalk: close bot connections"); return disposeRpc; } // src/channels/shared/token-config-store.mjs import { createHash as createHash2 } from "node:crypto"; import { mkdir as mkdir3, readFile as readFile3, rename as rename3, unlink as unlink4, writeFile as writeFile3 } from "node:fs/promises"; import { dirname as dirname3 } from "node:path"; var EMPTY_DOCUMENT2 = Object.freeze({ version: 1, bots: Object.freeze([]) }); function cleanString4(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function escapePattern(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function deriveTokenBotIdentity(platformId, { botPrefix, tokenRefPrefix }) { const raw = cleanString4(platformId); if (!raw) throw new TypeError("platformId is required"); const digest2 = createHash2("sha256").update(raw).digest("hex").slice(0, 24); return { botId: `${botPrefix}_${digest2}`, tokenRef: `${tokenRefPrefix}_${digest2.toUpperCase()}` }; } function maskPlatformId(platformId, fallback) { const value = cleanString4(platformId) ?? ""; if (value.length <= 10) return value ? `${value.slice(0, 3)}\u2022\u2022\u2022` : fallback; return `${value.slice(0, 6)}\u2022\u2022\u2022\u2022${value.slice(-4)}`; } var TokenBotConfigStore = class { #path; #channel; #botPrefix; #tokenRefPrefix; #botIdPattern; #tokenRefPattern; #value = EMPTY_DOCUMENT2; #writeQueue = Promise.resolve(); constructor(path, { channel, botPrefix, tokenRefPrefix }) { this.#path = path; this.#channel = channel; this.#botPrefix = botPrefix; this.#tokenRefPrefix = tokenRefPrefix; this.#botIdPattern = new RegExp(`^${escapePattern(botPrefix)}_[a-f0-9]{24}$`); this.#tokenRefPattern = new RegExp(`^${escapePattern(tokenRefPrefix)}_[A-F0-9]{24}$`); } async load() { try { const normalized = this.#normalizeDocument(JSON.parse(await readFile3(this.#path, "utf8"))); if (!normalized) throw new Error(`dsh-im ${this.#channel} config contains invalid bot data`); this.#value = normalized; } catch (error) { if (error?.code !== "ENOENT") throw error; this.#value = EMPTY_DOCUMENT2; } return this; } list() { return structuredClone(this.#value.bots); } get(botId) { const bot = this.#value.bots.find((candidate) => candidate.botId === botId); return bot ? structuredClone(bot) : null; } getByPlatformId(platformId) { const bot = this.#value.bots.find((candidate) => candidate.platformId === platformId); return bot ? structuredClone(bot) : null; } async save(value) { const normalized = this.#normalizeBot(value); if (!normalized) throw new Error(`Refusing to persist incomplete ${this.#channel} bot data`); return this.#mutate((bots) => { const identityCollision = bots.find( (bot) => bot.platformId === normalized.platformId && bot.botId !== normalized.botId ); const refCollision = bots.find( (bot) => bot.tokenRef === normalized.tokenRef && bot.botId !== normalized.botId ); if (identityCollision || refCollision) throw new Error(`Duplicate ${this.#channel} bot identity`); const index = bots.findIndex((bot) => bot.botId === normalized.botId); if (index === -1) bots.push(normalized); else bots[index] = normalized; return structuredClone(normalized); }); } async remove(botId) { if (!this.#botIdPattern.test(botId)) throw new TypeError(`Invalid ${this.#channel} bot id`); return this.#mutate((bots) => { const index = bots.findIndex((bot) => bot.botId === botId); if (index === -1) return null; const [removed] = bots.splice(index, 1); return structuredClone(removed); }); } async clear() { const operation = this.#writeQueue.then(async () => { try { await unlink4(this.#path); } catch (error) { if (error?.code !== "ENOENT") throw error; } this.#value = EMPTY_DOCUMENT2; }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; } #normalizeBot(value) { if (!value || typeof value !== "object") return null; const platformId = cleanString4(value.platformId); const botId = cleanString4(value.botId); const tokenRef = cleanString4(value.tokenRef); const name2 = cleanString4(value.name); if (!platformId || !botId || !tokenRef || !name2 || !this.#botIdPattern.test(botId) || !this.#tokenRefPattern.test(tokenRef)) return null; const derived = deriveTokenBotIdentity(platformId, { botPrefix: this.#botPrefix, tokenRefPrefix: this.#tokenRefPrefix }); if (derived.botId !== botId || derived.tokenRef !== tokenRef) return null; return Object.freeze({ botId, platformId, tokenRef, name: name2, username: cleanString4(value.username), createdAt: cleanString4(value.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString(), connectedAt: cleanString4(value.connectedAt) }); } #normalizeDocument(value) { if (!value || value.version !== 1 || !Array.isArray(value.bots)) return null; const bots = value.bots.map((bot) => this.#normalizeBot(bot)); if (bots.some((bot) => bot === null)) return null; const ids = /* @__PURE__ */ new Set(); const platformIds = /* @__PURE__ */ new Set(); const refs = /* @__PURE__ */ new Set(); for (const bot of bots) { if (ids.has(bot.botId) || platformIds.has(bot.platformId) || refs.has(bot.tokenRef)) return null; ids.add(bot.botId); platformIds.add(bot.platformId); refs.add(bot.tokenRef); } return Object.freeze({ version: 1, bots: Object.freeze(bots) }); } async #mutate(mutator) { let result; const operation = this.#writeQueue.then(async () => { const bots = [...this.#value.bots]; result = mutator(bots); const document = Object.freeze({ version: 1, bots: Object.freeze(bots) }); await this.#write(document); this.#value = document; }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; return result; } async #write(document) { await mkdir3(dirname3(this.#path), { recursive: true, mode: 448 }); const temporary = `${this.#path}.tmp`; await writeFile3(temporary, `${JSON.stringify(document, null, 2)} `, { encoding: "utf8", mode: 384 }); await rename3(temporary, this.#path); } }; // src/channels/discord/config-store.mjs var IDENTITY_OPTIONS = Object.freeze({ botPrefix: "discord", tokenRefPrefix: "DSH_DISCORD_BOT_TOKEN" }); function deriveDiscordBotIdentity(platformId) { return deriveTokenBotIdentity(platformId, IDENTITY_OPTIONS); } function maskDiscordBotId(platformId) { return maskPlatformId(platformId, "Discord\u673A\u5668\u4EBA"); } var DiscordConfigStore = class extends TokenBotConfigStore { constructor(path) { super(path, { channel: "Discord", ...IDENTITY_OPTIONS }); } }; // src/channels/shared/token-bot-controller.mjs function cleanString5(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function safeError2(code, message) { return Object.freeze({ code, message }); } var TokenBotController = class { #descriptor; #credentials; #configStore; #inspectToken; #deriveIdentity; #maskPlatformId; #createRuntime; #deleteState; #logger; #runtimes = /* @__PURE__ */ new Map(); #errors = /* @__PURE__ */ new Map(); #transitions = /* @__PURE__ */ new Map(); #revision = 0; #closed = false; constructor({ descriptor, credentials, configStore, inspectToken, deriveIdentity, maskPlatformId: maskPlatformId2, createRuntime, deleteState = async () => { }, logger = console }) { if (!descriptor?.key || !descriptor?.label || !descriptor?.connectionLabel) { throw new TypeError("TokenBotController requires a channel descriptor"); } if (!credentials || typeof credentials.resolve !== "function" || typeof credentials.set !== "function" || typeof credentials.unset !== "function") { throw new TypeError(`${descriptor.label} requires the DSH credential provider`); } if (!configStore || typeof configStore.list !== "function" || typeof configStore.save !== "function" || typeof configStore.remove !== "function") { throw new TypeError(`${descriptor.label} requires a config store`); } if (typeof inspectToken !== "function" || typeof deriveIdentity !== "function" || typeof maskPlatformId2 !== "function" || typeof createRuntime !== "function") { throw new TypeError(`${descriptor.label} controller dependencies are incomplete`); } this.#descriptor = descriptor; this.#credentials = credentials; this.#configStore = configStore; this.#inspectToken = inspectToken; this.#deriveIdentity = deriveIdentity; this.#maskPlatformId = maskPlatformId2; this.#createRuntime = createRuntime; this.#deleteState = deleteState; this.#logger = logger; } async initialize() { if (this.#closed) return this.status(); for (const config of this.#configStore.list()) { await this.#withBotTransition(config.botId, async () => { if (this.#closed || this.#runtimes.get(config.botId)?.status?.ready) return; const token = await this.#resolveToken(config.tokenRef); if (!token) { this.#errors.set(config.botId, safeError2( "missing-token", `${this.#descriptor.label}\u673A\u5668\u4EBA\u51ED\u636E\u7F3A\u5931\uFF0C\u8BF7\u79FB\u9664\u540E\u91CD\u65B0\u63A5\u5165\u3002` )); return; } try { await this.#startRuntime(config, token); this.#errors.delete(config.botId); } catch (error) { this.#errors.set(config.botId, safeError2( "connection-failed", `${this.#descriptor.label}\u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u91CD\u8BD5\u3002` )); this.#logger.warn?.( `[dsh-im:${this.#descriptor.key}] bot ${config.botId} failed to initialize:`, error ); } finally { this.#touch(); } }); } return this.status(); } async bindCredentials({ token } = {}) { if (this.#closed) throw new Error(`${this.#descriptor.label} controller is closed`); const normalizedToken = cleanString5(token); if (!normalizedToken) throw new TypeError(`${this.#descriptor.label} Bot Token is required`); const inspected = await this.#inspectToken(normalizedToken); const platformId = cleanString5(inspected?.platformId); const name2 = cleanString5(inspected?.name); if (!platformId || !name2) throw new Error(`${this.#descriptor.label} returned an invalid bot identity`); const identity = this.#deriveIdentity(platformId); await this.#withBotTransition(identity.botId, async () => { if (this.#closed) throw new Error(`${this.#descriptor.label} controller is closed`); const previousConfig = this.#configStore.getByPlatformId(platformId); const previousToken = await this.#credentials.resolve(identity.tokenRef).catch(() => void 0); const config = { botId: identity.botId, platformId, tokenRef: identity.tokenRef, name: name2, username: cleanString5(inspected.username), createdAt: previousConfig?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(), connectedAt: (/* @__PURE__ */ new Date()).toISOString() }; await this.#credentials.set(identity.tokenRef, normalizedToken); try { await this.#configStore.save(config); } catch (error) { await this.#restoreCredential(identity.tokenRef, previousToken); throw error; } try { await this.#startRuntime(config, normalizedToken); this.#errors.delete(identity.botId); } catch (error) { this.#errors.set(identity.botId, safeError2( "connection-failed", `${this.#descriptor.label}\u673A\u5668\u4EBA\u5DF2\u63A5\u5165\uFF0C\u6D88\u606F\u8FDE\u63A5\u6682\u672A\u5C31\u7EEA\u3002` )); this.#logger.warn?.( `[dsh-im:${this.#descriptor.key}] bot ${identity.botId} credential connection failed:`, error ); } this.#touch(); }); return this.status(); } async reconnectBot(botId) { const config = this.#configStore.get(botId); if (!config) throw new Error(`Unknown ${this.#descriptor.label} bot`); await this.#withBotTransition(botId, async () => { const token = await this.#resolveToken(config.tokenRef); if (!token) throw new Error(`${this.#descriptor.label} bot token is missing`); try { await this.#startRuntime(config, token); this.#errors.delete(botId); } catch (error) { this.#errors.set(botId, safeError2( "connection-failed", `${this.#descriptor.label}\u8FDE\u63A5\u4ECD\u672A\u5C31\u7EEA\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002` )); throw error; } finally { this.#touch(); } }); return this.status(); } async deleteBot(botId) { const config = this.#configStore.get(botId); if (!config) throw new Error(`Unknown ${this.#descriptor.label} bot`); await this.#withBotTransition(botId, async () => { const previous = await this.#credentials.resolve(config.tokenRef).catch(() => void 0); await this.#stopRuntime(botId); try { await this.#credentials.unset(config.tokenRef); await this.#configStore.remove(botId); } catch (error) { if (previous?.value) { await this.#credentials.set(config.tokenRef, previous.value).catch(() => void 0); await this.#startRuntime(config, previous.value).catch(() => void 0); } throw new Error(`Unable to remove the ${this.#descriptor.label} bot safely.`, { cause: error }); } await this.#deleteState({ botId, config }).catch((error) => { this.#logger.warn?.( `[dsh-im:${this.#descriptor.key}] bot ${botId} state cleanup failed:`, error ); }); this.#errors.delete(botId); this.#touch(); }); return this.status(); } status() { const bots = this.#configStore.list().map((config) => { const runtimeStatus2 = this.#runtimes.get(config.botId)?.status ?? null; const connected = runtimeStatus2?.ready === true && runtimeStatus2.connectionState === "connected" && runtimeStatus2.harnessReachable === true; const state = connected ? "connected" : runtimeStatus2?.connectionState === "connecting" ? "connecting" : this.#errors.has(config.botId) || runtimeStatus2?.connectionState === "failed" ? "error" : "offline"; return { botId: config.botId, state, connected, configured: true, bot: { name: config.name, username: config.username, idMasked: this.#maskPlatformId(config.platformId) }, health: { status: connected ? "healthy" : state === "error" ? "error" : "offline", summary: connected ? `${this.#descriptor.label}${this.#descriptor.connectionLabel}\u8FD0\u884C\u6B63\u5E38` : state === "error" ? `${this.#descriptor.label}\u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u91CD\u8BD5` : `${this.#descriptor.label}\u8FDE\u63A5\u5F53\u524D\u79BB\u7EBF`, lastCheckedAt: runtimeStatus2?.lastCheckedAt ?? null, lastConnectedAt: runtimeStatus2?.lastConnectedAt ?? null }, stats: { messagesReceived: runtimeStatus2?.messagesReceived ?? 0, messagesReplied: runtimeStatus2?.messagesReplied ?? 0 }, error: structuredClone(this.#errors.get(config.botId) ?? null) }; }); const connectedCount = bots.filter((bot) => bot.connected).length; return { schemaVersion: 1, revision: this.#revision, state: bots.length === 0 ? "disconnected" : connectedCount === bots.length ? "connected" : connectedCount > 0 ? "degraded" : "offline", bots, totals: { configured: bots.length, connected: connectedCount } }; } async close() { if (this.#closed) return; this.#closed = true; await Promise.allSettled([...this.#transitions.values()]); await Promise.allSettled([...this.#runtimes.keys()].map((botId) => this.#stopRuntime(botId))); } async #startRuntime(config, token) { if (this.#closed) throw new Error(`${this.#descriptor.label} controller is closed`); await this.#stopRuntime(config.botId); if (this.#closed) throw new Error(`${this.#descriptor.label} controller is closed`); const runtime = await this.#createRuntime({ botId: config.botId, config, token }); if (!runtime || typeof runtime.start !== "function" || typeof runtime.stop !== "function") { throw new TypeError(`createRuntime returned an invalid ${this.#descriptor.label} runtime`); } this.#runtimes.set(config.botId, runtime); try { await runtime.start(); } catch (error) { await runtime.stop().catch(() => void 0); this.#runtimes.delete(config.botId); throw error; } } async #stopRuntime(botId) { const runtime = this.#runtimes.get(botId); this.#runtimes.delete(botId); await runtime?.stop().catch((error) => { this.#logger.warn?.( `[dsh-im:${this.#descriptor.key}] bot ${botId} failed to stop cleanly:`, error ); }); } async #resolveToken(ref) { const result = await this.#credentials.resolve(ref).catch(() => void 0); return cleanString5(result?.value); } async #restoreCredential(ref, previous) { if (previous?.value) await this.#credentials.set(ref, previous.value).catch(() => void 0); else await this.#credentials.unset(ref).catch(() => void 0); } #withBotTransition(botId, operation) { const previous = this.#transitions.get(botId) ?? Promise.resolve(); const current = previous.catch(() => void 0).then(operation); const settled = current.finally(() => { if (this.#transitions.get(botId) === settled) this.#transitions.delete(botId); }); this.#transitions.set(botId, settled); return settled; } #touch() { this.#revision += 1; } }; // src/channels/discord/discord-api.mjs var DEFAULT_BASE_URL = "https://discord.com/api/v10/"; function cleanString6(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function requestSignal(signal, timeoutMs) { const timeout = AbortSignal.timeout(timeoutMs); return signal ? AbortSignal.any([signal, timeout]) : timeout; } function delay(ms, signal) { return new Promise((resolve7, reject) => { if (signal?.aborted) { reject(signal.reason ?? new DOMException("Aborted", "AbortError")); return; } const timer = setTimeout(resolve7, ms); timer?.unref?.(); signal?.addEventListener("abort", () => { clearTimeout(timer); reject(signal.reason ?? new DOMException("Aborted", "AbortError")); }, { once: true }); }); } function snowflake(value, name2) { const id = cleanString6(value); if (!id || !/^\d{5,30}$/.test(id)) throw new TypeError(`Invalid Discord ${name2}`); return id; } function validDiscordToken(value) { return typeof value === "string" && /^[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{20,}$/.test(value.trim()); } var DiscordApi = class { #token; #fetch; #baseUrl; constructor({ token, fetchImpl = fetch, baseUrl = DEFAULT_BASE_URL }) { if (!validDiscordToken(token)) throw new TypeError("Discord Bot Token is invalid"); if (typeof fetchImpl !== "function") throw new TypeError("DiscordApi requires fetch"); this.#token = token.trim(); this.#fetch = fetchImpl; this.#baseUrl = new URL(baseUrl); } getCurrentUser(options = {}) { return this.#request("users/@me", { ...options, method: "GET" }); } getGatewayBot(options = {}) { return this.#request("gateway/bot", { ...options, method: "GET" }); } createMessage({ channelId, content, replyToMessageId, signal }) { return this.#request(`channels/${snowflake(channelId, "channel id")}/messages`, { method: "POST", signal, body: { content, allowed_mentions: { parse: [], replied_user: false }, ...replyToMessageId ? { message_reference: { message_id: snowflake(replyToMessageId, "message id"), channel_id: snowflake(channelId, "channel id"), fail_if_not_exists: false } } : {} } }); } editMessage({ channelId, messageId, content, signal }) { return this.#request( `channels/${snowflake(channelId, "channel id")}/messages/${snowflake(messageId, "message id")}`, { method: "PATCH", signal, body: { content, allowed_mentions: { parse: [], replied_user: false } } } ); } sendTyping({ channelId, signal }) { return this.#request(`channels/${snowflake(channelId, "channel id")}/typing`, { method: "POST", signal, expectBody: false }); } async #request(path, { method, body, signal, timeoutMs = 15e3, expectBody = true, retry = true }) { let response; try { response = await this.#fetch(new URL(path, this.#baseUrl), { method, headers: { authorization: `Bot ${this.#token}`, "content-type": "application/json", "user-agent": "DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.1.0)" }, ...body === void 0 ? {} : { body: JSON.stringify(body) }, signal: requestSignal(signal, timeoutMs), redirect: "error" }); } catch (error) { if (error?.name === "AbortError" || error?.name === "TimeoutError") throw error; throw new Error(`Discord ${method} transport failed`); } let parsed = null; if (expectBody || response.status === 429 || !response.ok) { try { parsed = await response.json(); } catch { if (expectBody) throw new Error(`Discord ${method} returned invalid JSON`); } } if (response.status === 429 && retry) { const retryAfterMs = Math.min(1e4, Math.max(50, Number(parsed?.retry_after) * 1e3 || 1e3)); await delay(retryAfterMs, signal); return this.#request(path, { method, body, signal, timeoutMs, expectBody, retry: false }); } if (!response.ok) { const error = new Error(cleanString6(parsed?.message) ?? `Discord API failed with HTTP ${response.status}`); error.code = `discord-${response.status}`; throw error; } return expectBody ? parsed : null; } }; async function inspectDiscordToken(token, options = {}) { const api = new DiscordApi({ token, ...options }); const bot = await api.getCurrentUser(); if (!bot?.id || bot?.bot !== true) throw new Error("Discord token does not belong to a bot"); return { platformId: String(bot.id), name: cleanString6(bot.global_name) ?? cleanString6(bot.username) ?? "Discord\u673A\u5668\u4EBA", username: cleanString6(bot.username) }; } // src/channels/shared/text-harness-bridge.mjs function cleanText(value) { return typeof value === "string" ? value.trim() : ""; } function createTextBridgeStatus() { return { messagesReceived: 0, messagesReplied: 0, messagesRejected: 0, lastMessageAt: null, lastReplyAt: null, lastRejectedAt: null, lastError: null }; } var TextHarnessBridge = class { #descriptor; #bot; #harness; #state; #status; #logger; #replyTimeoutMs; #queues = /* @__PURE__ */ new Map(); constructor({ descriptor, bot, harness, state, status = createTextBridgeStatus(), logger = console, replyTimeoutMs = 6e5 }) { if (!descriptor?.key || !descriptor?.label) throw new TypeError("A channel descriptor is required"); if (!bot || typeof bot.sendText !== "function") throw new TypeError("A bot client is required"); if (!harness || !state) throw new TypeError("Harness client and state store are required"); this.#descriptor = descriptor; this.#bot = bot; this.#harness = harness; this.#state = state; this.#status = status; this.#logger = logger; this.#replyTimeoutMs = replyTimeoutMs; } get status() { return structuredClone(this.#status); } accept(message) { const conversationId = cleanText(message?.conversationId); const kind = message?.kind === "group" ? "group" : "direct"; const key = `${kind}:${conversationId}`; const previous = this.#queues.get(key) ?? Promise.resolve(); const current = previous.catch(() => void 0).then(() => this.#process({ ...message, kind, conversationId })).finally(() => { if (this.#queues.get(key) === current) this.#queues.delete(key); }); this.#queues.set(key, current); return current; } async waitForIdle() { await Promise.allSettled([...this.#queues.values()]); } async #process(message) { const messageId = cleanText(message.messageId); const senderId = cleanText(message.senderId); if (!messageId || !senderId || !message.conversationId || message.senderIsBot === true) return; if (this.#state.hasSeen(messageId)) return; this.#status.messagesReceived += 1; this.#status.lastMessageAt = (/* @__PURE__ */ new Date()).toISOString(); if (message.kind === "group" && message.addressed !== true) { this.#status.messagesRejected += 1; this.#status.lastRejectedAt = (/* @__PURE__ */ new Date()).toISOString(); return; } const target = message.replyTarget; const text = cleanText(message.content); try { if (!text) { await this.#bot.sendText(target, "\u76EE\u524D\u4EC5\u652F\u6301\u6587\u5B57\u6D88\u606F\u3002"); await this.#state.markSeen(messageId); return; } const command = text.toLowerCase(); if (command === "/help") { await this.#bot.sendText(target, [ `${this.#descriptor.label}\u673A\u5668\u4EBA\u5DF2\u8FDE\u63A5 DeepSeek Harness\u3002`, "", "\u76F4\u63A5\u53D1\u9001\u6587\u5B57\u5373\u53EF\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\u3002", "/new \u5F00\u542F\u4E00\u4E2A\u5168\u65B0\u4F1A\u8BDD", "/status \u68C0\u67E5\u8FDE\u63A5\u72B6\u6001", "/help \u663E\u793A\u672C\u5E2E\u52A9" ].join("\n")); await this.#state.markSeen(messageId); return; } if (command === "/status") { await this.#harness.ensureRunning(); await this.#bot.sendText(target, `${this.#descriptor.label}\u673A\u5668\u4EBA\u4E0E DeepSeek Harness \u8FDE\u63A5\u6B63\u5E38\u3002`); await this.#state.markSeen(messageId); return; } const conversationKey6 = `${message.kind}:${message.conversationId}`; if (command === "/new") { await this.#state.clearSession(conversationKey6); await this.#bot.sendText(target, "\u5DF2\u5F00\u542F\u65B0\u4F1A\u8BDD\u3002\u8BF7\u53D1\u9001\u4F60\u7684\u95EE\u9898\u3002"); await this.#state.markSeen(messageId); return; } let sessionId = this.#state.sessionFor(conversationKey6); if (!sessionId || !await this.#harness.sessionExists(sessionId)) { sessionId = await this.#harness.createSession(); await this.#state.setSession(conversationKey6, sessionId); } await this.#bot.sendTyping?.(target).catch((error) => { this.#logger.warn?.(`[dsh-im:${this.#descriptor.key}] typing indicator failed:`, error); }); let stream = null; let streamFinished = false; if (typeof this.#bot.openStream === "function") { try { stream = await this.#bot.openStream(target); } catch (error) { this.#logger.warn?.( `[dsh-im:${this.#descriptor.key}] unable to start a streamed reply; using text:`, error ); } } const answer = await this.#harness.ask(sessionId, text, { timeoutMs: this.#replyTimeoutMs, onUpdate: stream ? async (update) => { const progress = update.type === "text" ? update.text : update.type === "tool" ? `\u6B63\u5728\u4F7F\u7528${update.name}\u2026` : update.text; if (progress) await stream.update(progress); } : void 0 }); if (stream) { try { await stream.finish(answer); streamFinished = true; } catch (error) { stream.cancel?.(); this.#logger.warn?.( `[dsh-im:${this.#descriptor.key}] streamed reply finalization failed; using text:`, error ); } } if (!streamFinished) await this.#bot.sendText(target, answer); await this.#state.markSeen(messageId); this.#status.messagesReplied += 1; this.#status.lastReplyAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.lastError = null; } catch (error) { this.#status.lastError = error?.message ?? String(error); this.#logger.error?.(`[dsh-im:${this.#descriptor.key}] failed to process a message:`, error); try { await this.#bot.sendText(target, "\u6D88\u606F\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"); await this.#state.markSeen(messageId); } catch (sendError) { this.#logger.error?.( `[dsh-im:${this.#descriptor.key}] failed to send the safe error reply:`, sendError ); } } } }; // src/channels/discord/discord-bridge.mjs var DISCORD_DESCRIPTOR = Object.freeze({ key: "discord", label: "Discord", connectionLabel: " Gateway \u957F\u8FDE\u63A5" }); var DiscordHarnessBridge = class extends TextHarnessBridge { constructor(options) { super({ descriptor: DISCORD_DESCRIPTOR, ...options }); } }; // src/channels/discord/discord-controller.mjs var DiscordController = class extends TokenBotController { constructor(options) { super({ ...options, descriptor: DISCORD_DESCRIPTOR, inspectToken: options.inspectToken ?? inspectDiscordToken, deriveIdentity: deriveDiscordBotIdentity, maskPlatformId: maskDiscordBotId }); } }; // src/channels/weixin/harness-client.mjs import { spawn as spawn2 } from "node:child_process"; import { randomUUID as randomUUID6 } from "node:crypto"; var sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)); function assistantMessageText2(event) { return (event?.data?.message?.content ?? []).filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n").trim(); } var HarnessReplyTracker2 = class { #promptRpcId; #lastSeq; #openTurn = null; #targetTurn = null; #stepText = /* @__PURE__ */ new Map(); #latestText = ""; #finished = false; #reason = null; constructor({ promptRpcId, afterSeq = -1 }) { this.#promptRpcId = promptRpcId; this.#lastSeq = afterSeq; } get finished() { return this.#finished; } get answer() { return this.#latestText.trim(); } get reason() { return this.#reason; } consume(entries) { let update = null; const ordered = [...entries].map((entry) => entry?.event ?? entry).filter(Boolean).sort((left, right) => (left.seq ?? -1) - (right.seq ?? -1)); for (const event of ordered) { const seq = event.seq ?? -1; if (seq <= this.#lastSeq) continue; this.#lastSeq = seq; if (event.type === "turn/start") this.#openTurn = event.data?.turn ?? null; if (event.type === "user/message" && event.data?.source?.rpcId === this.#promptRpcId) { this.#targetTurn = this.#openTurn; continue; } if (this.#targetTurn === null) continue; if (event.type === "turn/end") { if (event.data?.turn !== this.#targetTurn) continue; this.#finished = true; this.#reason = event.data?.reason ?? null; this.#openTurn = null; continue; } if (event.data?.turn !== this.#targetTurn) continue; if (event.type === "assistant/chunk" && event.data?.chunk?.type === "text-delta") { const step = event.data?.step ?? 0; const index = event.data.chunk.index ?? 0; const key = `${step}:${index}`; this.#stepText.set(key, (this.#stepText.get(key) ?? "") + event.data.chunk.text); const prefix = `${step}:`; const text = [...this.#stepText.entries()].filter(([partKey]) => partKey.startsWith(prefix)).sort(([left], [right]) => Number(left.split(":")[1]) - Number(right.split(":")[1])).map(([, part]) => part).join("\n").trim(); if (text && text !== this.#latestText) { this.#latestText = text; update = { type: "text", text }; } continue; } if (event.type === "assistant/message") { const text = assistantMessageText2(event); if (text && text !== this.#latestText) { this.#latestText = text; update = { type: "text", text }; } continue; } if (event.type === "tool/call") { update = { type: "tool", name: event.data?.name ?? "\u5DE5\u5177" }; } else if (event.type === "tool/result") { update = { type: "status", text: "\u6B63\u5728\u6574\u7406\u7ED3\u679C\u2026" }; } } return update; } }; var HarnessRpcError2 = class extends Error { constructor(method, error) { super(`${method}: ${error?.message ?? "unknown Harness RPC error"}`); this.name = "HarnessRpcError"; this.method = method; this.code = error?.code ?? "internal"; this.details = error?.details ?? {}; } }; var HarnessClient2 = class { #baseUrl; #workspace; #agentPreset; #autostart; #dshBin; #managedProcess = null; constructor({ baseUrl, workspace, agentPreset = "standard", autostart = false, dshBin = "dsh" }) { this.#baseUrl = new URL(baseUrl); this.#workspace = workspace; this.#agentPreset = agentPreset; this.#autostart = autostart; this.#dshBin = dshBin; } async rpc(method, payload = {}, timeoutMs = 3e4, options = {}) { const rpcId = options.rpcId ?? `weixin-${randomUUID6()}`; const response = await fetch(new URL(`/api/${method}`, this.#baseUrl), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ type: "client-request", rpcId, method, payload }), signal: AbortSignal.timeout(timeoutMs) }); if (!response.ok) throw new Error(`Harness transport ${method} failed: HTTP ${response.status}`); const body = await response.json(); if (body?.type !== "server-response" || body?.rpcId !== rpcId) { throw new Error(`Harness returned an invalid response for ${method}`); } if (!body.result?.ok) throw new HarnessRpcError2(method, body.result?.error); return body.result.value; } async health() { await this.rpc("host.describe", {}, 5e3); return true; } async ensureRunning() { try { return await this.health(); } catch (firstError) { if (!this.#autostart) throw firstError; } if (!this.#managedProcess || this.#managedProcess.exitCode !== null) { const port = this.#baseUrl.port || (this.#baseUrl.protocol === "https:" ? "443" : "80"); this.#managedProcess = spawn2(this.#dshBin, [ "web", "--host", this.#baseUrl.hostname, "--port", port ], { cwd: this.#workspace, env: process.env, stdio: ["ignore", "inherit", "inherit"] }); this.#managedProcess.on("error", (error) => { console.error("[dsh-weixin] failed to start Harness:", error.message); }); } const deadline = Date.now() + 6e4; let lastError; while (Date.now() < deadline) { await sleep2(1e3); try { return await this.health(); } catch (error) { lastError = error; } } throw new Error(`Harness did not become ready: ${lastError?.message ?? "timeout"}`); } async workspaceId() { const { items } = await this.rpc("workspace.list", {}); const existing = items.find((item) => item.path === this.#workspace); if (existing) return existing.workspaceId; const created = await this.rpc("workspace.create", { path: this.#workspace }); return created.workspace.workspaceId; } async createSession() { await this.ensureRunning(); const workspaceId = await this.workspaceId(); const created = await this.rpc("session.create", { workspaceId, agentPreset: this.#agentPreset }); return created.sessionId; } async sessionExists(sessionId) { try { await this.rpc("session.history", { sessionId, maxMessages: 1 }); return true; } catch (error) { if (error instanceof HarnessRpcError2 && error.code === "session-not-found") return false; throw error; } } async ask(sessionId, text, options = {}) { if (typeof options === "number") options = { timeoutMs: options }; const timeoutMs = options.timeoutMs ?? 6e5; const onUpdate = typeof options.onUpdate === "function" ? options.onUpdate : null; await this.ensureRunning(); const before = await this.rpc("session.history", { sessionId, maxMessages: 1 }); const baselineSeq = Math.max(-1, ...(before.events ?? []).map(({ event }) => event.seq ?? -1)); const promptRpcId = `weixin-${randomUUID6()}`; const tracker = new HarnessReplyTracker2({ promptRpcId, afterSeq: baselineSeq }); await this.rpc("session.prompt", { sessionId, mode: "queue", content: [{ type: "text", text }], clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }, 3e4, { rpcId: promptRpcId }); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { await sleep2(300); const history = await this.rpc("session.history", { sessionId, maxMessages: 50 }); const update = tracker.consume(history.events ?? []); if (update && onUpdate) { try { await onUpdate(update); } catch (error) { console.warn("[dsh-weixin] ignored a progress update failure:", error.message); } } if (!tracker.finished) continue; if (tracker.answer) return tracker.answer; throw new Error( `Harness turn ended without a text reply${tracker.reason ? ` (${JSON.stringify(tracker.reason)})` : ""}` ); } throw new Error(`Harness reply timed out after ${Math.round(timeoutMs / 1e3)} seconds`); } stopManagedProcess() { if (this.#managedProcess?.exitCode === null) this.#managedProcess.kill("SIGTERM"); } }; // src/channels/discord/harness-client.mjs var DiscordHarnessClient = class extends HarnessClient2 { }; // src/channels/shared/editable-message-stream.mjs function splitMessageText(value, limit) { const text = typeof value === "string" ? value.trim() : ""; if (!text) return []; const chunks = []; let remaining = text; while (remaining.length > limit) { let cut = remaining.lastIndexOf("\n", limit); if (cut < Math.floor(limit * 0.55)) cut = remaining.lastIndexOf(" ", limit); if (cut < Math.floor(limit * 0.55)) cut = limit; chunks.push(remaining.slice(0, cut).trim()); remaining = remaining.slice(cut).trimStart(); } if (remaining) chunks.push(remaining); return chunks; } function createEditableMessageStream({ initialText = "\u6B63\u5728\u5904\u7406\u2026", limit, updateIntervalMs = 800, create, edit, sendRemainder, logger = console }) { let messageId; let pending = null; let timer = null; let inFlight = null; let closed = false; let lastSent = initialText; const schedule = () => { if (closed || timer !== null || inFlight || !pending) return; timer = setTimeout(() => { timer = null; const text = pending; pending = null; const next = splitMessageText(text, limit)[0] ?? initialText; inFlight = Promise.resolve(next === lastSent ? void 0 : edit(messageId, next)).then(() => { lastSent = next; }).catch((error) => logger.warn?.("[dsh-im] streamed message update failed:", error)).finally(() => { inFlight = null; schedule(); }); }, updateIntervalMs); timer?.unref?.(); }; return { async start() { messageId = await create(initialText); return this; }, update(text) { if (closed || typeof text !== "string" || !text.trim()) return; pending = text; schedule(); }, async finish(text) { if (closed) throw new Error("Message stream is already closed"); closed = true; if (timer !== null) clearTimeout(timer); timer = null; pending = null; await inFlight?.catch(() => void 0); const chunks = splitMessageText(text, limit); const first = chunks[0] ?? "\u5904\u7406\u5B8C\u6210\u3002"; if (first !== lastSent) await edit(messageId, first); lastSent = first; for (const chunk of chunks.slice(1)) await sendRemainder(chunk); }, cancel() { closed = true; pending = null; if (timer !== null) clearTimeout(timer); timer = null; } }; } // src/channels/discord/discord-runtime.mjs var DISCORD_GATEWAY_INTENTS = 1 << 0 | 1 << 9 | 1 << 12; var RECONNECT_DELAYS_MS = Object.freeze([1e3, 3e3, 5e3, 1e4, 3e4]); function socketUrl(value) { const url = new URL(value); if (url.protocol !== "wss:") throw new Error("Discord returned an insecure Gateway URL"); url.searchParams.set("v", "10"); url.searchParams.set("encoding", "json"); return url.href; } function addSocketListener(socket, event, listener) { if (typeof socket.addEventListener === "function") socket.addEventListener(event, listener); else if (typeof socket.on === "function") socket.on(event, listener); else throw new TypeError("Discord WebSocket does not support events"); } function eventData(event) { if (typeof event === "string") return event; if (typeof event?.data === "string") return event.data; if (Buffer.isBuffer(event)) return event.toString("utf8"); if (Buffer.isBuffer(event?.data)) return event.data.toString("utf8"); return null; } function gatewayCloseError(code) { if (code === 4004) { const error2 = new Error("Discord Bot Token \u65E0\u6548\uFF0C\u8BF7\u91CD\u65B0\u586B\u5199\u3002"); error2.code = "discord-401"; return error2; } if (code === 4013 || code === 4014) { const error2 = new Error("Discord Gateway Intents \u914D\u7F6E\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5 Developer Portal \u7684 Bot \u8BBE\u7F6E\u3002"); error2.code = "discord-intents"; return error2; } const error = new Error(`Discord Gateway closed (${code || "unknown"})`); error.code = "discord-gateway-closed"; return error; } function stripBotMention(text, botId) { if (typeof text !== "string") return ""; return text.replace(new RegExp(`<@!?${botId}>`, "g"), "").trim(); } function normalizeDiscordMessage(message, botId) { if (!message?.id || !message?.channel_id || !message?.author?.id) return null; const direct = !message.guild_id; const addressed = direct || message.mentions?.some((mention) => String(mention?.id) === String(botId)); return { messageId: String(message.id), senderId: String(message.author.id), senderIsBot: message.author.bot === true, kind: direct ? "direct" : "group", conversationId: String(message.channel_id), content: stripBotMention(message.content ?? "", botId), addressed, replyTarget: { channelId: String(message.channel_id), replyToMessageId: String(message.id) } }; } var DiscordBotClient = class { #api; #signal; constructor({ api, signal }) { this.#api = api; this.#signal = signal; } async sendText(target, text) { const chunks = splitMessageText(text, 1900); let result = null; for (const [index, chunk] of chunks.entries()) { result = await this.#api.createMessage({ channelId: target.channelId, content: chunk, replyToMessageId: index === 0 ? target.replyToMessageId : void 0, signal: this.#signal }); } return result; } sendTyping(target) { return this.#api.sendTyping({ channelId: target.channelId, signal: this.#signal }); } async openStream(target) { const stream = createEditableMessageStream({ limit: 1900, create: async (content) => { const message = await this.#api.createMessage({ channelId: target.channelId, content, replyToMessageId: target.replyToMessageId, signal: this.#signal }); return message.id; }, edit: (messageId, content) => this.#api.editMessage({ channelId: target.channelId, messageId, content, signal: this.#signal }), sendRemainder: (content) => this.#api.createMessage({ channelId: target.channelId, content, signal: this.#signal }) }); return stream.start(); } }; function createDiscordRuntimeStatus() { return { startedAt: null, ready: false, connectionState: "idle", harnessReachable: false, lastCheckedAt: null, lastConnectedAt: null, lastError: null, ...createTextBridgeStatus() }; } var DiscordRuntime = class { #config; #token; #harness; #state; #logger; #replyTimeoutMs; #connectTimeoutMs; #createApi; #createWebSocket; #random; #status = createDiscordRuntimeStatus(); #api = null; #bridge = null; #abortController = null; #socket = null; #gatewayUrl = null; #resumeUrl = null; #sessionId = null; #sequence = null; #heartbeatTimer = null; #heartbeatAcked = true; #reconnectTimer = null; #reconnectAttempt = 0; #generation = 0; #stopped = true; #starting = null; constructor({ config, token, harness, state, logger = console, replyTimeoutMs = 6e5, connectTimeoutMs = 2e4, createApi = (options) => new DiscordApi(options), createWebSocket = (url) => new WebSocket(url), random = Math.random }) { if (!config || !token || !harness || !state) { throw new TypeError("DiscordRuntime requires config, token, Harness, and state"); } if (typeof createWebSocket !== "function") throw new TypeError("DiscordRuntime requires WebSocket"); this.#config = config; this.#token = token; this.#harness = harness; this.#state = state; this.#logger = logger; this.#replyTimeoutMs = replyTimeoutMs; this.#connectTimeoutMs = connectTimeoutMs; this.#createApi = createApi; this.#createWebSocket = createWebSocket; this.#random = random; } get status() { return structuredClone(this.#status); } async start() { if (this.#status.ready && this.#socket) return this.status; if (this.#starting) return this.#starting; this.#starting = this.#start().finally(() => { this.#starting = null; }); return this.#starting; } async #start() { await this.stop(); this.#stopped = false; this.#sessionId = null; this.#resumeUrl = null; this.#sequence = null; this.#reconnectAttempt = 0; this.#status.startedAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.connectionState = "connecting"; this.#status.lastError = null; await this.#harness.ensureRunning(); this.#status.harnessReachable = true; const controller = new AbortController(); this.#abortController = controller; const api = this.#createApi({ token: this.#token }); this.#api = api; try { const [bot, gateway] = await Promise.all([ api.getCurrentUser({ signal: controller.signal }), api.getGatewayBot({ signal: controller.signal }) ]); if (String(bot?.id ?? "") !== this.#config.platformId || bot?.bot !== true) { throw new Error("Discord token identity does not match the saved bot"); } this.#gatewayUrl = gateway?.url; const client = new DiscordBotClient({ api, signal: controller.signal }); this.#bridge = new DiscordHarnessBridge({ bot: client, harness: this.#harness, state: this.#state, status: this.#status, logger: this.#logger, replyTimeoutMs: this.#replyTimeoutMs }); let timer; try { await Promise.race([ this.#openSocket(false), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("Discord Gateway did not become ready in time")), this.#connectTimeoutMs); }) ]); } finally { clearTimeout(timer); } return this.status; } catch (error) { this.#status.ready = false; this.#status.connectionState = "failed"; this.#status.lastError = error?.message ?? String(error); await this.stop(); throw error; } } #openSocket(resume) { if (this.#stopped) return Promise.reject(new Error("Discord runtime is stopped")); const generation = ++this.#generation; const url = socketUrl(resume && this.#resumeUrl ? this.#resumeUrl : this.#gatewayUrl); const socket = this.#createWebSocket(url); this.#socket = socket; let settled = false; return new Promise((resolve7, reject) => { const markReady = () => { if (settled || generation !== this.#generation) return; settled = true; this.#reconnectAttempt = 0; const now = Date.now(); this.#status.ready = true; this.#status.connectionState = "connected"; this.#status.lastCheckedAt = now; this.#status.lastConnectedAt = now; this.#status.lastError = null; resolve7(); }; addSocketListener(socket, "message", (event) => { if (generation !== this.#generation || this.#stopped) return; const raw = eventData(event); if (!raw) return; let packet; try { packet = JSON.parse(raw); } catch { this.#logger.warn?.("[dsh-im:discord] ignored malformed Gateway JSON"); return; } if (Number.isSafeInteger(packet.s)) this.#sequence = packet.s; if (packet.op === 10) { this.#startHeartbeat(packet.d?.heartbeat_interval, socket, generation); if (resume && this.#sessionId) { this.#sendGateway(socket, { op: 6, d: { token: this.#token, session_id: this.#sessionId, seq: this.#sequence } }); } else { this.#sendGateway(socket, { op: 2, d: { token: this.#token, intents: DISCORD_GATEWAY_INTENTS, properties: { os: process.platform, browser: "dsh-im", device: "dsh-im" } } }); } return; } if (packet.op === 11) { this.#heartbeatAcked = true; this.#status.lastCheckedAt = Date.now(); return; } if (packet.op === 1) { this.#heartbeat(socket); return; } if (packet.op === 7) { socket.close(4e3, "Reconnect requested"); return; } if (packet.op === 9) { if (packet.d !== true) { this.#sessionId = null; this.#resumeUrl = null; this.#sequence = null; } socket.close(4e3, "Invalid session"); return; } if (packet.op !== 0) return; if (packet.t === "READY") { this.#sessionId = packet.d?.session_id ?? null; this.#resumeUrl = packet.d?.resume_gateway_url ?? null; markReady(); } else if (packet.t === "RESUMED") { markReady(); } else if (packet.t === "MESSAGE_CREATE") { const message = normalizeDiscordMessage(packet.d, this.#config.platformId); if (message) void this.#bridge?.accept(message); } }); addSocketListener(socket, "close", (event = {}) => { if (generation !== this.#generation) return; this.#clearHeartbeat(); if (this.#socket === socket) this.#socket = null; if (this.#stopped) { if (!settled) reject(new DOMException("Stopped", "AbortError")); return; } const error = gatewayCloseError(Number(event.code) || 0); this.#status.ready = false; this.#status.connectionState = "connecting"; this.#status.lastError = error.message; if (!settled) { settled = true; reject(error); } if (error.code === "discord-401" || error.code === "discord-intents") { this.#status.connectionState = "failed"; return; } this.#scheduleReconnect(); }); addSocketListener(socket, "error", () => { if (generation !== this.#generation || this.#stopped) return; this.#status.lastError = "Discord Gateway WebSocket error"; }); }); } #sendGateway(socket, payload) { if (socket.readyState !== 1) return; socket.send(JSON.stringify(payload)); } #startHeartbeat(interval, socket, generation) { this.#clearHeartbeat(); if (!Number.isFinite(interval) || interval < 1e3) { socket.close(4e3, "Invalid heartbeat interval"); return; } this.#heartbeatAcked = true; const schedule = (delay3) => { this.#heartbeatTimer = setTimeout(() => { if (this.#stopped || generation !== this.#generation || this.#socket !== socket) return; if (!this.#heartbeatAcked) { socket.close(4e3, "Heartbeat was not acknowledged"); return; } this.#heartbeat(socket); schedule(interval); }, delay3); this.#heartbeatTimer?.unref?.(); }; schedule(Math.floor(interval * this.#random())); } #heartbeat(socket) { this.#heartbeatAcked = false; this.#sendGateway(socket, { op: 1, d: this.#sequence }); } #clearHeartbeat() { if (this.#heartbeatTimer !== null) clearTimeout(this.#heartbeatTimer); this.#heartbeatTimer = null; this.#heartbeatAcked = true; } #scheduleReconnect() { if (this.#stopped || this.#reconnectTimer !== null) return; const delay3 = RECONNECT_DELAYS_MS[Math.min(this.#reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)]; this.#reconnectAttempt += 1; this.#reconnectTimer = setTimeout(() => { this.#reconnectTimer = null; void this.#openSocket(Boolean(this.#sessionId)).catch((error) => { if (this.#stopped) return; this.#logger.warn?.("[dsh-im:discord] Gateway reconnect failed:", error); this.#scheduleReconnect(); }); }, delay3); this.#reconnectTimer?.unref?.(); } async stop() { this.#stopped = true; this.#generation += 1; this.#abortController?.abort(); this.#abortController = null; if (this.#reconnectTimer !== null) clearTimeout(this.#reconnectTimer); this.#reconnectTimer = null; this.#clearHeartbeat(); const socket = this.#socket; const bridge = this.#bridge; this.#socket = null; this.#bridge = null; this.#api = null; try { if (socket && socket.readyState < 2) socket.close(1e3, "Plugin stopped"); } catch (error) { this.#logger.warn?.(`[dsh-im:discord] bot ${this.#config.botId} failed to close Gateway:`, error); } await Promise.race([ bridge?.waitForIdle() ?? Promise.resolve(), new Promise((resolve7) => setTimeout(resolve7, 2e3)) ]); this.#status.ready = false; this.#status.connectionState = "idle"; return this.status; } }; // src/channels/shared/conversation-state-store.mjs import { mkdir as mkdir4, readFile as readFile4, rename as rename4, unlink as unlink5, writeFile as writeFile4 } from "node:fs/promises"; import { dirname as dirname4 } from "node:path"; var EMPTY_STATE2 = Object.freeze({ version: 1, sessions: {}, seenMessageIds: [], cursor: null }); function normalizeState2(value) { if (!value || typeof value !== "object") return structuredClone(EMPTY_STATE2); const sessions = {}; if (value.sessions && typeof value.sessions === "object" && !Array.isArray(value.sessions)) { for (const [key, sessionId] of Object.entries(value.sessions)) { if (typeof key === "string" && key && typeof sessionId === "string" && sessionId) { sessions[key] = sessionId; } } } return { version: 1, sessions, seenMessageIds: Array.isArray(value.seenMessageIds) ? value.seenMessageIds.filter((id) => typeof id === "string" && id).slice(-1e3) : [], cursor: Number.isSafeInteger(value.cursor) && value.cursor >= 0 ? value.cursor : null }; } var ConversationStateStore = class { #path; #state = structuredClone(EMPTY_STATE2); #writeQueue = Promise.resolve(); constructor(path) { this.#path = path; } async load() { try { this.#state = normalizeState2(JSON.parse(await readFile4(this.#path, "utf8"))); } catch (error) { if (error?.code !== "ENOENT") throw error; this.#state = structuredClone(EMPTY_STATE2); await this.#persist(); } return this; } sessionFor(key) { return this.#state.sessions[key] ?? null; } async setSession(key, sessionId) { this.#state.sessions[key] = sessionId; await this.#persist(); } async clearSession(key) { delete this.#state.sessions[key]; await this.#persist(); } hasSeen(messageId) { return this.#state.seenMessageIds.includes(messageId); } async markSeen(messageId) { if (this.hasSeen(messageId)) return; this.#state.seenMessageIds.push(messageId); if (this.#state.seenMessageIds.length > 1e3) { this.#state.seenMessageIds.splice(0, this.#state.seenMessageIds.length - 1e3); } await this.#persist(); } cursor() { return this.#state.cursor; } async setCursor(cursor) { if (!Number.isSafeInteger(cursor) || cursor < 0) throw new TypeError("Invalid update cursor"); this.#state.cursor = cursor; await this.#persist(); } snapshot() { return structuredClone(this.#state); } async remove() { try { await unlink5(this.#path); } catch (error) { if (error?.code !== "ENOENT") throw error; } this.#state = structuredClone(EMPTY_STATE2); } async #persist() { const snapshot = `${JSON.stringify(this.#state, null, 2)} `; const operation = this.#writeQueue.then(async () => { await mkdir4(dirname4(this.#path), { recursive: true, mode: 448 }); const temporary = `${this.#path}.tmp`; await writeFile4(temporary, snapshot, { encoding: "utf8", mode: 384 }); await rename4(temporary, this.#path); }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; } }; // src/channels/discord/state-store.mjs var DiscordStateStore = class extends ConversationStateStore { }; // plugin-src/host/channels/shared/production.mjs import { unlink as unlink6 } from "node:fs/promises"; import { homedir as homedir2 } from "node:os"; import { join as join2, resolve as resolve2 } from "node:path"; // plugin-src/host/channels/shared/connection-supervisor.mjs var DEFAULT_RETRY_DELAYS_MS2 = Object.freeze([250, 1e3, 3e3, 5e3, 1e4, 3e4]); function retryDelays2(value) { if (!Array.isArray(value) || value.length === 0) return [...DEFAULT_RETRY_DELAYS_MS2]; const valid = value.filter((delay3) => Number.isFinite(delay3) && delay3 >= 0); return valid.length > 0 ? valid : [...DEFAULT_RETRY_DELAYS_MS2]; } var TokenConnectionSupervisor = class { #channel; #controller; #harness; #logger; #retryDelays; #healthyIntervalMs; #setTimeout; #clearTimeout; #timer = null; #running = null; #retryIndex = 0; #closed = false; #started = false; #ready; #resolveReady; constructor({ channel, controller, harness, logger = console, retryDelaysMs, healthyIntervalMs = 15e3, setTimeoutImpl = setTimeout, clearTimeoutImpl = clearTimeout }) { if (!channel) throw new TypeError("TokenConnectionSupervisor requires a channel"); if (!controller || typeof controller.initialize !== "function" || typeof controller.status !== "function") { throw new TypeError("TokenConnectionSupervisor requires a controller"); } if (!harness || typeof harness.ensureRunning !== "function") { throw new TypeError("TokenConnectionSupervisor requires a Harness client"); } this.#channel = channel; this.#controller = controller; this.#harness = harness; this.#logger = logger; this.#retryDelays = retryDelays2(retryDelaysMs); this.#healthyIntervalMs = Number.isFinite(healthyIntervalMs) && healthyIntervalMs >= 0 ? healthyIntervalMs : 15e3; this.#setTimeout = setTimeoutImpl; this.#clearTimeout = clearTimeoutImpl; this.#ready = new Promise((resolve7) => { this.#resolveReady = resolve7; }); } get ready() { return this.#ready; } start() { if (this.#started || this.#closed) return this; this.#started = true; this.#schedule(0); return this; } async close() { if (this.#closed) return; this.#closed = true; if (this.#timer !== null) this.#clearTimeout(this.#timer); this.#timer = null; await this.#running?.catch(() => void 0); this.#resolveReady?.(null); this.#resolveReady = null; } #schedule(delayMs) { if (this.#closed) return; this.#timer = this.#setTimeout(() => { this.#timer = null; void this.#run(); }, delayMs); this.#timer?.unref?.(); } async #run() { if (this.#closed || this.#running) return; const operation = this.#reconcile(); this.#running = operation; try { await operation; } finally { if (this.#running === operation) this.#running = null; } } async #reconcile() { try { await this.#harness.ensureRunning(); if (this.#closed) return; const status = await this.#controller.initialize(); if (this.#closed) return; this.#resolveReady?.(status); this.#resolveReady = null; const { configured, connected } = status.totals; if (connected < configured) { const delay3 = this.#retryDelays[Math.min(this.#retryIndex, this.#retryDelays.length - 1)]; this.#retryIndex += 1; this.#logger.warn?.( `[dsh-im:${this.#channel}] ${connected}/${configured} bots connected; retrying in ${delay3}ms` ); this.#schedule(delay3); return; } this.#retryIndex = 0; this.#schedule(this.#healthyIntervalMs); } catch (error) { if (this.#closed) return; const delay3 = this.#retryDelays[Math.min(this.#retryIndex, this.#retryDelays.length - 1)]; this.#retryIndex += 1; this.#logger.warn?.( `[dsh-im:${this.#channel}] connection reconciliation failed; retrying in ${delay3}ms`, error ); this.#schedule(delay3); } } }; function createTokenConnectionSupervisor(options) { return new TokenConnectionSupervisor(options); } // plugin-src/host/channels/shared/production.mjs function harnessOrigin2(webServer, configured) { if (configured !== void 0) return new URL(configured); const port = webServer?.port; if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error("dsh-im token channel requires an initialized DSH webServer port"); } return new URL(`http://127.0.0.1:${port}`); } function pluginPaths2(config, channel) { const dshHome = resolve2(config.dshHome ?? process.env.DSH_HOME ?? join2(homedir2(), ".dsh")); const root = resolve2(config.dataDir ?? join2(dshHome, "integrations", `dsh-${channel}`)); return { config: resolve2(config.configPath ?? join2(root, "config.json")), bots: resolve2(config.botsDir ?? join2(root, "bots")) }; } async function createTokenProductionController(ctx, config, internals, definitions) { const { channel, ConfigStore, StateStore: StateStore2, HarnessClient: HarnessClient4, Controller, Runtime } = definitions; if (!ctx?.credentials) throw new TypeError(`dsh-im ${channel} requires ctx.credentials`); if (!ctx?.webServer) throw new TypeError(`dsh-im ${channel} requires ctx.webServer`); const ResolvedConfigStore = internals.ConfigStore ?? ConfigStore; const ResolvedStateStore = internals.StateStore ?? StateStore2; const ResolvedHarness = internals.HarnessClient ?? HarnessClient4; const ResolvedController = internals.Controller ?? Controller; const ResolvedRuntime = internals.Runtime ?? Runtime; const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor; const logger = typeof ctx.logger === "function" ? ctx.logger(`dsh-im:${channel}`) : ctx.logger ?? console; const paths = pluginPaths2(config, channel); const configStore = await new ResolvedConfigStore(paths.config).load(); const stateStores = /* @__PURE__ */ new Map(); const statePath = (botId) => resolve2(paths.bots, botId, "state.json"); const stateFor = async (botId) => { let state = stateStores.get(botId); if (!state) { state = await new ResolvedStateStore(statePath(botId)).load(); stateStores.set(botId, state); } return state; }; const harness = new ResolvedHarness({ baseUrl: harnessOrigin2(ctx.webServer, config.harnessBaseUrl), workspace: resolve2(config.workspace ?? process.cwd()), agentPreset: config.agentPreset ?? "standard", autostart: false, dshBin: config.dshBin ?? "dsh" }); const controller = new ResolvedController({ credentials: ctx.credentials, configStore, logger, ...internals.inspectToken ? { inspectToken: internals.inspectToken } : {}, createRuntime: async ({ botId, config: botConfig, token }) => new ResolvedRuntime({ config: botConfig, token, harness, state: await stateFor(botId), replyTimeoutMs: config.replyTimeoutMs ?? 6e5, connectTimeoutMs: config.connectTimeoutMs ?? 2e4, logger: { error: (...args) => logger.error?.(`[${botId}]`, ...args), warn: (...args) => logger.warn?.(`[${botId}]`, ...args), info: (...args) => logger.info?.(`[${botId}]`, ...args), debug: (...args) => logger.debug?.(`[${botId}]`, ...args) } }), deleteState: async ({ botId }) => { const state = stateStores.get(botId); stateStores.delete(botId); if (state && typeof state.remove === "function") return state.remove(); try { await unlink6(statePath(botId)); } catch (error) { if (error?.code !== "ENOENT") throw error; } } }); const supervisor = createSupervisor({ channel, controller, harness, logger, retryDelaysMs: config.retryDelaysMs, healthyIntervalMs: config.healthyIntervalMs }).start(); return { controller, ready: supervisor.ready, async close() { await supervisor.close(); await controller.close(); harness.stopManagedProcess(); } }; } // plugin-src/host/channels/discord/production.mjs function createProductionController2(ctx, config = {}, internals = {}) { return createTokenProductionController(ctx, config, internals, { channel: "discord", ConfigStore: DiscordConfigStore, StateStore: DiscordStateStore, HarnessClient: DiscordHarnessClient, Controller: DiscordController, Runtime: DiscordRuntime }); } // plugin-src/host/channels/shared/rpc.mjs var TOKEN_BOT_ENDPOINTS = Object.freeze({ status: "connection.status", bindCredentials: "bot.bind-credentials", reconnectBot: "bot.reconnect", deleteBot: "bot.delete" }); var ENDPOINTS = Object.freeze(Object.values(TOKEN_BOT_ENDPOINTS)); var FORBIDDEN_PUBLIC_KEYS2 = /* @__PURE__ */ new Set([ "token", "botToken", "tokenRef", "platformId", "secret", "secretRef" ]); function isRecord2(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function exactKeys2(value, allowed) { return isRecord2(value) && Object.keys(value).every((key) => allowed.includes(key)); } function validId2(value) { return typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value); } function validToken(value) { return typeof value === "string" && value.trim().length >= 20 && value.length <= 4096; } function payloadFailure2(endpoint, payload) { if (!isRecord2(payload)) return "Payload must be an object."; if (endpoint === TOKEN_BOT_ENDPOINTS.status) { return exactKeys2(payload, []) ? null : "connection.status does not accept fields."; } if (endpoint === TOKEN_BOT_ENDPOINTS.bindCredentials) { return exactKeys2(payload, ["token"]) && validToken(payload.token) ? null : "bot.bind-credentials requires a Bot Token."; } if (endpoint === TOKEN_BOT_ENDPOINTS.reconnectBot) { return exactKeys2(payload, ["botId"]) && validId2(payload.botId) ? null : "bot.reconnect requires a botId."; } if (endpoint === TOKEN_BOT_ENDPOINTS.deleteBot) { return exactKeys2(payload, ["botId", "confirm"]) && validId2(payload.botId) && payload.confirm === true ? null : "bot.delete requires a botId and confirm=true."; } return "Unknown bot endpoint."; } function sanitizePublic2(value) { if (Array.isArray(value)) return value.map(sanitizePublic2); if (!isRecord2(value)) return value; const safe = {}; for (const [key, child] of Object.entries(value)) { if (!FORBIDDEN_PUBLIC_KEYS2.has(key)) safe[key] = sanitizePublic2(child); } return safe; } function operationError(channel, error) { if (error?.code === "webhook-configured") { return { code: "webhook-configured", message: error.message }; } if (error?.code === "telegram-401" || error?.code === "discord-401") { return { code: "invalid-token", message: `${channel} Bot Token \u65E0\u6548\uFF0C\u8BF7\u91CD\u65B0\u586B\u5199\u3002` }; } if (error?.code === "discord-intents") { return { code: "discord-intents", message: error.message }; } return { code: `${channel.toLowerCase()}-operation-failed`, message: `${channel} \u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002` }; } function createTokenBotRpcHandler(controller, { channel }) { for (const method of ["status", "bindCredentials", "reconnectBot", "deleteBot"]) { if (typeof controller?.[method] !== "function") { throw new TypeError(`A complete ${channel} controller is required (${method})`); } } return async (endpoint, payload, signal) => { if (signal?.aborted) { return { ok: false, error: { code: "cancelled", message: "The request was cancelled." } }; } if (!ENDPOINTS.includes(endpoint)) { return { ok: false, error: { code: "bad-request", message: `Unknown ${channel} endpoint.` } }; } const invalid = payloadFailure2(endpoint, payload); if (invalid) return { ok: false, error: { code: "bad-request", message: invalid } }; try { let value; if (endpoint === TOKEN_BOT_ENDPOINTS.status) value = await controller.status(); else if (endpoint === TOKEN_BOT_ENDPOINTS.bindCredentials) { value = await controller.bindCredentials(payload); } else if (endpoint === TOKEN_BOT_ENDPOINTS.reconnectBot) { value = await controller.reconnectBot(payload.botId); } else { value = await controller.deleteBot(payload.botId); } return signal?.aborted ? { ok: false, error: { code: "cancelled", message: "The request was cancelled." } } : { ok: true, value: sanitizePublic2(value) }; } catch (error) { return signal?.aborted ? { ok: false, error: { code: "cancelled", message: "The request was cancelled." } } : { ok: false, error: operationError(channel, error) }; } }; } function installTokenBotRpc(ctx, controller, { channel, rpcChannel, authority }) { if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== "function") { throw new TypeError("DSH Host Connection RPC is required"); } return ctx.connection.rpc.handle( rpcChannel, createTokenBotRpcHandler(controller, { channel }), { authority: resolveRpcAuthority(authority) } ); } // plugin-src/host/channels/discord/rpc.mjs var DISCORD_RPC_CHANNEL = "/discord"; var DISCORD_ENDPOINTS = TOKEN_BOT_ENDPOINTS; var DISCORD_RPC_ENDPOINTS = Object.freeze(Object.values(DISCORD_ENDPOINTS)); function installDiscordRpc(ctx, controller, authority) { return installTokenBotRpc(ctx, controller, { channel: "Discord", rpcChannel: DISCORD_RPC_CHANNEL, authority }); } // plugin-src/host/channels/discord/index.mjs async function apply2(ctx, config = {}) { if (config?.controller) { return installDiscordRpc(ctx, config.controller, config.rpcAuthority); } const production = await createProductionController2(ctx, config, config.internals ?? {}); const disposeRpc = installDiscordRpc(ctx, production.controller, config.rpcAuthority); ctx.effect(() => async () => production.close(), "dsh-im: close Discord bot connections"); return disposeRpc; } // plugin-src/host/channels/feishu/controller.mjs var ACTIVE_REGISTRATION_STATES = /* @__PURE__ */ 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; } var ProvisioningBackedController = class { #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 { 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."); } } }; function createProvisioningBackedController(options) { return new ProvisioningBackedController(options); } // plugin-src/host/channels/feishu/production.mjs import { homedir as homedir3 } from "node:os"; import { join as join3, resolve as resolve3 } from "node:path"; import { unlink as unlink8 } from "node:fs/promises"; import * as Lark from "@larksuiteoapi/node-sdk"; // plugin-src/host/channels/feishu/connection-supervisor.mjs var DEFAULT_RETRY_DELAYS_MS3 = Object.freeze([250, 1e3, 3e3, 5e3, 1e4, 3e4]); function safeDelay(value, fallback) { return Number.isFinite(value) && value >= 0 ? value : fallback; } function safeRetryDelays(value) { if (!Array.isArray(value) || value.length === 0) return [...DEFAULT_RETRY_DELAYS_MS3]; const delays = value.map((delay3) => safeDelay(delay3, -1)).filter((delay3) => delay3 >= 0); return delays.length > 0 ? delays : [...DEFAULT_RETRY_DELAYS_MS3]; } function totals(status) { const configured = Number.isInteger(status?.totals?.configured) ? status.totals.configured : Array.isArray(status?.bots) ? status.bots.length : 0; const connected = Number.isInteger(status?.totals?.connected) ? status.totals.connected : Array.isArray(status?.bots) ? status.bots.filter((bot) => bot?.connected === true).length : 0; return { configured, connected }; } var ConnectionSupervisor2 = class { #controller; #harness; #logger; #retryDelaysMs; #healthyIntervalMs; #setTimeout; #clearTimeout; #timer = null; #running = null; #retryIndex = 0; #started = false; #closed = false; #ready; #resolveReady; constructor({ controller, harness, logger = console, retryDelaysMs, healthyIntervalMs = 15e3, setTimeoutImpl = setTimeout, clearTimeoutImpl = clearTimeout }) { if (!controller || typeof controller.initialize !== "function" || typeof controller.status !== "function") { throw new TypeError("ConnectionSupervisor requires a controller"); } if (!harness || typeof harness.ensureRunning !== "function") { throw new TypeError("ConnectionSupervisor requires a Harness client"); } this.#controller = controller; this.#harness = harness; this.#logger = logger; this.#retryDelaysMs = safeRetryDelays(retryDelaysMs); this.#healthyIntervalMs = safeDelay(healthyIntervalMs, 15e3); this.#setTimeout = setTimeoutImpl; this.#clearTimeout = clearTimeoutImpl; this.#ready = new Promise((resolve7) => { this.#resolveReady = resolve7; }); } get ready() { return this.#ready; } start() { if (this.#started || this.#closed) return this; this.#started = true; this.#schedule(0); return this; } async close() { if (this.#closed) return; this.#closed = true; if (this.#timer !== null) { this.#clearTimeout(this.#timer); this.#timer = null; } await this.#running?.catch(() => void 0); this.#resolveReady?.(null); this.#resolveReady = null; } #schedule(delayMs) { if (this.#closed) return; this.#timer = this.#setTimeout(() => { this.#timer = null; void this.#run(); }, delayMs); this.#timer?.unref?.(); } async #run() { if (this.#closed || this.#running) return; const operation = this.#reconcile(); this.#running = operation; try { await operation; } finally { if (this.#running === operation) this.#running = null; } } async #reconcile() { try { await this.#harness.ensureRunning(); } catch (error) { if (this.#closed) return; this.#retry("Harness Host is not ready", error); return; } if (this.#closed) return; try { await this.#controller.initialize(); if (this.#closed) return; const status = this.#controller.status(); this.#resolveReady?.(status); this.#resolveReady = null; const current = totals(status); if (current.connected < current.configured) { const delay3 = this.#retryDelaysMs[Math.min(this.#retryIndex, this.#retryDelaysMs.length - 1)]; this.#retryIndex += 1; this.#logger.warn?.( `[dsh-feishu] ${current.connected}/${current.configured} bots connected; retrying automatically in ${delay3}ms` ); this.#schedule(delay3); return; } this.#retryIndex = 0; this.#schedule(this.#healthyIntervalMs); } catch (error) { if (this.#closed) return; this.#retry("Bot connection reconciliation failed", error); } } #retry(message, error) { const delay3 = this.#retryDelaysMs[Math.min(this.#retryIndex, this.#retryDelaysMs.length - 1)]; this.#retryIndex += 1; this.#logger.warn?.( `[dsh-feishu] ${message}; retrying automatically in ${delay3}ms`, error ); this.#schedule(delay3); } }; function createConnectionSupervisor2(options) { return new ConnectionSupervisor2(options); } // src/channels/feishu/feishu-app.mjs function endpointFor(domain, path) { const origin = domain === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn"; return new URL(path, origin); } async function jsonResponse(response, operation) { let body; try { body = await response.json(); } catch { throw new Error(`${operation} returned a non-JSON response`); } if (!response.ok || body?.code !== 0) { throw new Error(`${operation} failed: ${body?.msg || `HTTP ${response.status}`}`); } return body; } async function verifyFeishuApp({ appId, appSecret, domain = "feishu", fetchImpl = fetch, timeoutMs = 15e3 }) { if (!appId || !appSecret) throw new Error("Feishu credentials are incomplete"); const tokenResponse = await fetchImpl(endpointFor(domain, "/open-apis/auth/v3/tenant_access_token/internal"), { method: "POST", headers: { "content-type": "application/json; charset=utf-8" }, body: JSON.stringify({ app_id: appId, app_secret: appSecret }), signal: AbortSignal.timeout(timeoutMs) }); const tokenBody = await jsonResponse(tokenResponse, "Feishu authentication"); if (!tokenBody.tenant_access_token) { throw new Error("Feishu authentication returned no tenant access token"); } const botResponse = await fetchImpl(endpointFor(domain, "/open-apis/bot/v3/info/"), { headers: { authorization: `Bearer ${tokenBody.tenant_access_token}` }, signal: AbortSignal.timeout(timeoutMs) }); const botBody = await jsonResponse(botResponse, "Feishu bot verification"); const bot = botBody.bot ?? {}; return Object.freeze({ appId, name: bot.app_name ?? bot.bot_name ?? null, openId: bot.open_id ?? null, activated: bot.activate_status ?? null }); } // src/channels/feishu/message-utils.mjs function conversationKey2(event) { const chatType = event?.message?.chat_type; if (chatType === "p2p") { const senderId = event?.sender?.sender_id?.open_id || event?.sender?.sender_id?.user_id; if (!senderId) throw new Error("Feishu p2p event has no sender id"); return `p2p:${senderId}`; } const chatId = event?.message?.chat_id; if (!chatId) throw new Error("Feishu group event has no chat id"); return `group:${chatId}`; } function extractText(event) { if (event?.message?.message_type !== "text") return null; let parsed; try { parsed = JSON.parse(event.message.content); } catch { return null; } let text = typeof parsed.text === "string" ? parsed.text : ""; for (const mention of event.message.mentions ?? []) { if (typeof mention.key === "string" && mention.key) text = text.replaceAll(mention.key, ""); } return text.trim(); } function splitText(text, maxChars = 9e3) { if (text.length <= maxChars) return [text]; const chunks = []; let remaining = text; while (remaining.length > maxChars) { let splitAt = remaining.lastIndexOf("\n", maxChars); if (splitAt < Math.floor(maxChars * 0.6)) splitAt = maxChars; chunks.push(remaining.slice(0, splitAt)); remaining = remaining.slice(splitAt).replace(/^\n+/, ""); } if (remaining) chunks.push(remaining); return chunks; } function isBotSender(event) { return event?.sender?.sender_type === "bot"; } function isAllowedSender(event, allowedOpenIds) { if (!allowedOpenIds || allowedOpenIds.size === 0) return false; if (allowedOpenIds.has("*")) return true; const senderOpenId = event?.sender?.sender_id?.open_id; return typeof senderOpenId === "string" && allowedOpenIds.has(senderOpenId); } // src/channels/feishu/bridge.mjs var HELP_TEXT2 = [ "\u5317\u6C47\u661F\u6CB3 AIOS \u5DF2\u8FDE\u63A5 DeepSeek Harness\u3002", "", "\u76F4\u63A5\u53D1\u9001\u95EE\u9898\u5373\u53EF\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\u3002", "/new \u5F00\u542F\u4E00\u4E2A\u5168\u65B0\u4F1A\u8BDD", "/status \u68C0\u67E5\u8FDE\u63A5\u72B6\u6001", "/help \u663E\u793A\u672C\u5E2E\u52A9" ].join("\n"); var FeishuHarnessBridge = class { #client; #channel; #harness; #state; #queues = /* @__PURE__ */ new Map(); #acceptedMessageIds = /* @__PURE__ */ new Set(); #status; #allowedSenderOpenIds; #replyTimeoutMs; constructor({ client, channel, harness, state, status, allowedSenderOpenIds = /* @__PURE__ */ new Set(), replyTimeoutMs = 6e5 }) { this.#client = client; this.#channel = channel; this.#harness = harness; this.#state = state; this.#status = status; this.#allowedSenderOpenIds = allowedSenderOpenIds; this.#replyTimeoutMs = replyTimeoutMs; } accept(event) { const messageId = event?.message?.message_id; if (!messageId || isBotSender(event) || event?.message?.message_type !== "text") return; if (!isAllowedSender(event, this.#allowedSenderOpenIds)) { this.#status.messagesRejected += 1; this.#status.lastRejectedAt = (/* @__PURE__ */ new Date()).toISOString(); console.warn("[bridge] ignored a message from a sender outside the allowlist"); return; } if (this.#state.hasSeen(messageId) || this.#acceptedMessageIds.has(messageId)) return; this.#acceptedMessageIds.add(messageId); const processingReaction = this.#addReaction(messageId, "OnIt"); const key = conversationKey2(event); const previous = this.#queues.get(key) ?? Promise.resolve(); const task = previous.catch(() => void 0).then(() => this.#handle(event, key)).then(() => this.#finishReaction(messageId, processingReaction, "DONE")).catch(async (error) => { console.error("[bridge] message handling failed:", error.message); this.#status.lastError = error.message; await this.#finishReaction(messageId, processingReaction, "ERROR"); await this.#send( event.message.chat_id, "\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002\u5982\u679C\u95EE\u9898\u6301\u7EED\uFF0C\u8BF7\u5728 DeepSeek Harness \u7684\u98DE\u4E66\u63D2\u4EF6\u9875\u9762\u68C0\u67E5\u8FDE\u63A5\u72B6\u6001\u3002" ).catch(() => void 0); }).finally(() => { this.#acceptedMessageIds.delete(messageId); if (this.#queues.get(key) === task) this.#queues.delete(key); }); this.#queues.set(key, task); } async waitForIdle() { await Promise.allSettled([...this.#queues.values()]); } async #handle(event, key) { const messageId = event.message.message_id; await this.#state.markSeen(messageId); this.#status.lastMessageAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.messagesReceived += 1; const text = extractText(event); if (!text) return; if (text === "/help") { await this.#send(event.message.chat_id, HELP_TEXT2); return; } if (text === "/new") { await this.#state.clearSession(key); await this.#send(event.message.chat_id, "\u5DF2\u5F00\u542F\u5168\u65B0 Harness \u4F1A\u8BDD\u3002"); return; } if (text === "/status") { await this.#harness.ensureRunning(); await this.#send(event.message.chat_id, "\u98DE\u4E66\u673A\u5668\u4EBA\u4E0E DeepSeek Harness \u8FDE\u63A5\u6B63\u5E38\u3002"); return; } let sessionId = this.#state.sessionFor(key); if (!sessionId || !await this.#harness.sessionExists(sessionId)) { sessionId = await this.#harness.createSession(); await this.#state.setSession(key, sessionId); } console.info(`[bridge] processing ${event.message.chat_type} message ${messageId} in ${sessionId}`); await this.#answerWithStream(event, sessionId, text); this.#status.messagesReplied += 1; this.#status.lastReplyAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.lastError = null; } async #answerWithStream(event, sessionId, text) { const chatId = event.message.chat_id; const messageId = event.message.message_id; if (!this.#channel?.stream) { const answer = await this.#harness.ask(sessionId, text, { timeoutMs: this.#replyTimeoutMs }); for (const chunk of splitText(answer)) await this.#send(chatId, chunk); this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1; return; } let promptStarted = false; let completedAnswer = ""; try { await this.#channel.stream(chatId, { markdown: async (controller) => { promptStarted = true; completedAnswer = await this.#harness.ask(sessionId, text, { timeoutMs: this.#replyTimeoutMs, onUpdate: async (update) => { await controller.setContent(this.#progressText(update)); this.#status.streamUpdates = (this.#status.streamUpdates ?? 0) + 1; } }); await controller.setContent(completedAnswer); } }, { replyTo: messageId }); this.#status.streamResponses = (this.#status.streamResponses ?? 0) + 1; } catch (error) { this.#status.streamErrors = (this.#status.streamErrors ?? 0) + 1; if (completedAnswer) { console.warn("[bridge] native Feishu stream failed after generation; sending final text:", error.message); for (const chunk of splitText(completedAnswer)) await this.#send(chatId, chunk); this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1; return; } if (promptStarted) throw error; console.warn("[bridge] native Feishu stream unavailable; using text fallback:", error.message); const answer = await this.#harness.ask(sessionId, text, { timeoutMs: this.#replyTimeoutMs }); for (const chunk of splitText(answer)) await this.#send(chatId, chunk); this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1; } } #progressText(update) { if (update.type === "text" && update.text) return update.text; if (update.type === "tool") { if (update.name === "web_search") return "_\u6B63\u5728\u641C\u7D22\u7F51\u7EDC\u5E76\u6574\u7406\u4FE1\u606F\u2026_"; return `_\u6B63\u5728\u4F7F\u7528 ${update.name || "\u5DE5\u5177"}\u2026_`; } return `_${update.text || "\u6B63\u5728\u5904\u7406\u2026"}_`; } async #addReaction(messageId, emojiType) { if (!this.#channel?.addReaction) return null; try { const reactionId = await this.#channel.addReaction(messageId, emojiType); this.#status.reactionsAdded = (this.#status.reactionsAdded ?? 0) + 1; return reactionId; } catch (error) { this.#status.reactionErrors = (this.#status.reactionErrors ?? 0) + 1; console.warn(`[bridge] unable to add ${emojiType} reaction:`, error.message); return null; } } async #finishReaction(messageId, processingReaction, finalEmojiType) { const reactionId = await processingReaction; if (reactionId && this.#channel?.removeReaction) { try { await this.#channel.removeReaction(messageId, reactionId); this.#status.reactionsRemoved = (this.#status.reactionsRemoved ?? 0) + 1; } catch (error) { this.#status.reactionErrors = (this.#status.reactionErrors ?? 0) + 1; console.warn("[bridge] unable to remove processing reaction:", error.message); } } await this.#addReaction(messageId, finalEmojiType); } async #send(chatId, text) { const response = await this.#client.im.v1.message.create({ params: { receive_id_type: "chat_id" }, data: { receive_id: chatId, msg_type: "text", content: JSON.stringify({ text }) } }); if (response?.code && response.code !== 0) { throw new Error(`Feishu send failed: ${response.msg || response.code}`); } } }; // src/channels/feishu/feishu-channel.mjs var STREAM_ELEMENT_ID = "stream_md"; var DEFAULT_INITIAL_TEXT = "\u5DF2\u8FDE\u63A5 DeepSeek Harness\uFF0C\u6B63\u5728\u601D\u8003\u2026"; var MAX_STREAM_CHARS = 28e3; function assertApiSuccess(operation, response) { if (response?.code && response.code !== 0) { throw new Error(`${operation} failed: ${response.msg || response.code}`); } return response; } function summaryOf(text) { const summary = String(text ?? "").replace(/\s+/g, " ").trim(); return summary.length <= 50 ? summary : `${summary.slice(0, 49)}\u2026`; } function streamingCard(initialText) { return { schema: "2.0", config: { streaming_mode: true, summary: { content: "\u6B63\u5728\u751F\u6210\u2026" }, streaming_config: { print_frequency_ms: { default: 70 }, print_step: { default: 1 }, print_strategy: "fast" } }, body: { elements: [{ tag: "markdown", element_id: STREAM_ELEMENT_ID, content: initialText }] } }; } var VerifiedFeishuChannel = class { #client; #initialText; constructor({ client, initialText = DEFAULT_INITIAL_TEXT }) { this.#client = client; this.#initialText = initialText; } async stream(chatId, input, options = {}) { if (typeof input?.markdown !== "function") { throw new Error("Feishu stream requires a markdown producer"); } let messageId = null; const cardResponse = assertApiSuccess("Feishu card.create", await this.#client.cardkit.v1.card.create({ data: { type: "card_json", data: JSON.stringify(streamingCard(this.#initialText)) } })); const cardId = cardResponse?.data?.card_id; if (!cardId) throw new Error("Feishu card.create returned no card_id"); try { messageId = await this.#sendCard(chatId, cardId, options.replyTo); let sequence = 0; let lastContent = this.#initialText; const controller = { messageId, setContent: async (content) => { const next = String(content ?? "") || "\u2026"; if (next === lastContent) return; if (next.length > MAX_STREAM_CHARS) { throw new Error(`Feishu stream content exceeds ${MAX_STREAM_CHARS} characters`); } const response = await this.#client.cardkit.v1.cardElement.content({ path: { card_id: cardId, element_id: STREAM_ELEMENT_ID }, data: { content: next, sequence: ++sequence, uuid: `content_${cardId}_${sequence}` } }); assertApiSuccess("Feishu cardElement.content", response); lastContent = next; } }; await input.markdown(controller); const finishResponse = await this.#client.cardkit.v1.card.settings({ path: { card_id: cardId }, data: { settings: JSON.stringify({ config: { streaming_mode: false, summary: { content: summaryOf(lastContent) || "\u56DE\u7B54\u5B8C\u6210" } } }), sequence: ++sequence, uuid: `settings_${cardId}_${sequence}` } }); assertApiSuccess("Feishu card.settings", finishResponse); return { messageId }; } catch (error) { if (messageId) await this.#recall(messageId); throw error; } } async #sendCard(chatId, cardId, replyTo) { const content = JSON.stringify({ type: "card", data: { card_id: cardId } }); const response = replyTo ? await this.#client.im.v1.message.reply({ path: { message_id: replyTo }, data: { msg_type: "interactive", content } }) : await this.#client.im.v1.message.create({ params: { receive_id_type: "chat_id" }, data: { receive_id: chatId, msg_type: "interactive", content } }); assertApiSuccess("Feishu message send", response); const messageId = response?.data?.message_id; if (!messageId) throw new Error("Feishu message send returned no message_id"); return messageId; } async #recall(messageId) { try { const response = await this.#client.im.v1.message.delete({ path: { message_id: messageId } }); assertApiSuccess("Feishu message delete", response); } catch (error) { console.warn("[bridge] unable to recall a failed streaming card:", error.message); } } async addReaction(messageId, emojiType) { const response = assertApiSuccess("Feishu reaction.create", await this.#client.im.v1.messageReaction.create({ path: { message_id: messageId }, data: { reaction_type: { emoji_type: emojiType } } })); const reactionId = response?.data?.reaction_id; if (!reactionId) throw new Error("Feishu reaction.create returned no reaction_id"); return reactionId; } async removeReaction(messageId, reactionId) { assertApiSuccess("Feishu reaction.delete", await this.#client.im.v1.messageReaction.delete({ path: { message_id: messageId, reaction_id: reactionId } })); } }; // src/channels/feishu/feishu-runtime.mjs function createBridgeStatus({ allowedSenderCount = 1 } = {}) { return { startedAt: null, ready: false, feishuLongConnectionState: "idle", harnessReachable: false, messagesReceived: 0, messagesReplied: 0, messagesRejected: 0, reactionsAdded: 0, reactionsRemoved: 0, reactionErrors: 0, streamResponses: 0, streamUpdates: 0, streamFallbacks: 0, streamErrors: 0, lastMessageAt: null, lastReplyAt: null, lastRejectedAt: null, lastError: null, agentPreset: "standard", authorizationMode: "sender-open-id-allowlist", allowedSenderCount }; } var FeishuRuntime = class { #lark; #appId; #appSecret; #domain; #ownerOpenIds; #harness; #state; #replyTimeoutMs; #connectTimeoutMs; #logger; #client = null; #bridge = null; #wsClient = null; #starting = null; #status; constructor({ lark, appId, appSecret, domain = "feishu", ownerOpenId, ownerOpenIds, harness, state, replyTimeoutMs = 6e5, connectTimeoutMs = 15e3, logger = console }) { if (!lark) throw new Error("FeishuRuntime requires the Feishu SDK"); if (!appId || !appSecret) throw new Error("FeishuRuntime requires app credentials"); const allowedOwners = Array.isArray(ownerOpenIds) ? ownerOpenIds : [ownerOpenId]; const normalizedOwners = [...new Set(allowedOwners.filter((value) => typeof value === "string" && value))]; if (normalizedOwners.length === 0) throw new Error("FeishuRuntime requires at least one owner open_id"); if (!harness) throw new Error("FeishuRuntime requires a Harness client"); if (!state) throw new Error("FeishuRuntime requires a state store"); this.#lark = lark; this.#appId = appId; this.#appSecret = appSecret; this.#domain = domain; this.#ownerOpenIds = normalizedOwners; this.#harness = harness; this.#state = state; this.#replyTimeoutMs = replyTimeoutMs; this.#connectTimeoutMs = connectTimeoutMs; this.#logger = logger; this.#status = createBridgeStatus({ allowedSenderCount: normalizedOwners.length }); } get status() { return structuredClone(this.#status); } async start() { if (this.#wsClient && this.#status.ready) return this.status; if (this.#starting) return this.#starting; this.#starting = this.#start().finally(() => { this.#starting = null; }); return this.#starting; } async #start() { this.#status.startedAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.feishuLongConnectionState = "connecting"; this.#status.lastError = null; try { await this.#harness.ensureRunning(); this.#status.harnessReachable = true; const sdkDomain = this.#domain === "lark" ? this.#lark.Domain.Lark : this.#lark.Domain.Feishu; const larkConfig = { appId: this.#appId, appSecret: this.#appSecret, domain: sdkDomain }; this.#client = new this.#lark.Client(larkConfig); const channel = new VerifiedFeishuChannel({ client: this.#client, initialText: "\u5DF2\u8FDE\u63A5 DeepSeek Harness\uFF0C\u6B63\u5728\u601D\u8003\u2026" }); this.#bridge = new FeishuHarnessBridge({ client: this.#client, channel, harness: this.#harness, state: this.#state, status: this.#status, allowedSenderOpenIds: new Set(this.#ownerOpenIds), replyTimeoutMs: this.#replyTimeoutMs }); const dispatcher = new this.#lark.EventDispatcher({}).register({ "im.message.receive_v1": (event) => { this.#bridge.accept(event); return {}; }, "im.message.reaction.created_v1": () => ({}), "im.message.reaction.deleted_v1": () => ({}) }); let settleReady; let settleError; const ready = new Promise((resolve7, reject) => { let settled = false; const timer = setTimeout(() => { if (settled) return; settled = true; reject(new Error(`Feishu WebSocket handshake timed out after ${this.#connectTimeoutMs}ms`)); }, this.#connectTimeoutMs); settleReady = () => { if (settled) return; settled = true; clearTimeout(timer); resolve7(); }; settleError = (error) => { if (settled) return; settled = true; clearTimeout(timer); reject(error); }; }); this.#wsClient = new this.#lark.WSClient({ ...larkConfig, loggerLevel: this.#lark.LoggerLevel.info, handshakeTimeoutMs: 15e3, onReady: () => { this.#status.feishuLongConnectionState = "connected"; this.#status.ready = true; this.#status.lastError = null; settleReady(); }, onError: (error) => { this.#status.feishuLongConnectionState = "failed"; this.#status.ready = false; this.#status.lastError = error?.message ?? String(error); this.#logger.error("[dsh-feishu] Feishu long connection failed:", this.#status.lastError); settleError(error); }, onReconnecting: () => { this.#status.feishuLongConnectionState = "reconnecting"; this.#status.ready = false; }, onReconnected: () => { this.#status.feishuLongConnectionState = "connected"; this.#status.ready = true; this.#status.lastError = null; } }); await this.#wsClient.start({ eventDispatcher: dispatcher }).catch((error) => { settleError(error); }); await ready; return this.status; } catch (error) { this.#status.ready = false; this.#status.feishuLongConnectionState = "failed"; this.#status.lastError = error?.message ?? String(error); await this.stop({ preserveError: true }); throw error; } } async stop({ preserveError = false } = {}) { const error = preserveError ? this.#status.lastError : null; this.#status.ready = false; if (this.#wsClient) { this.#wsClient.close({ force: true }); this.#wsClient = null; } if (this.#bridge) { await this.#bridge.waitForIdle(); this.#bridge = null; } this.#client = null; this.#status.feishuLongConnectionState = preserveError ? "failed" : "idle"; this.#status.lastError = error; return this.status; } }; // src/channels/feishu/harness-client.mjs import { spawn as spawn3 } from "node:child_process"; import { randomUUID as randomUUID7 } from "node:crypto"; var sleep3 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)); function messageText(event) { return (event?.data?.message?.content ?? []).filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n").trim(); } var HarnessReplyTracker3 = class { #promptRpcId; #lastSeq; #openTurn = null; #targetTurn = null; #stepText = /* @__PURE__ */ new Map(); #latestText = ""; #finished = false; #reason = null; constructor({ promptRpcId, afterSeq = -1 }) { this.#promptRpcId = promptRpcId; this.#lastSeq = afterSeq; } get finished() { return this.#finished; } get answer() { return this.#latestText.trim(); } get reason() { return this.#reason; } consume(entries) { let update = null; const ordered = [...entries].map((entry) => entry?.event ?? entry).filter(Boolean).sort((left, right) => (left.seq ?? -1) - (right.seq ?? -1)); for (const event of ordered) { const seq = event.seq ?? -1; if (seq <= this.#lastSeq) continue; this.#lastSeq = seq; if (event.type === "turn/start") { this.#openTurn = event.data?.turn ?? null; } if (event.type === "user/message" && event.data?.source?.rpcId === this.#promptRpcId) { this.#targetTurn = this.#openTurn; continue; } if (this.#targetTurn === null) continue; if (event.type === "turn/end") { if (event.data?.turn !== this.#targetTurn) continue; this.#finished = true; this.#reason = event.data?.reason ?? null; this.#openTurn = null; continue; } if (event.data?.turn !== this.#targetTurn) continue; if (event.type === "assistant/chunk" && event.data?.chunk?.type === "text-delta") { const step = event.data?.step ?? 0; const index = event.data.chunk.index ?? 0; const key = `${step}:${index}`; this.#stepText.set(key, (this.#stepText.get(key) ?? "") + event.data.chunk.text); const stepPrefix = `${step}:`; const text = [...this.#stepText.entries()].filter(([partKey]) => partKey.startsWith(stepPrefix)).sort(([left], [right]) => Number(left.split(":")[1]) - Number(right.split(":")[1])).map(([, part]) => part).join("\n").trim(); if (text && text !== this.#latestText) { this.#latestText = text; update = { type: "text", text }; } continue; } if (event.type === "assistant/message") { const text = messageText(event); if (text && text !== this.#latestText) { this.#latestText = text; update = { type: "text", text }; } continue; } if (event.type === "tool/call") { update = { type: "tool", name: event.data?.name ?? "\u5DE5\u5177" }; } else if (event.type === "tool/result") { update = { type: "status", text: "\u6B63\u5728\u6574\u7406\u7ED3\u679C\u2026" }; } } return update; } }; var HarnessRpcError3 = class extends Error { constructor(method, error) { super(`${method}: ${error?.message ?? "unknown Harness RPC error"}`); this.name = "HarnessRpcError"; this.method = method; this.code = error?.code ?? "internal"; this.details = error?.details ?? {}; } }; var HarnessClient3 = class { #baseUrl; #workspace; #agentPreset; #autostart; #dshBin; #managedProcess = null; constructor({ baseUrl, workspace, agentPreset, autostart, dshBin }) { this.#baseUrl = new URL(baseUrl); this.#workspace = workspace; this.#agentPreset = agentPreset; this.#autostart = autostart; this.#dshBin = dshBin; } async rpc(method, payload = {}, timeoutMs = 3e4, options = {}) { const rpcId = options.rpcId ?? `feishu-${randomUUID7()}`; const response = await fetch(new URL(`/api/${method}`, this.#baseUrl), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ type: "client-request", rpcId, method, payload }), signal: AbortSignal.timeout(timeoutMs) }); if (!response.ok) throw new Error(`Harness transport ${method} failed: HTTP ${response.status}`); const body = await response.json(); if (body?.type !== "server-response" || body?.rpcId !== rpcId) { throw new Error(`Harness returned an invalid response for ${method}`); } if (!body.result?.ok) throw new HarnessRpcError3(method, body.result?.error); return body.result.value; } async health() { await this.rpc("host.describe", {}, 5e3); return true; } async ensureRunning() { try { return await this.health(); } catch (firstError) { if (!this.#autostart) throw firstError; } if (!this.#managedProcess || this.#managedProcess.exitCode !== null) { const port = this.#baseUrl.port || (this.#baseUrl.protocol === "https:" ? "443" : "80"); this.#managedProcess = spawn3(this.#dshBin, [ "web", "--host", this.#baseUrl.hostname, "--port", port ], { cwd: this.#workspace, env: process.env, stdio: ["ignore", "inherit", "inherit"] }); this.#managedProcess.on("error", (error) => { console.error("[bridge] failed to start Harness:", error.message); }); } const deadline = Date.now() + 6e4; let lastError; while (Date.now() < deadline) { await sleep3(1e3); try { return await this.health(); } catch (error) { lastError = error; } } throw new Error(`Harness did not become ready: ${lastError?.message ?? "timeout"}`); } async workspaceId() { const { items } = await this.rpc("workspace.list", {}); const existing = items.find((item) => item.path === this.#workspace); if (existing) return existing.workspaceId; const created = await this.rpc("workspace.create", { path: this.#workspace }); return created.workspace.workspaceId; } async createSession() { await this.ensureRunning(); const workspaceId = await this.workspaceId(); const created = await this.rpc("session.create", { workspaceId, agentPreset: this.#agentPreset }); return created.sessionId; } async sessionExists(sessionId) { try { await this.rpc("session.history", { sessionId, maxMessages: 1 }); return true; } catch (error) { if (error instanceof HarnessRpcError3 && error.code === "session-not-found") return false; throw error; } } async ask(sessionId, text, options = {}) { if (typeof options === "number") options = { timeoutMs: options }; const timeoutMs = options.timeoutMs ?? 6e5; const onUpdate = typeof options.onUpdate === "function" ? options.onUpdate : null; await this.ensureRunning(); const before = await this.rpc("session.history", { sessionId, maxMessages: 1 }); const baselineSeq = Math.max(-1, ...(before.events ?? []).map(({ event }) => event.seq ?? -1)); const promptRpcId = `feishu-${randomUUID7()}`; const tracker = new HarnessReplyTracker3({ promptRpcId, afterSeq: baselineSeq }); await this.rpc("session.prompt", { sessionId, mode: "queue", content: [{ type: "text", text }], clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }, 3e4, { rpcId: promptRpcId }); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { await sleep3(300); const history = await this.rpc("session.history", { sessionId, maxMessages: 50 }); const update = tracker.consume(history.events ?? []); if (update && onUpdate) { try { await onUpdate(update); } catch (error) { console.warn("[bridge] ignored a progress update failure:", error.message); } } if (!tracker.finished) continue; if (tracker.answer) return tracker.answer; throw new Error(`Harness turn ended without a text reply${tracker.reason ? ` (${JSON.stringify(tracker.reason)})` : ""}`); } throw new Error(`Harness reply timed out after ${Math.round(timeoutMs / 1e3)} seconds`); } stopManagedProcess() { if (this.#managedProcess?.exitCode === null) this.#managedProcess.kill("SIGTERM"); } }; // src/channels/feishu/plugin-config-store.mjs import { createHash as createHash3 } from "node:crypto"; import { mkdir as mkdir5, readFile as readFile5, rename as rename5, unlink as unlink7, writeFile as writeFile5 } from "node:fs/promises"; import { dirname as dirname5 } from "node:path"; var LEGACY_FEISHU_SECRET_REF = "DSH_FEISHU_APP_SECRET"; function cleanString7(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function safeId(value) { const id = cleanString7(value); return id && /^[A-Za-z0-9_-]{1,128}$/.test(id) ? id : null; } function legacyBotId(appId) { return `bot_${createHash3("sha256").update(appId).digest("hex").slice(0, 24)}`; } function normalizeOwners(value) { const candidates = Array.isArray(value.ownerOpenIds) ? value.ownerOpenIds : [value.ownerOpenId]; return [...new Set(candidates.map(cleanString7).filter(Boolean))]; } function normalizeBot2(value, { legacy = false } = {}) { if (!value || typeof value !== "object") return null; const appId = cleanString7(value.appId); const ownerOpenIds = normalizeOwners(value); if (!appId || ownerOpenIds.length === 0) return null; const id = safeId(value.id) ?? (legacy ? legacyBotId(appId) : null); const secretRef = cleanString7(value.secretRef) ?? (legacy ? LEGACY_FEISHU_SECRET_REF : null); if (!id || !secretRef) return null; if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(secretRef)) return null; const domain = value.domain === "lark" ? "lark" : "feishu"; return Object.freeze({ id, appId, secretRef, ownerOpenIds: Object.freeze(ownerOpenIds), domain, botName: cleanString7(value.botName), botOpenId: cleanString7(value.botOpenId), activated: value.activated ?? null, deletionPending: value.deletionPending === true, connectedAt: cleanString7(value.connectedAt), createdAt: cleanString7(value.createdAt) ?? cleanString7(value.connectedAt) }); } function normalizeDocument2(value) { if (!value || typeof value !== "object") return null; if (value.version === 2 && Array.isArray(value.bots)) { const bots = value.bots.map((bot) => normalizeBot2(bot)); if (bots.some((bot) => bot === null)) { throw new Error("dsh-feishu config contains an invalid bot entry"); } const ids = /* @__PURE__ */ new Set(); const refs = /* @__PURE__ */ new Set(); const appIds = /* @__PURE__ */ new Set(); for (const bot of bots) { if (ids.has(bot.id) || refs.has(bot.secretRef) || appIds.has(bot.appId)) { throw new Error("dsh-feishu config contains duplicate bot identities"); } ids.add(bot.id); refs.add(bot.secretRef); appIds.add(bot.appId); } return { value: Object.freeze({ version: 2, bots: Object.freeze(bots) }), migrated: false }; } const legacyBot = normalizeBot2(value, { legacy: true }); if (!legacyBot) return null; return { value: Object.freeze({ version: 2, bots: Object.freeze([legacyBot]) }), migrated: true }; } var PluginConfigStore = class { #path; #value = Object.freeze({ version: 2, bots: Object.freeze([]) }); #writeQueue = Promise.resolve(); constructor(path) { this.#path = path; } async load() { try { const parsed = JSON.parse(await readFile5(this.#path, "utf8")); const normalized = normalizeDocument2(parsed); if (!normalized) throw new Error("dsh-feishu config is incomplete or invalid"); this.#value = normalized.value; if (normalized.migrated) await this.#writeDocument(this.#value); } catch (error) { if (error?.code !== "ENOENT") throw error; this.#value = Object.freeze({ version: 2, bots: Object.freeze([]) }); } return this; } /** Backward-compatible single-bot view used by the original controller. */ get() { const bot = this.#value.bots[0]; if (!bot) return null; const result = structuredClone(bot); result.ownerOpenId = result.ownerOpenIds[0]; return result; } list() { return structuredClone(this.#value.bots); } getBot(id) { const bot = this.#value.bots.find((candidate) => candidate.id === id); return bot ? structuredClone(bot) : null; } /** Backward-compatible save replaces the original single-bot view. */ async save(value) { const fallback = { ...value }; if (!fallback.id) fallback.id = legacyBotId(cleanString7(fallback.appId) ?? "invalid"); if (!fallback.secretRef) fallback.secretRef = LEGACY_FEISHU_SECRET_REF; const normalized = normalizeBot2(fallback); if (!normalized) throw new Error("Refusing to persist incomplete dsh-feishu configuration"); await this.#replaceBots([normalized]); return this.get(); } async saveBot(value) { const normalized = normalizeBot2(value); if (!normalized) throw new Error("Refusing to persist incomplete dsh-feishu bot configuration"); return this.#mutate((bots) => { const collision = bots.find((bot) => bot.secretRef === normalized.secretRef && bot.id !== normalized.id); if (collision) throw new Error("Refusing to share a credential reference between Feishu bots"); const appCollision = bots.find((bot) => bot.appId === normalized.appId && bot.id !== normalized.id); if (appCollision) throw new Error("Refusing to persist the same Feishu app twice"); const index = bots.findIndex((bot) => bot.id === normalized.id); if (index === -1) bots.push(normalized); else bots[index] = normalized; return structuredClone(normalized); }); } async removeBot(id) { if (!safeId(id)) throw new TypeError("Invalid Feishu bot id"); return this.#mutate((bots) => { const index = bots.findIndex((bot) => bot.id === id); if (index === -1) return null; const [removed] = bots.splice(index, 1); return structuredClone(removed); }); } async clear() { const operation = this.#writeQueue.then(async () => { try { await unlink7(this.#path); } catch (error) { if (error?.code !== "ENOENT") throw error; } this.#value = Object.freeze({ version: 2, bots: Object.freeze([]) }); }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; } async #replaceBots(bots) { const document = Object.freeze({ version: 2, bots: Object.freeze([...bots]) }); const operation = this.#writeQueue.then(async () => { await this.#writeDocument(document); this.#value = document; }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; } async #mutate(mutator) { let result; const operation = this.#writeQueue.then(async () => { const bots = [...this.#value.bots]; result = mutator(bots); const document = Object.freeze({ version: 2, bots: Object.freeze(bots) }); await this.#writeDocument(document); this.#value = document; }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; return result; } async #writeDocument(document) { await mkdir5(dirname5(this.#path), { recursive: true, mode: 448 }); const temporary = `${this.#path}.tmp`; await writeFile5(temporary, `${JSON.stringify(document, null, 2)} `, { encoding: "utf8", mode: 384 }); await rename5(temporary, this.#path); } }; // src/channels/feishu/multi-bot-controller.mjs import { randomUUID as randomUUID8 } from "node:crypto"; // src/channels/feishu/registration-manager.mjs var ACTIVE_STATES = /* @__PURE__ */ new Set([ "starting", "qr_ready", "polling", "slow_down", "domain_switched", "saving" ]); var SDK_POLLING_STATES = /* @__PURE__ */ new Set([ "polling", "slow_down", "domain_switched" ]); var REGISTRATION_STATES = Object.freeze({ IDLE: "idle", STARTING: "starting", QR_READY: "qr_ready", POLLING: "polling", SLOW_DOWN: "slow_down", DOMAIN_SWITCHED: "domain_switched", SAVING: "saving", SUCCEEDED: "succeeded", EXPIRED: "expired", CANCELLED: "cancelled", ERROR: "error" }); function errorCode(error) { if (["access_denied", "expired_token", "abort"].includes(error?.code)) return error.code; return "registration_failed"; } function publicError(error) { const code = errorCode(error); const messages = { access_denied: "Registration was denied.", abort: "Registration was cancelled.", expired_token: "The registration QR code expired." }; return { code, message: messages[code] ?? "Unable to register the Feishu app." }; } function expirySeconds(value) { const seconds = Number(value); if (!Number.isFinite(seconds) || seconds <= 0) { throw new TypeError("registerApp onQRCodeReady returned an invalid expireIn"); } return seconds; } function copyUserInfo(userInfo) { if (userInfo === void 0) return void 0; if (userInfo === null || typeof userInfo !== "object" || Array.isArray(userInfo)) { throw new TypeError("registerApp returned invalid user_info"); } return { ...userInfo }; } var RegistrationManager = class { #registerApp; #onCredentials; #now; #setTimeout; #clearTimeout; #attempt = 0; #active = null; #snapshot; constructor({ registerApp, onCredentials, now = Date.now, setTimeout: setTimeoutFn = globalThis.setTimeout, clearTimeout: clearTimeoutFn = globalThis.clearTimeout } = {}) { if (typeof registerApp !== "function") { throw new TypeError("RegistrationManager requires a registerApp function"); } if (typeof onCredentials !== "function") { throw new TypeError("RegistrationManager requires an onCredentials function"); } if (typeof now !== "function" || typeof setTimeoutFn !== "function" || typeof clearTimeoutFn !== "function") { throw new TypeError("RegistrationManager clock dependencies must be functions"); } this.#registerApp = registerApp; this.#onCredentials = onCredentials; this.#now = now; this.#setTimeout = setTimeoutFn; this.#clearTimeout = clearTimeoutFn; this.#snapshot = this.#makeSnapshot(null, REGISTRATION_STATES.IDLE); } start(registerOptions = {}) { if (registerOptions === null || typeof registerOptions !== "object" || Array.isArray(registerOptions)) { throw new TypeError("Registration options must be an object"); } this.#supersedeActiveAttempt(); const run = { id: ++this.#attempt, controller: new AbortController(), qrCodeUrl: null, expiresAt: null, pollIntervalSeconds: null, expiryTimer: null }; this.#active = run; this.#snapshot = this.#makeSnapshot(run, REGISTRATION_STATES.STARTING); const options = { ...registerOptions, signal: run.controller.signal, onQRCodeReady: (info) => this.#onQRCodeReady(run, info), onStatusChange: (info) => this.#onStatusChange(run, info) }; const registration = Promise.resolve().then(() => this.#registerApp(options)); void registration.then( (result) => this.#onRegistrationSucceeded(run, result), (error) => this.#onRegistrationFailed(run, error) ); return this.status(); } status() { this.#expireIfNeeded(); const snapshot = { ...this.#snapshot }; if (snapshot.error) snapshot.error = { ...snapshot.error }; const run = this.#active; if (run && run.expiresAt !== null && ACTIVE_STATES.has(snapshot.state)) { snapshot.remainingSeconds = Math.max(0, Math.ceil((run.expiresAt - this.#now()) / 1e3)); } return snapshot; } cancel() { const run = this.#active; if (!run) return this.status(); this.#finishRun(run, REGISTRATION_STATES.CANCELLED, { error: { code: "abort", message: "Registration was cancelled." } }); run.controller.abort(); return this.status(); } #isCurrent(run) { return this.#active === run; } #makeSnapshot(run, state, extra = {}) { const snapshot = { state, attempt: run?.id ?? this.#attempt, updatedAt: this.#now(), ...extra }; if (run?.qrCodeUrl && ACTIVE_STATES.has(state)) { snapshot.qrCodeUrl = run.qrCodeUrl; snapshot.expiresAt = run.expiresAt; } if (run?.pollIntervalSeconds !== null && ACTIVE_STATES.has(state)) { snapshot.pollIntervalSeconds = run.pollIntervalSeconds; } return snapshot; } #setRunState(run, state, extra = {}) { if (!this.#isCurrent(run)) return; this.#snapshot = this.#makeSnapshot(run, state, extra); } #onQRCodeReady(run, info) { if (!this.#isCurrent(run)) return; if (typeof info?.url !== "string" || !info.url) { throw new TypeError("registerApp onQRCodeReady returned an invalid URL"); } const seconds = expirySeconds(info.expireIn); run.qrCodeUrl = info.url; run.expiresAt = this.#now() + seconds * 1e3; this.#clearExpiryTimer(run); run.expiryTimer = this.#setTimeout(() => this.#expireRun(run), seconds * 1e3); run.expiryTimer?.unref?.(); this.#setRunState(run, REGISTRATION_STATES.QR_READY); } #onStatusChange(run, info) { if (!this.#isCurrent(run) || !SDK_POLLING_STATES.has(info?.status)) return; if (info.status === REGISTRATION_STATES.SLOW_DOWN && Number.isFinite(Number(info.interval))) { run.pollIntervalSeconds = Number(info.interval); } this.#setRunState(run, info.status); } async #onRegistrationSucceeded(run, result) { if (!this.#isCurrent(run)) return; const clientId = result?.client_id; const clientSecret = result?.client_secret; if (typeof clientId !== "string" || !clientId || typeof clientSecret !== "string" || !clientSecret) { this.#finishRun(run, REGISTRATION_STATES.ERROR, { error: { code: "invalid_credentials", message: "Feishu registration returned invalid credentials." } }); return; } let userInfo; try { userInfo = copyUserInfo(result.user_info); } catch { this.#finishRun(run, REGISTRATION_STATES.ERROR, { error: { code: "invalid_credentials", message: "Feishu registration returned invalid credentials." } }); return; } this.#clearExpiryTimer(run); run.qrCodeUrl = null; run.expiresAt = null; run.pollIntervalSeconds = null; this.#setRunState(run, REGISTRATION_STATES.SAVING); try { await this.#onCredentials({ client_id: clientId, client_secret: clientSecret, user_info: userInfo }); } catch { if (this.#isCurrent(run)) { this.#finishRun(run, REGISTRATION_STATES.ERROR, { error: { code: "credentials_callback_failed", message: "Unable to store the Feishu credentials." } }); } return; } if (this.#isCurrent(run)) { this.#finishRun(run, REGISTRATION_STATES.SUCCEEDED); } } #onRegistrationFailed(run, error) { if (!this.#isCurrent(run)) return; const code = errorCode(error); if (code === "expired_token") { this.#finishRun(run, REGISTRATION_STATES.EXPIRED, { error: publicError(error) }); return; } if (code === "abort") { this.#finishRun(run, REGISTRATION_STATES.CANCELLED, { error: publicError(error) }); return; } this.#finishRun(run, REGISTRATION_STATES.ERROR, { error: publicError(error) }); } #expireIfNeeded() { const run = this.#active; if (run && run.expiresAt !== null && this.#now() >= run.expiresAt) { this.#expireRun(run); } } #expireRun(run) { if (!this.#isCurrent(run)) return; this.#finishRun(run, REGISTRATION_STATES.EXPIRED, { error: { code: "expired_token", message: "The registration QR code expired." } }); run.controller.abort(); } #finishRun(run, state, extra = {}) { if (!this.#isCurrent(run)) return; this.#clearExpiryTimer(run); this.#snapshot = this.#makeSnapshot(run, state, extra); this.#active = null; } #clearExpiryTimer(run) { if (run.expiryTimer !== null) { this.#clearTimeout(run.expiryTimer); run.expiryTimer = null; } } #supersedeActiveAttempt() { const previous = this.#active; if (!previous) return; this.#clearExpiryTimer(previous); this.#active = null; previous.controller.abort(); } }; // src/channels/feishu/plugin-controller.mjs var REQUIRED_TENANT_SCOPES = Object.freeze([ "im:message.p2p_msg:readonly", "im:message.group_at_msg:readonly", "im:message:send_as_bot", "im:message.reactions:write_only", "im:message:recall", "cardkit:card:write" ]); // src/channels/feishu/multi-bot-controller.mjs var ACTIVE_REGISTRATION_STATES2 = /* @__PURE__ */ new Set([ "starting", "qr_ready", "polling", "slow_down", "domain_switched" ]); var MUTABLE_REGISTRATION_STATES = /* @__PURE__ */ new Set([...ACTIVE_REGISTRATION_STATES2, "saving"]); var ALL_VISIBLE_SENDERS = "*"; function idleConnection() { return { ready: false, feishuLongConnectionState: "idle", harnessReachable: false }; } function connectionStatus(runtime) { return runtime ? runtime.status : idleConnection(); } function isConnected2(connection) { return connection.ready === true && connection.feishuLongConnectionState === "connected" && connection.harnessReachable === true; } function maskedAppId(appId) { return appId.length > 12 ? `${appId.slice(0, 8)}\u2022\u2022\u2022\u2022${appId.slice(-4)}` : "cli_\u2022\u2022\u2022\u2022"; } function publicBot(config) { return { name: config.botName, appIdMasked: maskedAppId(config.appId), activated: config.activated, domain: config.domain }; } function botPhase({ connected, error, connection }) { if (connected) return "connected"; if (error || connection.feishuLongConnectionState === "failed") return "error"; return "disconnected"; } function makeBotId() { return `bot_${randomUUID8().replaceAll("-", "")}`; } function makeRegistrationId() { return `reg_${randomUUID8().replaceAll("-", "")}`; } function secretRefFor(botId) { return `DSH_FEISHU_APP_SECRET_${botId.slice(4).toUpperCase()}`; } var MultiBotDshFeishuController = class { #registerApp; #verifyApp; #credentials; #configStore; #createRuntime; #deleteState; #createBotId; #createRegistrationId; #runtimes = /* @__PURE__ */ new Map(); #botErrors = /* @__PURE__ */ new Map(); #registrations = /* @__PURE__ */ new Map(); #botOwnership = /* @__PURE__ */ new Map(); #latestRegistrationId = null; #configTransition = Promise.resolve(); #botTransitions = /* @__PURE__ */ new Map(); #revision = 1; #closed = false; constructor({ registerApp, verifyApp, credentials, configStore, createRuntime, deleteState = async () => { }, createBotId = makeBotId, createRegistrationId = makeRegistrationId }) { if (typeof registerApp !== "function") throw new Error("registerApp is required"); if (typeof verifyApp !== "function") throw new Error("verifyApp is required"); if (!credentials) throw new Error("credentials service is required"); if (!configStore || typeof configStore.list !== "function") { throw new Error("multi-bot config store is required"); } if (typeof createRuntime !== "function") throw new Error("createRuntime is required"); if (typeof deleteState !== "function") throw new Error("deleteState must be a function"); this.#registerApp = registerApp; this.#verifyApp = verifyApp; this.#credentials = credentials; this.#configStore = configStore; this.#createRuntime = createRuntime; this.#deleteState = deleteState; this.#createBotId = createBotId; this.#createRegistrationId = createRegistrationId; } async initialize() { if (this.#closed) return this.status(); const bots = this.#configStore.list(); let attempted = false; await Promise.allSettled(bots.map((config) => this.#withBotTransition(config.id, async () => { const current = connectionStatus(this.#runtimes.get(config.id)); if (isConnected2(current) || current.feishuLongConnectionState === "connecting" || current.feishuLongConnectionState === "reconnecting") { return; } attempted = true; if (config.deletionPending) { this.#botErrors.set(config.id, { code: "deletion_pending", message: "\u673A\u5668\u4EBA\u6B63\u5728\u7B49\u5F85\u5B8C\u6210\u672C\u5730\u5220\u9664\uFF0C\u8BF7\u91CD\u8BD5\u79FB\u9664\u3002" }); return; } let resolved; try { resolved = await this.#credentials.resolve(config.secretRef); } catch { this.#botErrors.set(config.id, { code: "missing_credentials", message: "\u65E0\u6CD5\u8BFB\u53D6\u673A\u5668\u4EBA\u51ED\u636E\uFF0C\u8BF7\u68C0\u67E5\u51ED\u636E\u5B58\u50A8\u3002" }); return; } if (!resolved?.value) { this.#botErrors.set(config.id, { code: "missing_credentials", message: "\u673A\u5668\u4EBA\u51ED\u636E\u7F3A\u5931\uFF0C\u8BF7\u5220\u9664\u540E\u91CD\u65B0\u626B\u7801\u63A5\u5165\u3002" }); return; } try { await this.#startRuntime(config, resolved.value); this.#botErrors.delete(config.id); } catch { this.#botErrors.set(config.id, { code: "connection_failed", message: "\u673A\u5668\u4EBA\u6682\u65F6\u65E0\u6CD5\u8FDE\u63A5\u98DE\u4E66\uFF0C\u8BF7\u91CD\u8BD5\u3002" }); } }))); if (attempted) this.#touch(); return this.status(); } startRegistration() { this.#assertOpen(); const id = this.#createRegistrationId(); if (typeof id !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(id) || this.#registrations.has(id)) { throw new Error("Registration id generator returned an invalid or duplicate id"); } const record = { id, manager: null, botId: null, createdNew: false, cancelled: false }; record.manager = new RegistrationManager({ registerApp: this.#registerApp, onCredentials: (result) => this.#serializeConfig(() => this.#acceptCredentials(record, result)) }); this.#registrations.set(id, record); this.#latestRegistrationId = id; this.#trimRegistrations(); record.manager.start({ source: "deepseek-harness", createOnly: true, appPreset: { name: "{user} \u7684\u5317\u6C47\u661F\u6CB3 AI \u52A9\u624B", desc: "\u8FDE\u63A5\u98DE\u4E66\u4E0E DeepSeek Harness\uFF0C\u5728\u804A\u5929\u4E2D\u4F7F\u7528\u4F01\u4E1A AI \u52A9\u624B\u3002" }, addons: { preset: false, scopes: { tenant: [...REQUIRED_TENANT_SCOPES] }, events: { items: { tenant: ["im.message.receive_v1"] } } } }); this.#touch(); return this.registrationStatus(id); } hasRegistration(attemptId) { return this.#registrations.has(attemptId); } registrationStatus(attemptId) { const record = this.#registrations.get(attemptId); if (!record) return null; return this.#status({ registration: record, selectedBotId: record.botId }); } async cancelRegistration(attemptId = this.#latestRegistrationId) { const record = this.#registrations.get(attemptId); if (!record) return this.status(); if (!MUTABLE_REGISTRATION_STATES.has(record.manager.status().state)) { return this.registrationStatus(attemptId); } record.cancelled = true; record.manager.cancel(); await this.#serializeConfig(async () => { if (record.createdNew && record.botId && this.#botOwnership.get(record.botId) === record.id && this.#configStore.getBot(record.botId)) { await this.#withBotTransition(record.botId, () => this.#deleteBot(record.botId)); } }); this.#touch(); return this.registrationStatus(attemptId) ?? this.status(); } status(botId) { return this.#status({ registration: this.#registrations.get(this.#latestRegistrationId) ?? null, selectedBotId: botId }); } async bindCredentials({ appId, appSecret, domain = "feishu" } = {}) { this.#assertOpen(); const normalizedAppId = typeof appId === "string" ? appId.trim() : ""; const normalizedSecret = typeof appSecret === "string" ? appSecret.trim() : ""; const normalizedDomain = domain === "lark" ? "lark" : "feishu"; if (!normalizedAppId || !normalizedSecret) { throw new TypeError("Feishu App ID and App Secret are required"); } return this.#serializeConfig(async () => { this.#assertOpen(); const bot = await this.#verifyApp({ appId: normalizedAppId, appSecret: normalizedSecret, domain: normalizedDomain }); this.#assertOpen(); const existing = this.#configStore.list().find( (candidate) => candidate.appId === normalizedAppId ); const botId = existing?.id ?? this.#createBotId(); if (typeof botId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(botId) || !existing && this.#configStore.getBot(botId)) { throw new Error("Bot id generator returned an invalid or duplicate id"); } const secretRef = existing?.secretRef ?? secretRefFor(botId); const previousSecret = await this.#credentials.resolve(secretRef).catch(() => void 0); const config = { ...existing, id: botId, appId: normalizedAppId, secretRef, ownerOpenIds: existing?.ownerOpenIds?.length ? existing.ownerOpenIds : [ALL_VISIBLE_SENDERS], domain: normalizedDomain, botName: bot.name, botOpenId: bot.openId, activated: bot.activated, deletionPending: false, connectedAt: (/* @__PURE__ */ new Date()).toISOString(), createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString() }; await this.#credentials.set(secretRef, normalizedSecret); let saved; try { saved = await this.#configStore.saveBot(config); } catch (error) { await this.#restoreCredential(secretRef, previousSecret); throw error; } await this.#withBotTransition(botId, async () => { try { await this.#startRuntime(saved, normalizedSecret); this.#botErrors.delete(botId); } catch { this.#botErrors.set(botId, { code: "connection_failed", message: "\u673A\u5668\u4EBA\u5DF2\u7ECF\u7ED1\u5B9A\uFF0C\u4F46\u957F\u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u8BF7\u70B9\u51FB\u91CD\u8BD5\u3002" }); } }); this.#touch(); return this.status(botId); }); } async reconnectBot(botId) { this.#assertOpen(); return this.#withBotTransition(botId, async () => { const config = this.#requireBot(botId); if (config.deletionPending) { this.#botErrors.set(botId, { code: "deletion_pending", message: "\u673A\u5668\u4EBA\u6B63\u5728\u7B49\u5F85\u5B8C\u6210\u672C\u5730\u5220\u9664\uFF0C\u8BF7\u91CD\u8BD5\u79FB\u9664\u3002" }); return this.status(botId); } if (isConnected2(connectionStatus(this.#runtimes.get(botId)))) { return this.status(botId); } let resolved; try { resolved = await this.#credentials.resolve(config.secretRef); } catch { resolved = null; } if (!resolved?.value) { this.#botErrors.set(botId, { code: "missing_credentials", message: "\u673A\u5668\u4EBA\u51ED\u636E\u7F3A\u5931\uFF0C\u8BF7\u5220\u9664\u540E\u91CD\u65B0\u626B\u7801\u63A5\u5165\u3002" }); this.#touch(); return this.status(botId); } try { await this.#startRuntime(config, resolved.value); this.#botErrors.delete(botId); } catch { this.#botErrors.set(botId, { code: "connection_failed", message: "\u673A\u5668\u4EBA\u6682\u65F6\u65E0\u6CD5\u8FDE\u63A5\u98DE\u4E66\uFF0C\u8BF7\u91CD\u8BD5\u3002" }); } this.#touch(); return this.status(botId); }); } async disconnectBot(botId) { this.#assertOpen(); return this.#withBotTransition(botId, async () => { this.#requireBot(botId); await this.#stopRuntime(botId); this.#botErrors.delete(botId); this.#touch(); return this.status(botId); }); } async deleteBot(botId) { this.#assertOpen(); return this.#serializeConfig(() => this.#withBotTransition(botId, async () => { this.#requireBot(botId); await this.#deleteBot(botId); this.#touch(); return this.status(); })); } // Compatibility methods for the original one-bot browser contract. async reconnect() { const bot = this.#configStore.list()[0]; return bot ? this.reconnectBot(bot.id) : this.status(); } async disconnect() { const bot = this.#configStore.list()[0]; return bot ? this.deleteBot(bot.id) : this.status(); } async close() { if (this.#closed) return; this.#closed = true; for (const record of this.#registrations.values()) { if (MUTABLE_REGISTRATION_STATES.has(record.manager.status().state)) { record.cancelled = true; record.manager.cancel(); } } await this.#configTransition; await Promise.allSettled([...this.#botTransitions.values()]); await Promise.allSettled([...this.#runtimes.keys()].map((id) => this.#stopRuntime(id))); } #status({ registration, selectedBotId } = {}) { const bots = this.#configStore.list().map((config) => { const connection = connectionStatus(this.#runtimes.get(config.id)); const connected = isConnected2(connection); const error = this.#botErrors.get(config.id) ?? null; return { botId: config.id, phase: botPhase({ connected, error, connection }), connected, configured: true, bot: publicBot(config), connection, error }; }); const registrationSnapshot = registration ? this.#registrationSnapshot(registration) : { state: "idle", attempt: 0, updatedAt: Date.now() }; const registering = ACTIVE_REGISTRATION_STATES2.has(registrationSnapshot.state); const connecting = registrationSnapshot.state === "saving"; const registrationOwnsProjection = Boolean(registration) && (registering || connecting); const selected = bots.find((bot) => bot.botId === selectedBotId) ?? (registrationOwnsProjection ? null : bots[0] ?? null); const aggregateConnected = bots.some((bot) => bot.connected); let phase = selected?.phase ?? "unconfigured"; if (registering) phase = "registering"; else if (connecting) phase = "connecting"; else if (registrationSnapshot.state === "error" && !selected) phase = "error"; return { schemaVersion: 2, revision: this.#revision, phase, connected: selected?.connected ?? false, configured: bots.length > 0, bot: selected?.bot ?? null, connection: selected?.connection ?? idleConnection(), error: selected?.error ?? registrationSnapshot.error ?? null, registration: registrationSnapshot, bots, totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length }, anyConnected: aggregateConnected }; } #registrationSnapshot(record) { const snapshot = record.manager.status(); return { ...snapshot, attempt: record.id, ...record.botId ? { botId: record.botId } : {} }; } async #acceptCredentials(record, result) { if (record.cancelled) throw new Error("Registration was cancelled"); const appId = result.client_id; const appSecret = result.client_secret; const ownerOpenId = result.user_info?.open_id; const domain = result.user_info?.tenant_brand === "lark" ? "lark" : "feishu"; if (!ownerOpenId) throw new Error("Feishu registration returned no owner open_id"); const bot = await this.#verifyApp({ appId, appSecret, domain }); if (record.cancelled) throw new Error("Registration was cancelled"); const existing = this.#configStore.list().find((candidate) => candidate.appId === appId); const botId = existing?.id ?? this.#createBotId(); if (typeof botId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(botId) || !existing && this.#configStore.getBot(botId)) { throw new Error("Bot id generator returned an invalid or duplicate id"); } const secretRef = existing?.secretRef ?? secretRefFor(botId); const previousOwnership = this.#botOwnership.get(botId); const previousSecret = await this.#credentials.resolve(secretRef).catch(() => void 0); await this.#credentials.set(secretRef, appSecret); let config; try { config = await this.#configStore.saveBot({ ...existing, id: botId, appId, secretRef, ownerOpenIds: [.../* @__PURE__ */ new Set([...existing?.ownerOpenIds ?? [], ownerOpenId])], domain, botName: bot.name, botOpenId: bot.openId, activated: bot.activated, deletionPending: false, connectedAt: (/* @__PURE__ */ new Date()).toISOString(), createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString() }); record.botId = botId; record.createdNew = !existing; this.#botOwnership.set(botId, record.id); } catch (error) { try { await this.#restoreCredential(secretRef, previousSecret); } catch (restoreError) { throw new Error("Unable to restore the Feishu credential after a config failure.", { cause: restoreError }); } throw error; } if (record.cancelled) { if (record.createdNew) { await this.#withBotTransition(botId, () => this.#deleteBot(botId)); } else { await this.#configStore.saveBot(existing); await this.#restoreCredential(secretRef, previousSecret); if (previousOwnership) this.#botOwnership.set(botId, previousOwnership); else this.#botOwnership.delete(botId); } throw new Error("Registration was cancelled"); } let cancellationRolledBack = false; try { await this.#withBotTransition(botId, () => this.#startRuntime(config, appSecret)); if (record.cancelled) { if (record.createdNew) { await this.#withBotTransition(botId, () => this.#deleteBot(botId)); } else { await this.#configStore.saveBot(existing); await this.#restoreCredential(secretRef, previousSecret); if (previousOwnership) this.#botOwnership.set(botId, previousOwnership); else this.#botOwnership.delete(botId); if (previousSecret?.value && !existing.deletionPending) { await this.#withBotTransition(botId, () => this.#startRuntime(existing, previousSecret.value)); } else { await this.#withBotTransition(botId, () => this.#stopRuntime(botId)); } } cancellationRolledBack = true; throw new Error("Registration was cancelled"); } this.#botErrors.delete(botId); this.#touch(); } catch (error) { if (record.cancelled) { if (!cancellationRolledBack && record.createdNew && this.#configStore.getBot(botId)) { await this.#withBotTransition(botId, () => this.#deleteBot(botId)); } else if (!cancellationRolledBack && existing) { await this.#configStore.saveBot(existing); await this.#restoreCredential(secretRef, previousSecret); if (previousOwnership) this.#botOwnership.set(botId, previousOwnership); else this.#botOwnership.delete(botId); if (!this.#closed && previousSecret?.value && !existing.deletionPending) { await this.#withBotTransition(botId, () => this.#startRuntime(existing, previousSecret.value)); } else { await this.#withBotTransition(botId, () => this.#stopRuntime(botId)); } } this.#touch(); throw error; } if (existing && previousSecret?.value) { try { await this.#configStore.saveBot(existing); await this.#restoreCredential(secretRef, previousSecret); if (previousOwnership) this.#botOwnership.set(botId, previousOwnership); else this.#botOwnership.delete(botId); if (!existing.deletionPending) { await this.#withBotTransition(botId, () => this.#startRuntime(existing, previousSecret.value)); this.#botErrors.delete(botId); } else { await this.#withBotTransition(botId, () => this.#stopRuntime(botId)); this.#botErrors.set(botId, { code: "deletion_pending", message: "\u673A\u5668\u4EBA\u6B63\u5728\u7B49\u5F85\u5B8C\u6210\u672C\u5730\u5220\u9664\uFF0C\u8BF7\u91CD\u8BD5\u79FB\u9664\u3002" }); } this.#touch(); throw error; } catch (restoreError) { if (restoreError === error) throw error; this.#botErrors.set(botId, { code: "connection_failed", message: "\u673A\u5668\u4EBA\u8FDE\u63A5\u66F4\u65B0\u5931\u8D25\uFF0C\u4E14\u539F\u8FDE\u63A5\u65E0\u6CD5\u6062\u590D\uFF0C\u8BF7\u91CD\u8BD5\u3002" }); this.#touch(); throw new Error("Unable to restore the previous Feishu bot connection.", { cause: restoreError }); } } this.#botErrors.set(botId, { code: "connection_failed", message: "\u673A\u5668\u4EBA\u5DF2\u7ECF\u521B\u5EFA\uFF0C\u4F46\u957F\u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u8BF7\u70B9\u51FB\u91CD\u8BD5\u3002" }); this.#touch(); throw error; } } async #startRuntime(config, appSecret) { await this.#stopRuntime(config.id); const runtime = await this.#createRuntime({ botId: config.id, config, appSecret }); this.#runtimes.set(config.id, runtime); try { await runtime.start(); } catch (error) { if (this.#runtimes.get(config.id) === runtime) this.#runtimes.delete(config.id); await runtime.stop({ preserveError: true }).catch(() => void 0); throw error; } } async #stopRuntime(botId) { const runtime = this.#runtimes.get(botId); this.#runtimes.delete(botId); if (runtime) await runtime.stop(); } async #deleteBot(botId) { let config = this.#configStore.getBot(botId); if (!config) return; if (!config.deletionPending) { config = await this.#configStore.saveBot({ ...config, deletionPending: true }); } await this.#stopRuntime(botId); try { await this.#credentials.unset(config.secretRef); } catch (error) { this.#botErrors.set(botId, { code: "credential_removal_failed", message: "\u65E0\u6CD5\u5220\u9664\u673A\u5668\u4EBA\u51ED\u636E\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" }); throw new Error("Unable to remove the Feishu credential.", { cause: error }); } try { await this.#deleteState({ botId, config }); } catch (error) { this.#botErrors.set(botId, { code: "state_cleanup_failed", message: "\u65E0\u6CD5\u5220\u9664\u673A\u5668\u4EBA\u7684\u672C\u5730\u4F1A\u8BDD\u6570\u636E\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" }); throw new Error("Unable to remove the Feishu bot session state.", { cause: error }); } await this.#configStore.removeBot(botId); this.#botErrors.delete(botId); this.#botOwnership.delete(botId); } async #restoreCredential(secretRef, previous) { if (previous?.value) await this.#credentials.set(secretRef, previous.value); else await this.#credentials.unset(secretRef); } #requireBot(botId) { const config = this.#configStore.getBot(botId); if (!config) throw new Error("Unknown Feishu bot"); return config; } #assertOpen() { if (this.#closed) throw new Error("The Feishu controller is closed"); } #serializeConfig(operation) { const result = this.#configTransition.then(operation, operation); this.#configTransition = result.then(() => void 0, () => void 0); return result; } #withBotTransition(botId, operation) { const previous = this.#botTransitions.get(botId) ?? Promise.resolve(); const result = previous.then(operation, operation); const tail = result.then(() => void 0, () => void 0); this.#botTransitions.set(botId, tail); void tail.finally(() => { if (this.#botTransitions.get(botId) === tail) this.#botTransitions.delete(botId); }); return result; } #trimRegistrations() { if (this.#registrations.size <= 32) return; for (const [id, record] of this.#registrations) { if (id === this.#latestRegistrationId) continue; const state = record.manager.status().state; if (!ACTIVE_REGISTRATION_STATES2.has(state) && state !== "saving") { this.#registrations.delete(id); } if (this.#registrations.size <= 32) break; } } #touch() { this.#revision += 1; } }; // src/channels/feishu/state-store.mjs import { mkdir as mkdir6, readFile as readFile6, rename as rename6, writeFile as writeFile6 } from "node:fs/promises"; import { dirname as dirname6 } from "node:path"; var EMPTY_STATE3 = Object.freeze({ version: 1, sessions: {}, seenMessageIds: [] }); var StateStore = class { #path; #state = structuredClone(EMPTY_STATE3); #writeQueue = Promise.resolve(); constructor(path) { this.#path = path; } async load() { try { const parsed = JSON.parse(await readFile6(this.#path, "utf8")); this.#state = { version: 1, sessions: parsed.sessions && typeof parsed.sessions === "object" ? parsed.sessions : {}, seenMessageIds: Array.isArray(parsed.seenMessageIds) ? parsed.seenMessageIds.slice(-1e3) : [] }; } catch (error) { if (error?.code !== "ENOENT") throw error; await this.#persist(); } return this; } sessionFor(key) { return this.#state.sessions[key] ?? null; } async setSession(key, sessionId) { this.#state.sessions[key] = sessionId; await this.#persist(); } async clearSession(key) { delete this.#state.sessions[key]; await this.#persist(); } hasSeen(messageId) { return this.#state.seenMessageIds.includes(messageId); } async markSeen(messageId) { if (this.hasSeen(messageId)) return; this.#state.seenMessageIds.push(messageId); if (this.#state.seenMessageIds.length > 1e3) { this.#state.seenMessageIds.splice(0, this.#state.seenMessageIds.length - 1e3); } await this.#persist(); } snapshot() { return structuredClone(this.#state); } async #persist() { const snapshot = JSON.stringify(this.#state, null, 2) + "\n"; this.#writeQueue = this.#writeQueue.then(async () => { await mkdir6(dirname6(this.#path), { recursive: true, mode: 448 }); const temporary = `${this.#path}.tmp`; await writeFile6(temporary, snapshot, { encoding: "utf8", mode: 384 }); await rename6(temporary, this.#path); }); await this.#writeQueue; } }; // plugin-src/host/channels/feishu/production.mjs function harnessOrigin3(webServer, configured) { if (configured !== void 0) return new URL(configured); const port = webServer?.port; if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error("dsh-feishu requires an initialized DSH webServer port"); } return new URL(`http://127.0.0.1:${port}`); } function pluginPaths3(config) { const dshHome = resolve3(config.dshHome ?? process.env.DSH_HOME ?? join3(homedir3(), ".dsh")); const root = resolve3(config.dataDir ?? join3(dshHome, "integrations", "dsh-feishu")); return { root, config: resolve3(config.configPath ?? join3(root, "config.json")), legacyState: resolve3(config.statePath ?? join3(root, "state.json")), bots: resolve3(config.botsDir ?? join3(root, "bots")) }; } async function createProductionController3(ctx, config = {}, internals = {}) { if (!ctx?.credentials) throw new TypeError("dsh-feishu requires ctx.credentials"); if (!ctx?.webServer) throw new TypeError("dsh-feishu requires ctx.webServer"); const lark = internals.lark ?? Lark; const Controller = internals.Controller ?? MultiBotDshFeishuController; const ConfigStore = internals.ConfigStore ?? PluginConfigStore; const SessionStateStore = internals.StateStore ?? StateStore; const Harness = internals.HarnessClient ?? HarnessClient3; const Runtime = internals.FeishuRuntime ?? FeishuRuntime; const verifyApp = internals.verifyFeishuApp ?? verifyFeishuApp; const createSupervisor = internals.createConnectionSupervisor ?? createConnectionSupervisor2; const logger = typeof ctx.logger === "function" ? ctx.logger("dsh-feishu") : ctx.logger ?? console; const paths = pluginPaths3(config); const configStore = await new ConfigStore(paths.config).load(); const stateStores = /* @__PURE__ */ new Map(); const statePathFor = (botConfig) => !botConfig.id || !botConfig.secretRef || botConfig.secretRef === LEGACY_FEISHU_SECRET_REF ? paths.legacyState : resolve3(paths.bots, botConfig.id, "state.json"); const stateFor = async (botConfig) => { const stateKey = botConfig.id ?? "__legacy__"; let state = stateStores.get(stateKey); if (!state) { state = await new SessionStateStore(statePathFor(botConfig)).load(); stateStores.set(stateKey, state); } return state; }; const harness = new Harness({ baseUrl: harnessOrigin3(ctx.webServer, config.harnessBaseUrl), workspace: resolve3(config.workspace ?? process.cwd()), agentPreset: config.agentPreset ?? "standard", // This plugin is already hosted by a running DSH process. Starting a // second DSH would create a competing server and lifecycle. autostart: false, dshBin: config.dshBin ?? "dsh" }); const controller = new Controller({ registerApp: (options) => lark.registerApp(options), verifyApp, credentials: ctx.credentials, configStore, createRuntime: async ({ botId, config: botConfig, appSecret }) => { const state = await stateFor(botConfig); return new Runtime({ lark, appId: botConfig.appId, appSecret, domain: botConfig.domain, ownerOpenIds: botConfig.ownerOpenIds ?? [botConfig.ownerOpenId], harness, state, replyTimeoutMs: config.replyTimeoutMs ?? 6e5, logger: { error: (...args) => logger.error?.(`[${botId ?? botConfig.id}]`, ...args), warn: (...args) => logger.warn?.(`[${botId ?? botConfig.id}]`, ...args), info: (...args) => logger.info?.(`[${botId ?? botConfig.id}]`, ...args), debug: (...args) => logger.debug?.(`[${botId ?? botConfig.id}]`, ...args) } }); }, deleteState: async ({ botId, config: botConfig }) => { stateStores.delete(botId); try { await unlink8(statePathFor(botConfig)); } catch (error) { if (error?.code !== "ENOENT") throw error; } } }); const supervisor = createSupervisor({ controller, harness, logger, retryDelaysMs: config.retryDelaysMs, healthyIntervalMs: config.healthyIntervalMs }).start(); return { controller, ready: supervisor.ready, async close() { await supervisor.close(); await controller.close(); harness.stopManagedProcess(); } }; } // plugin-src/host/channels/feishu/rpc.mjs import QRCode2 from "qrcode"; // plugin-src/client/channels/feishu/api.js var FEISHU_RPC_CHANNEL = "/feishu"; var FEISHU_ENDPOINTS = Object.freeze({ status: "connection.status", beginProvisioning: "provision.begin", pollProvisioning: "provision.poll", cancelProvisioning: "provision.cancel", bindCredentials: "bot.bind-credentials", reconnectBot: "bot.reconnect", disconnectBot: "bot.disconnect", deleteBot: "bot.delete", // Kept for rolling upgrades. The multi-bot UI never calls these endpoints. testConnection: "connection.test", disconnect: "connection.disconnect" }); // plugin-src/host/channels/feishu/rpc.mjs var FEISHU_MULTI_ENDPOINTS = Object.freeze({ reconnectBot: "bot.reconnect", disconnectBot: "bot.disconnect", deleteBot: "bot.delete" }); var FEISHU_RPC_ENDPOINTS = Object.freeze([ .../* @__PURE__ */ new Set([...Object.values(FEISHU_ENDPOINTS), ...Object.values(FEISHU_MULTI_ENDPOINTS)]) ]); var REGISTRATION_STATES2 = /* @__PURE__ */ new Set([ "idle", "starting", "qr_ready", "polling", "slow_down", "domain_switched", "saving", "succeeded", "expired", "cancelled", "error" ]); var SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/; var PUBLIC_ERROR_MESSAGES = Object.freeze({ abort: "Registration was cancelled.", access_denied: "Registration was denied.", expired_token: "The registration QR code expired.", invalid_credentials: "Feishu returned invalid app credentials.", credentials_callback_failed: "Unable to activate the Feishu connection.", registration_failed: "Unable to register the Feishu app.", connection_failed: "The bot was created, but its connection could not be started.", credential_removal_failed: "Unable to remove the Feishu credentials.", state_cleanup_failed: "Unable to remove the bot session data. Please retry.", deletion_pending: "Bot deletion is incomplete. Retry removal to finish cleanup.", missing_credentials: "The bot credentials are missing. Delete it and scan again." }); var POLL_STATUS_BY_REGISTRATION = Object.freeze({ idle: "pending", starting: "pending", qr_ready: "pending", polling: "pending", slow_down: "pending", domain_switched: "pending", saving: "connecting", succeeded: "connected", expired: "expired", cancelled: "failed", error: "failed" }); function isPlainObject(value) { if (value === null || typeof value !== "object" || Array.isArray(value)) return false; const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; } function hasOnlyKeys(value, allowed) { return isPlainObject(value) && Reflect.ownKeys(value).every((key) => typeof key === "string" && allowed.has(key)); } function finiteNumber(value) { return Number.isFinite(value) ? value : void 0; } function safeOpaqueId(value) { return typeof value === "string" && SAFE_ID.test(value); } function validCredential2(value, maxLength) { return typeof value === "string" && value.trim().length > 0 && value.length <= maxLength; } function publicError2(error) { if (!error || typeof error !== "object") return null; const code = typeof error.code === "string" && Object.hasOwn(PUBLIC_ERROR_MESSAGES, error.code) ? error.code : "registration_failed"; return { code, message: PUBLIC_ERROR_MESSAGES[code] }; } function publicRegistration(registration) { if (!registration || typeof registration !== "object") return { state: "idle", attempt: 0 }; const state = REGISTRATION_STATES2.has(registration.state) ? registration.state : "error"; const attempt = safeOpaqueId(registration.attempt) ? registration.attempt : finiteNumber(registration.attempt) ?? 0; const result = { state, attempt }; const updatedAt = finiteNumber(registration.updatedAt); const expiresAt = finiteNumber(registration.expiresAt); const remainingSeconds = finiteNumber(registration.remainingSeconds); const pollIntervalSeconds = finiteNumber(registration.pollIntervalSeconds); if (updatedAt !== void 0) result.updatedAt = updatedAt; if (typeof registration.qrCodeUrl === "string" && registration.qrCodeUrl.length > 0) { result.qrCodeUrl = registration.qrCodeUrl; } if (expiresAt !== void 0) result.expiresAt = expiresAt; if (remainingSeconds !== void 0) result.remainingSeconds = remainingSeconds; if (pollIntervalSeconds !== void 0) result.pollIntervalSeconds = pollIntervalSeconds; if (safeOpaqueId(registration.botId)) result.botId = registration.botId; const error = publicError2(registration.error); if (error) result.error = error; return result; } function connectionFacts(connection) { const source = connection && typeof connection === "object" ? connection : {}; const connected = source.connected === true || source.ready === true && source.feishuLongConnectionState === "connected" && source.harnessReachable === true; return { connected, ready: source.ready === true, harnessReachable: source.harnessReachable === true }; } function publicBot2(bot) { const source = bot && typeof bot === "object" ? bot : {}; const result = { name: typeof source.name === "string" && source.name.length > 0 ? source.name : "\u98DE\u4E66\u673A\u5668\u4EBA" }; if (typeof source.avatarUrl === "string") result.avatarUrl = source.avatarUrl; if (typeof source.appIdMasked === "string") result.appIdMasked = source.appIdMasked; if (typeof source.tenantName === "string") result.tenantName = source.tenantName; if (source.domain === "feishu" || source.domain === "lark") result.domain = source.domain; if (typeof source.activated === "boolean" || typeof source.activated === "number") { result.activated = source.activated; } return result; } function publicHealth(status, connected) { if (connected) return { status: "healthy", summary: "\u957F\u8FDE\u63A5\u8FD0\u884C\u6B63\u5E38", lastCheckedAt: Date.now() }; if (status?.configured === true) { return { status: "offline", summary: "\u673A\u5668\u4EBA\u5C1A\u672A\u8FDE\u63A5", lastCheckedAt: Date.now() }; } return { status: "offline", summary: "\u5C1A\u672A\u63A5\u5165\u98DE\u4E66\u673A\u5668\u4EBA", lastCheckedAt: Date.now() }; } function connectionState(status, registration, connected) { if (connected) return "connected"; if (status?.phase === "error" || registration.state === "error") return "error"; if (status?.phase === "connecting" || registration.state === "saving") return "connecting"; if (status?.phase === "registering" || ["starting", "qr_ready", "polling", "slow_down", "domain_switched"].includes(registration.state)) { return "provisioning"; } return "disconnected"; } async function qrCodeDataUrl(verificationUrl) { return QRCode2.toDataURL(verificationUrl, { errorCorrectionLevel: "M", margin: 1, width: 320, type: "image/png" }); } async function publicProvisioning(registration, encodeQr) { if (!registration.qrCodeUrl) return void 0; return { attemptId: String(registration.attempt), verificationUrl: registration.qrCodeUrl, qrCodeDataUrl: await encodeQr(registration.qrCodeUrl), expiresAt: registration.expiresAt ?? Date.now() + 5 * 6e4, pollIntervalMs: Math.max(800, Math.min(1e4, (registration.pollIntervalSeconds ?? 1.8) * 1e3)) }; } function publicBotEntry(entry) { const source = entry && typeof entry === "object" ? entry : {}; if (!safeOpaqueId(source.botId)) return null; const facts = connectionFacts(source.connection); const connected = source.connected === true || facts.connected; const registration = { state: "idle" }; const result = { botId: source.botId, state: connectionState(source, registration, connected), connected, configured: source.configured === true, bot: publicBot2(source.bot), health: publicHealth(source, connected) }; const error = publicError2(source.error); if (error) result.error = error; return result; } async function toPublicFeishuStatus(status, { encodeQr = qrCodeDataUrl } = {}) { const source = status && typeof status === "object" ? status : {}; const registration = publicRegistration(source.registration); const facts = connectionFacts(source.connection); const connected = source.connected === true || facts.connected; const provisioning = await publicProvisioning(registration, encodeQr); const error = publicError2(source.error) ?? registration.error ?? null; const bots = Array.isArray(source.bots) ? source.bots.map(publicBotEntry).filter(Boolean) : []; const snapshot = { schemaVersion: source.schemaVersion === 2 ? 2 : 1, revision: Number.isSafeInteger(source.revision) && source.revision >= 0 ? source.revision : 0, state: connectionState(source, registration, connected), connected, configured: source.configured === true, bot: publicBot2(source.bot), health: publicHealth(source, connected), bots, totals: { configured: bots.length || (source.configured === true ? 1 : 0), connected: bots.length ? bots.filter((bot) => bot.connected).length : connected ? 1 : 0 } }; if (provisioning) snapshot.provisioning = provisioning; if (error) snapshot.error = error; return snapshot; } function badRequest2(message) { return { ok: false, error: { code: "bad-request", message, details: { issues: [] } } }; } function cancelled2() { return { ok: false, error: { code: "cancelled", message: "The Feishu request was cancelled.", details: {} } }; } function internalFailure2() { return { ok: false, error: { code: "internal", message: "The Feishu integration operation failed.", details: {} } }; } function validPayload(endpoint, payload) { if (endpoint === FEISHU_ENDPOINTS.status) { return hasOnlyKeys(payload, /* @__PURE__ */ new Set()) ? null : "This endpoint accepts an empty payload only."; } if (endpoint === FEISHU_ENDPOINTS.testConnection) { return hasOnlyKeys(payload, /* @__PURE__ */ new Set()) ? null : "This endpoint accepts an empty payload only."; } if (endpoint === FEISHU_ENDPOINTS.beginProvisioning) { if (!hasOnlyKeys(payload, /* @__PURE__ */ new Set(["locale", "replaceAttemptId"]))) { return "Provisioning accepts locale and replaceAttemptId only."; } if (payload.locale !== void 0 && payload.locale !== "zh-CN") return "The provisioning locale must be zh-CN."; if (payload.replaceAttemptId !== void 0 && !safeOpaqueId(payload.replaceAttemptId)) { return "replaceAttemptId must be a valid opaque id."; } return null; } if (endpoint === FEISHU_ENDPOINTS.bindCredentials) { return hasOnlyKeys(payload, /* @__PURE__ */ new Set(["appId", "appSecret"])) && validCredential2(payload.appId, 256) && validCredential2(payload.appSecret, 1024) ? null : "Credential binding requires App ID and App Secret."; } if (endpoint === FEISHU_ENDPOINTS.pollProvisioning || endpoint === FEISHU_ENDPOINTS.cancelProvisioning) { return hasOnlyKeys(payload, /* @__PURE__ */ new Set(["attemptId"])) && safeOpaqueId(payload.attemptId) ? null : "A single valid attemptId is required."; } if (endpoint === FEISHU_ENDPOINTS.disconnect) { return hasOnlyKeys(payload, /* @__PURE__ */ new Set(["removeCredentials"])) && payload.removeCredentials === true ? null : "Disconnect requires removeCredentials=true."; } if (endpoint === FEISHU_MULTI_ENDPOINTS.reconnectBot || endpoint === FEISHU_MULTI_ENDPOINTS.disconnectBot) { return hasOnlyKeys(payload, /* @__PURE__ */ new Set(["botId"])) && safeOpaqueId(payload.botId) ? null : "A single valid botId is required."; } if (endpoint === FEISHU_MULTI_ENDPOINTS.deleteBot) { return hasOnlyKeys(payload, /* @__PURE__ */ new Set(["botId", "confirm"])) && safeOpaqueId(payload.botId) && payload.confirm === true ? null : "Deleting a bot requires a valid botId and confirm=true."; } return "Unknown Feishu endpoint."; } function abortableDelay2(milliseconds, signal) { return new Promise((resolve7, reject) => { if (signal?.aborted) { reject(signal.reason ?? new Error("aborted")); return; } const timer = setTimeout(done, milliseconds); timer.unref?.(); function done() { signal?.removeEventListener("abort", aborted); resolve7(); } function aborted() { clearTimeout(timer); reject(signal.reason ?? new Error("aborted")); } signal?.addEventListener("abort", aborted, { once: true }); }); } async function statusForRegistration(controller, attemptId) { if (typeof controller.registrationStatus === "function") { return controller.registrationStatus(attemptId); } return controller.status(); } async function waitForQr(controller, initial, attemptId, signal) { let current = initial; const deadline = Date.now() + 15e3; for (; ; ) { const registration = publicRegistration(current?.registration); if (registration.qrCodeUrl) return current; if (["error", "expired", "cancelled"].includes(registration.state)) { throw new Error("Provisioning stopped before the QR code was ready."); } if (Date.now() >= deadline) throw new Error("Provisioning QR code timed out."); await abortableDelay2(50, signal); current = await statusForRegistration(controller, attemptId); if (!current) throw new Error("The provisioning attempt is no longer active."); } } function sameAttempt(status, attemptId) { return String(publicRegistration(status?.registration).attempt) === attemptId; } function pollStatus(status) { const registration = publicRegistration(status?.registration); if (registration.state === "succeeded") { const connected = registration.botId && (status?.connected === true || connectionFacts(status?.connection).connected); return connected ? "connected" : "connecting"; } return POLL_STATUS_BY_REGISTRATION[registration.state] ?? "failed"; } function assertController2(controller) { if (!controller || typeof controller.status !== "function" || typeof controller.startRegistration !== "function" || typeof controller.cancelRegistration !== "function" || typeof controller.disconnect !== "function") { throw new TypeError("A Feishu controller with status/start/cancel/disconnect is required"); } } function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl } = {}) { assertController2(controller); const qrCache = /* @__PURE__ */ new Map(); const attemptQr = /* @__PURE__ */ new Map(); const cachedEncodeQr = (url) => { let encoded = qrCache.get(url); if (!encoded) { if (qrCache.size >= 32) qrCache.delete(qrCache.keys().next().value); encoded = Promise.resolve().then(() => encodeQr(url)); qrCache.set(url, encoded); } return encoded; }; return async (endpoint, payload, signal) => { if (signal?.aborted) return cancelled2(); if (!FEISHU_RPC_ENDPOINTS.includes(endpoint)) return badRequest2("Unknown Feishu endpoint."); const payloadFailure6 = validPayload(endpoint, payload); if (payloadFailure6) return badRequest2(payloadFailure6); try { let value; if (endpoint === FEISHU_ENDPOINTS.status) { value = await toPublicFeishuStatus(await controller.status(), { encodeQr: cachedEncodeQr }); } else if (endpoint === FEISHU_ENDPOINTS.beginProvisioning) { if (payload.replaceAttemptId) { await controller.cancelRegistration(payload.replaceAttemptId); } const started = await controller.startRegistration({ locale: payload.locale }); const attemptId = String(publicRegistration(started?.registration).attempt); const ready = await waitForQr(controller, started, attemptId, signal); value = (await toPublicFeishuStatus(ready, { encodeQr: cachedEncodeQr })).provisioning; if (!value) throw new Error("Provisioning did not produce a QR code."); attemptQr.set(attemptId, value.verificationUrl); } else if (endpoint === FEISHU_ENDPOINTS.pollProvisioning) { const current = await statusForRegistration(controller, payload.attemptId); if (!current || !sameAttempt(current, payload.attemptId)) { return badRequest2("The provisioning attempt is no longer active."); } const registration = publicRegistration(current.registration); const connection = await toPublicFeishuStatus(current, { encodeQr: cachedEncodeQr }); value = { status: pollStatus(current), ...registration.botId ? { botId: registration.botId } : {}, ...connection.provisioning ? { provisioning: connection.provisioning } : {}, ...registration.botId && connection.connected ? { connection } : {}, ...connection.error ? { message: connection.error.message } : {} }; if (["connected", "expired", "failed"].includes(value.status)) { const url = attemptQr.get(payload.attemptId); if (url) qrCache.delete(url); attemptQr.delete(payload.attemptId); } } else if (endpoint === FEISHU_ENDPOINTS.cancelProvisioning) { const current = await statusForRegistration(controller, payload.attemptId); if (!current || !sameAttempt(current, payload.attemptId)) { return badRequest2("The provisioning attempt is no longer active."); } const multi = typeof controller.registrationStatus === "function"; const registration = publicRegistration(current.registration); if (!multi && registration.state === "saving") await controller.disconnect(); else await controller.cancelRegistration(payload.attemptId); const url = attemptQr.get(payload.attemptId); if (url) qrCache.delete(url); attemptQr.delete(payload.attemptId); value = { status: "failed", message: "Registration was cancelled." }; } else if (endpoint === FEISHU_ENDPOINTS.bindCredentials) { if (typeof controller.bindCredentials !== "function") { throw new Error("Credential binding is unavailable"); } value = await toPublicFeishuStatus( await controller.bindCredentials(payload), { encodeQr: cachedEncodeQr } ); } else if (endpoint === FEISHU_ENDPOINTS.testConnection) { const current = await controller.status(); const alreadyConnected = current?.connected === true || connectionFacts(current?.connection).connected; const checked = alreadyConnected || typeof controller.reconnect !== "function" ? current : await controller.reconnect(); value = await toPublicFeishuStatus(checked, { encodeQr: cachedEncodeQr }); } else if (endpoint === FEISHU_ENDPOINTS.disconnect) { value = await toPublicFeishuStatus(await controller.disconnect(), { encodeQr: cachedEncodeQr }); } else if (endpoint === FEISHU_MULTI_ENDPOINTS.reconnectBot) { if (typeof controller.reconnectBot !== "function") throw new Error("Multi-bot reconnect is unavailable"); value = await toPublicFeishuStatus(await controller.reconnectBot(payload.botId), { encodeQr: cachedEncodeQr }); } else if (endpoint === FEISHU_MULTI_ENDPOINTS.disconnectBot) { if (typeof controller.disconnectBot !== "function") throw new Error("Multi-bot disconnect is unavailable"); value = await toPublicFeishuStatus(await controller.disconnectBot(payload.botId), { encodeQr: cachedEncodeQr }); } else { if (typeof controller.deleteBot !== "function") throw new Error("Multi-bot delete is unavailable"); value = await toPublicFeishuStatus(await controller.deleteBot(payload.botId), { encodeQr: cachedEncodeQr }); } if (signal?.aborted) return cancelled2(); return { ok: true, value }; } catch { return signal?.aborted ? cancelled2() : internalFailure2(); } }; } function installFeishuRpc(ctx, controller, options, authority) { if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== "function") { throw new TypeError("DSH Host Connection RPC is required"); } return ctx.connection.rpc.handle( FEISHU_RPC_CHANNEL, createFeishuRpcHandler(controller, options), { authority: resolveRpcAuthority(authority) } ); } // plugin-src/host/channels/feishu/index.mjs function controllerFrom(ctx, config) { if (config?.controller) return config.controller; if (typeof config?.createController === "function") return config.createController(); if (typeof config?.createProvisioningManager === "function") { return createProvisioningBackedController(config); } return void 0; } async function apply3(ctx, config = {}) { const controller = controllerFrom(ctx, config); if (controller) { return installFeishuRpc(ctx, controller, config.rpcOptions, config.rpcAuthority); } const production = await createProductionController3(ctx, config); const disposeRpc = installFeishuRpc( ctx, production.controller, config.rpcOptions, config.rpcAuthority ); ctx.effect(() => async () => { await production.close(); }, "dsh-feishu: close controller and live connection"); return disposeRpc; } // plugin-src/host/channels/qq/production.mjs import { unlink as unlink11 } from "node:fs/promises"; import { homedir as homedir4 } from "node:os"; import { join as join4, resolve as resolve4 } from "node:path"; // src/channels/qq/config-store.mjs import { createHash as createHash4 } from "node:crypto"; import { mkdir as mkdir7, readFile as readFile7, rename as rename7, unlink as unlink9, writeFile as writeFile7 } from "node:fs/promises"; import { dirname as dirname7 } from "node:path"; var EMPTY_DOCUMENT3 = Object.freeze({ version: 1, bots: Object.freeze([]) }); function cleanString8(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function safeBotId2(value) { const id = cleanString8(value); return id && /^qq_[a-f0-9]{24}$/.test(id) ? id : null; } function safeSecretRef2(value) { const ref = cleanString8(value); return ref && /^DSH_QQBOT_APP_SECRET_[A-F0-9]{24}$/.test(ref) ? ref : null; } function deriveQqBotIdentity(appId) { const raw = cleanString8(appId); if (!raw) throw new TypeError("appId is required"); const digest2 = createHash4("sha256").update(raw).digest("hex").slice(0, 24); return { botId: `qq_${digest2}`, secretRef: `DSH_QQBOT_APP_SECRET_${digest2.toUpperCase()}` }; } function maskQqAppId(appId) { const value = cleanString8(appId) ?? ""; if (value.length <= 10) return value ? `${value.slice(0, 3)}\u2022\u2022\u2022` : "QQ\u673A\u5668\u4EBA"; return `${value.slice(0, 6)}\u2022\u2022\u2022\u2022${value.slice(-4)}`; } function normalizeBot3(value) { if (!value || typeof value !== "object") return null; const appId = cleanString8(value.appId); const ownerUserOpenid = cleanString8(value.ownerUserOpenid); const botId = safeBotId2(value.botId); const secretRef = safeSecretRef2(value.secretRef); if (!appId || !ownerUserOpenid || !botId || !secretRef) return null; const derived = deriveQqBotIdentity(appId); if (derived.botId !== botId || derived.secretRef !== secretRef) return null; return Object.freeze({ botId, appId, secretRef, ownerUserOpenid, createdAt: cleanString8(value.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString(), connectedAt: cleanString8(value.connectedAt) }); } function normalizeDocument3(value) { if (!value || value.version !== 1 || !Array.isArray(value.bots)) return null; const bots = value.bots.map(normalizeBot3); if (bots.some((bot) => bot === null)) return null; const ids = /* @__PURE__ */ new Set(); const appIds = /* @__PURE__ */ new Set(); const refs = /* @__PURE__ */ new Set(); for (const bot of bots) { if (ids.has(bot.botId) || appIds.has(bot.appId) || refs.has(bot.secretRef)) return null; ids.add(bot.botId); appIds.add(bot.appId); refs.add(bot.secretRef); } return Object.freeze({ version: 1, bots: Object.freeze(bots) }); } var QqConfigStore = class { #path; #value = EMPTY_DOCUMENT3; #writeQueue = Promise.resolve(); constructor(path) { this.#path = path; } async load() { try { const normalized = normalizeDocument3(JSON.parse(await readFile7(this.#path, "utf8"))); if (!normalized) throw new Error("dsh-im QQ config contains invalid bot data"); this.#value = normalized; } catch (error) { if (error?.code !== "ENOENT") throw error; this.#value = EMPTY_DOCUMENT3; } return this; } list() { return structuredClone(this.#value.bots); } get(botId) { const bot = this.#value.bots.find((candidate) => candidate.botId === botId); return bot ? structuredClone(bot) : null; } getByAppId(appId) { const bot = this.#value.bots.find((candidate) => candidate.appId === appId); return bot ? structuredClone(bot) : null; } async save(value) { const normalized = normalizeBot3(value); if (!normalized) throw new Error("Refusing to persist incomplete QQ bot data"); return this.#mutate((bots) => { const appCollision = bots.find( (bot) => bot.appId === normalized.appId && bot.botId !== normalized.botId ); const refCollision = bots.find( (bot) => bot.secretRef === normalized.secretRef && bot.botId !== normalized.botId ); if (appCollision || refCollision) throw new Error("Duplicate QQ bot identity"); const index = bots.findIndex((bot) => bot.botId === normalized.botId); if (index === -1) bots.push(normalized); else bots[index] = normalized; return structuredClone(normalized); }); } async remove(botId) { if (!safeBotId2(botId)) throw new TypeError("Invalid QQ bot id"); return this.#mutate((bots) => { const index = bots.findIndex((bot) => bot.botId === botId); if (index === -1) return null; const [removed] = bots.splice(index, 1); return structuredClone(removed); }); } async clear() { const operation = this.#writeQueue.then(async () => { try { await unlink9(this.#path); } catch (error) { if (error?.code !== "ENOENT") throw error; } this.#value = EMPTY_DOCUMENT3; }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; } async #mutate(mutator) { let result; const operation = this.#writeQueue.then(async () => { const bots = [...this.#value.bots]; result = mutator(bots); const document = Object.freeze({ version: 1, bots: Object.freeze(bots) }); await this.#write(document); this.#value = document; }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; return result; } async #write(document) { await mkdir7(dirname7(this.#path), { recursive: true, mode: 448 }); const temporary = `${this.#path}.tmp`; await writeFile7(temporary, `${JSON.stringify(document, null, 2)} `, { encoding: "utf8", mode: 384 }); await rename7(temporary, this.#path); } }; // src/channels/qq/harness-client.mjs var QqHarnessClient = class extends HarnessClient2 { }; // src/channels/qq/qq-controller.mjs import { randomUUID as randomUUID9 } from "node:crypto"; var ACTIVE_ATTEMPT_STATES2 = /* @__PURE__ */ new Set(["starting", "pending", "refreshing", "connecting"]); var TERMINAL_ATTEMPT_STATES2 = /* @__PURE__ */ new Set(["connected", "failed", "cancelled"]); var QR_TTL_MS = 5 * 6e4; function cleanString9(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function safeError3(code, message) { return Object.freeze({ code, message }); } function publicAttempt2(record) { if (!record) return null; return { attemptId: record.id, status: record.state, qrRevision: record.qrRevision, pollIntervalMs: 1e3, ...record.verificationUrl ? { verificationUrl: record.verificationUrl } : {}, ...record.expiresAt ? { expiresAt: record.expiresAt } : {}, ...record.botId ? { botId: record.botId } : {}, ...record.error ? { error: structuredClone(record.error) } : {} }; } var QqController = class { #qrAuth; #credentials; #configStore; #createRuntime; #deleteState; #logger; #runtimes = /* @__PURE__ */ new Map(); #errors = /* @__PURE__ */ new Map(); #attempts = /* @__PURE__ */ new Map(); #activeAttemptId = null; #transitions = /* @__PURE__ */ new Map(); #revision = 0; #closed = false; constructor({ qrAuth, credentials, configStore, createRuntime, deleteState = async () => { }, logger = console }) { if (!qrAuth || typeof qrAuth.start !== "function") throw new TypeError("QQ QR auth is required"); if (!credentials || typeof credentials.resolve !== "function" || typeof credentials.set !== "function" || typeof credentials.unset !== "function") { throw new TypeError("QqController requires the DSH credential provider"); } if (!configStore || typeof configStore.list !== "function" || typeof configStore.save !== "function" || typeof configStore.remove !== "function") { throw new TypeError("QqController requires a config store"); } if (typeof createRuntime !== "function") throw new TypeError("createRuntime is required"); this.#qrAuth = qrAuth; this.#credentials = credentials; this.#configStore = configStore; this.#createRuntime = createRuntime; this.#deleteState = deleteState; this.#logger = logger; } async initialize() { if (this.#closed) return this.status(); for (const config of this.#configStore.list()) { await this.#withBotTransition(config.botId, async () => { if (this.#closed || this.#runtimes.get(config.botId)?.status?.ready) return; const appSecret = await this.#resolveSecret(config.secretRef); if (!appSecret) { this.#errors.set(config.botId, safeError3("missing-secret", "QQ \u673A\u5668\u4EBA\u51ED\u636E\u7F3A\u5931\uFF0C\u8BF7\u79FB\u9664\u540E\u91CD\u65B0\u626B\u7801\u3002")); return; } try { await this.#startRuntime(config, appSecret); this.#errors.delete(config.botId); } catch (error) { this.#errors.set(config.botId, safeError3("connection-failed", "QQ \u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u91CD\u8BD5\u3002")); this.#logger.warn?.(`[dsh-im:qq] bot ${config.botId} failed to initialize:`, error); } finally { this.#touch(); } }); } return this.status(); } async startProvisioning() { if (this.#closed) throw new Error("QQ controller is closed"); if (this.#activeAttemptId) await this.cancelProvisioning(this.#activeAttemptId); let firstQrResolve; let firstQrReject; const firstQr = new Promise((resolve7, reject) => { firstQrResolve = resolve7; firstQrReject = reject; }); const record = { id: randomUUID9(), state: "starting", createdAt: Date.now(), expiresAt: null, qrRevision: 0, verificationUrl: null, controller: new AbortController(), dispose: null, task: null, error: null, botId: null }; this.#attempts.set(record.id, record); this.#activeAttemptId = record.id; this.#touch(); try { record.dispose = this.#qrAuth.start({ onQrDisplayed: (url) => { if (record.controller.signal.aborted || TERMINAL_ATTEMPT_STATES2.has(record.state)) return; const verificationUrl = cleanString9(url); if (!verificationUrl) return; record.verificationUrl = verificationUrl; record.qrRevision += 1; record.expiresAt = Date.now() + QR_TTL_MS; record.state = "pending"; this.#touch(); firstQrResolve(); }, onQrExpired: () => { if (record.controller.signal.aborted || TERMINAL_ATTEMPT_STATES2.has(record.state)) return; record.state = "refreshing"; record.verificationUrl = null; record.expiresAt = null; this.#touch(); }, onSuccess: (credentials) => { if (record.controller.signal.aborted || TERMINAL_ATTEMPT_STATES2.has(record.state)) return; record.task = this.#completeProvisioning(record, credentials); }, onFailure: (error) => { if (record.controller.signal.aborted || TERMINAL_ATTEMPT_STATES2.has(record.state)) return; record.state = "failed"; record.error = safeError3("qr-connect-failed", "QQ \u626B\u7801\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801\u3002"); if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; this.#touch(); firstQrReject(error); } }, { signal: record.controller.signal }); await firstQr; return publicAttempt2(record); } catch (error) { if (record.controller.signal.aborted) { record.state = "cancelled"; record.error = safeError3("cancelled", "\u626B\u7801\u7ED1\u5B9A\u5DF2\u53D6\u6D88\u3002"); } else if (!TERMINAL_ATTEMPT_STATES2.has(record.state)) { record.state = "failed"; record.error = safeError3("qr-start-failed", "\u65E0\u6CD5\u751F\u6210 QQ \u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"); } if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; this.#touch(); throw error; } } registrationStatus(attemptId) { return publicAttempt2(this.#attempts.get(attemptId)); } async bindCredentials({ appId, appSecret } = {}) { if (this.#closed) throw new Error("QQ controller is closed"); const normalizedAppId = cleanString9(appId); const normalizedSecret = cleanString9(appSecret); if (!normalizedAppId || !normalizedSecret) { throw new TypeError("QQ AppID and AppSecret are required"); } if (this.#activeAttemptId) await this.cancelProvisioning(this.#activeAttemptId); if (this.#closed) throw new Error("QQ controller is closed"); const identity = deriveQqBotIdentity(normalizedAppId); await this.#withBotTransition(identity.botId, async () => { if (this.#closed) throw new Error("QQ controller is closed"); const previousConfig = this.#configStore.getByAppId(normalizedAppId); const previousSecret = await this.#credentials.resolve(identity.secretRef).catch(() => void 0); if (this.#closed) throw new Error("QQ controller is closed"); const config = { botId: identity.botId, appId: normalizedAppId, secretRef: identity.secretRef, ownerUserOpenid: previousConfig?.ownerUserOpenid ?? "*", createdAt: previousConfig?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(), connectedAt: (/* @__PURE__ */ new Date()).toISOString() }; await this.#credentials.set(identity.secretRef, normalizedSecret); try { await this.#configStore.save(config); } catch (error) { await this.#restoreCredential(identity.secretRef, previousSecret); throw error; } try { await this.#startRuntime(config, normalizedSecret); this.#errors.delete(identity.botId); } catch (error) { this.#errors.set(identity.botId, safeError3("connection-failed", "QQ \u673A\u5668\u4EBA\u5DF2\u7ED1\u5B9A\uFF0C\u6D88\u606F\u8FDE\u63A5\u6682\u672A\u5C31\u7EEA\u3002")); this.#logger.warn?.(`[dsh-im:qq] bot ${identity.botId} credential connection failed:`, error); } this.#touch(); }); return this.status(); } async cancelProvisioning(attemptId) { const record = this.#attempts.get(attemptId); if (!record) return null; if (!TERMINAL_ATTEMPT_STATES2.has(record.state)) { record.controller.abort(); record.dispose?.(); await record.task?.catch(() => void 0); if (!TERMINAL_ATTEMPT_STATES2.has(record.state)) record.state = "cancelled"; record.error ??= safeError3("cancelled", "\u626B\u7801\u7ED1\u5B9A\u5DF2\u53D6\u6D88\u3002"); } if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; this.#touch(); return publicAttempt2(record); } async reconnectBot(botId) { const config = this.#configStore.get(botId); if (!config) throw new Error("Unknown QQ bot"); await this.#withBotTransition(botId, async () => { const secret = await this.#resolveSecret(config.secretRef); if (!secret) throw new Error("QQ bot secret is missing"); try { await this.#startRuntime(config, secret); this.#errors.delete(botId); } catch (error) { this.#errors.set(botId, safeError3("connection-failed", "QQ \u8FDE\u63A5\u4ECD\u672A\u5C31\u7EEA\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002")); throw error; } finally { this.#touch(); } }); return this.status(); } async deleteBot(botId) { const config = this.#configStore.get(botId); if (!config) throw new Error("Unknown QQ bot"); await this.#withBotTransition(botId, async () => { const previous = await this.#credentials.resolve(config.secretRef).catch(() => void 0); await this.#stopRuntime(botId); try { await this.#credentials.unset(config.secretRef); await this.#configStore.remove(botId); } catch (error) { if (previous?.value) { await this.#credentials.set(config.secretRef, previous.value).catch(() => void 0); await this.#startRuntime(config, previous.value).catch(() => void 0); } throw new Error("Unable to remove the QQ bot safely.", { cause: error }); } await this.#deleteState({ botId, config }).catch((error) => { this.#logger.warn?.(`[dsh-im:qq] bot ${botId} state cleanup failed:`, error); }); this.#errors.delete(botId); this.#touch(); }); return this.status(); } status() { const bots = this.#configStore.list().map((config) => { const runtimeStatus2 = this.#runtimes.get(config.botId)?.status ?? null; const connected = runtimeStatus2?.ready === true && runtimeStatus2.qqConnectionState === "connected" && runtimeStatus2.harnessReachable === true; const state = connected ? "connected" : runtimeStatus2?.qqConnectionState === "connecting" ? "connecting" : this.#errors.has(config.botId) || runtimeStatus2?.qqConnectionState === "failed" ? "error" : "offline"; return { botId: config.botId, state, connected, configured: true, bot: { name: "QQ\u673A\u5668\u4EBA", appIdMasked: maskQqAppId(config.appId) }, health: { status: connected ? "healthy" : state === "error" ? "error" : "offline", summary: connected ? "QQ WebSocket \u957F\u8FDE\u63A5\u8FD0\u884C\u6B63\u5E38" : state === "error" ? "QQ \u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u91CD\u8BD5" : "QQ \u8FDE\u63A5\u5F53\u524D\u79BB\u7EBF", lastCheckedAt: runtimeStatus2?.lastCheckedAt ?? null, lastConnectedAt: runtimeStatus2?.lastConnectedAt ?? null }, stats: { messagesReceived: runtimeStatus2?.messagesReceived ?? 0, messagesReplied: runtimeStatus2?.messagesReplied ?? 0 }, error: structuredClone(this.#errors.get(config.botId) ?? null) }; }); const connectedCount = bots.filter((bot) => bot.connected).length; const active = this.#activeAttemptId ? this.#attempts.get(this.#activeAttemptId) : null; return { schemaVersion: 1, revision: this.#revision, state: active && ACTIVE_ATTEMPT_STATES2.has(active.state) ? "provisioning" : bots.length === 0 ? "disconnected" : connectedCount === bots.length ? "connected" : connectedCount > 0 ? "degraded" : "offline", bots, totals: { configured: bots.length, connected: connectedCount }, ...active && ACTIVE_ATTEMPT_STATES2.has(active.state) ? { provisioning: publicAttempt2(active) } : {} }; } async close() { if (this.#closed) return; this.#closed = true; if (this.#activeAttemptId) await this.cancelProvisioning(this.#activeAttemptId); await Promise.allSettled([...this.#transitions.values()]); await Promise.allSettled([...this.#runtimes.keys()].map((botId) => this.#stopRuntime(botId))); } async #completeProvisioning(record, credentials) { try { record.state = "connecting"; record.verificationUrl = null; record.expiresAt = null; this.#touch(); const credential = Array.isArray(credentials) ? credentials[0] : null; const appId = cleanString9(credential?.appId); const appSecret = cleanString9(credential?.appSecret); const ownerUserOpenid = cleanString9(credential?.userOpenid); if (!appId || !appSecret || !ownerUserOpenid) { throw new Error("QQ authorization returned incomplete credentials"); } record.botId = await this.#activateBot(record, { appId, appSecret, ownerUserOpenid }); record.state = "connected"; record.error = null; } catch (error) { if (record.controller.signal.aborted) { record.state = "cancelled"; record.error = safeError3("cancelled", "\u626B\u7801\u7ED1\u5B9A\u5DF2\u53D6\u6D88\u3002"); } else { record.state = "failed"; record.error = safeError3("activation-failed", "QQ \u5DF2\u6388\u6743\uFF0C\u4F46\u65E0\u6CD5\u5B89\u5168\u4FDD\u5B58\u63A5\u5165\u914D\u7F6E\u3002"); this.#logger.error?.("[dsh-im:qq] provisioning failed:", error); } } finally { record.dispose?.(); if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; this.#touch(); } } async #activateBot(record, { appId, appSecret, ownerUserOpenid }) { const identity = deriveQqBotIdentity(appId); const previousConfig = this.#configStore.getByAppId(appId); const previousSecret = await this.#credentials.resolve(identity.secretRef).catch(() => void 0); const config = { botId: identity.botId, appId, secretRef: identity.secretRef, ownerUserOpenid, createdAt: previousConfig?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(), connectedAt: (/* @__PURE__ */ new Date()).toISOString() }; return this.#withBotTransition(identity.botId, async () => { await this.#credentials.set(identity.secretRef, appSecret); try { if (record.controller.signal.aborted) throw new DOMException("Cancelled", "AbortError"); await this.#configStore.save(config); } catch (error) { await this.#restoreCredential(identity.secretRef, previousSecret); throw error; } try { if (record.controller.signal.aborted) throw new DOMException("Cancelled", "AbortError"); await this.#startRuntime(config, appSecret); this.#errors.delete(identity.botId); } catch (error) { if (record.controller.signal.aborted) { await this.#stopRuntime(identity.botId); if (previousConfig) await this.#configStore.save(previousConfig).catch(() => void 0); else await this.#configStore.remove(identity.botId).catch(() => void 0); await this.#restoreCredential(identity.secretRef, previousSecret); throw error; } this.#errors.set(identity.botId, safeError3("connection-failed", "QQ \u673A\u5668\u4EBA\u5DF2\u7ED1\u5B9A\uFF0C\u6D88\u606F\u8FDE\u63A5\u6682\u672A\u5C31\u7EEA\u3002")); this.#logger.warn?.(`[dsh-im:qq] bot ${identity.botId} activation connection failed:`, error); } this.#touch(); return identity.botId; }); } async #startRuntime(config, appSecret) { if (this.#closed) throw new Error("QQ controller is closed"); await this.#stopRuntime(config.botId); if (this.#closed) throw new Error("QQ controller is closed"); const runtime = await this.#createRuntime({ botId: config.botId, config, appSecret }); if (!runtime || typeof runtime.start !== "function" || typeof runtime.stop !== "function") { throw new TypeError("createRuntime returned an invalid QQ runtime"); } this.#runtimes.set(config.botId, runtime); try { await runtime.start(); } catch (error) { await runtime.stop().catch(() => void 0); this.#runtimes.delete(config.botId); throw error; } } async #stopRuntime(botId) { const runtime = this.#runtimes.get(botId); this.#runtimes.delete(botId); await runtime?.stop().catch((error) => { this.#logger.warn?.(`[dsh-im:qq] bot ${botId} failed to stop cleanly:`, error); }); } async #resolveSecret(ref) { const result = await this.#credentials.resolve(ref).catch(() => void 0); return cleanString9(result?.value); } async #restoreCredential(ref, previous) { if (previous?.value) await this.#credentials.set(ref, previous.value).catch(() => void 0); else await this.#credentials.unset(ref).catch(() => void 0); } #withBotTransition(botId, operation) { const previous = this.#transitions.get(botId) ?? Promise.resolve(); const current = previous.catch(() => void 0).then(operation); const settled = current.finally(() => { if (this.#transitions.get(botId) === settled) this.#transitions.delete(botId); }); this.#transitions.set(botId, settled); return settled; } #touch() { this.#revision += 1; } }; // src/channels/qq/qq-runtime.mjs import { QQBot, typingIndicator } from "@tencent-connect/qqbot-nodejs"; // src/channels/qq/qq-bridge.mjs var HELP_TEXT3 = [ "QQ \u673A\u5668\u4EBA\u5DF2\u8FDE\u63A5 DeepSeek Harness\u3002", "", "\u76F4\u63A5\u53D1\u9001\u6587\u5B57\u5373\u53EF\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\u3002", "/new \u5F00\u542F\u4E00\u4E2A\u5168\u65B0\u4F1A\u8BDD", "/status \u68C0\u67E5\u8FDE\u63A5\u72B6\u6001", "/help \u663E\u793A\u672C\u5E2E\u52A9" ].join("\n"); function conversationKey3(message) { return `${message.kind}:${message.kind === "group" ? message.groupOpenid : message.senderId}`; } function safeText(message) { return typeof message?.content === "string" ? message.content.trim() : ""; } function createQqBridgeStatus() { return { messagesReceived: 0, messagesReplied: 0, messagesRejected: 0, lastMessageAt: null, lastReplyAt: null, lastRejectedAt: null, lastError: null }; } var QqHarnessBridge = class { #bot; #ownerUserOpenid; #harness; #state; #status; #logger; #replyTimeoutMs; #queues = /* @__PURE__ */ new Map(); constructor({ bot, ownerUserOpenid, harness, state, status = createQqBridgeStatus(), logger = console, replyTimeoutMs = 6e5 }) { if (!bot || typeof bot.sendText !== "function") throw new TypeError("QQ bot client is required"); if (!ownerUserOpenid) throw new TypeError("QQ scanner identity is required"); if (!harness || !state) throw new TypeError("Harness client and state store are required"); this.#bot = bot; this.#ownerUserOpenid = ownerUserOpenid; this.#harness = harness; this.#state = state; this.#status = status; this.#logger = logger; this.#replyTimeoutMs = replyTimeoutMs; } get status() { return structuredClone(this.#status); } accept(message) { const key = conversationKey3(message); const previous = this.#queues.get(key) ?? Promise.resolve(); const current = previous.catch(() => void 0).then(() => this.#process(message)).finally(() => { if (this.#queues.get(key) === current) this.#queues.delete(key); }); this.#queues.set(key, current); return current; } async waitForIdle() { await Promise.allSettled([...this.#queues.values()]); } async #process(message) { const messageId = typeof message?.messageId === "string" ? message.messageId : ""; const sender = typeof message?.senderId === "string" ? message.senderId : ""; if (!messageId || !sender || message.senderIsBot === true) return; if (!["c2c", "group"].includes(message.kind) || this.#state.hasSeen(messageId)) return; this.#status.messagesReceived += 1; this.#status.lastMessageAt = (/* @__PURE__ */ new Date()).toISOString(); if (this.#ownerUserOpenid !== "*" && sender !== this.#ownerUserOpenid) { this.#status.messagesRejected += 1; this.#status.lastRejectedAt = (/* @__PURE__ */ new Date()).toISOString(); return; } if (message.kind === "group" && message.rawEventType !== "GROUP_AT_MESSAGE_CREATE") return; const target = message.replyTarget; const text = safeText(message); try { if (!text) { await this.#bot.sendText(target, "\u76EE\u524D\u4EC5\u652F\u6301\u6587\u5B57\u6D88\u606F\u3002"); await this.#state.markSeen(messageId); return; } const command = text.toLowerCase(); if (command === "/help") { await this.#bot.sendText(target, HELP_TEXT3); await this.#state.markSeen(messageId); return; } if (command === "/status") { await this.#harness.ensureRunning(); await this.#bot.sendText(target, "QQ \u673A\u5668\u4EBA\u4E0E DeepSeek Harness \u8FDE\u63A5\u6B63\u5E38\u3002"); await this.#state.markSeen(messageId); return; } const key = conversationKey3(message); if (command === "/new") { await this.#state.clearSession(key); await this.#bot.sendText(target, "\u5DF2\u5F00\u542F\u65B0\u4F1A\u8BDD\u3002\u8BF7\u53D1\u9001\u4F60\u7684\u95EE\u9898\u3002"); await this.#state.markSeen(messageId); return; } let sessionId = this.#state.sessionFor(key); if (!sessionId || !await this.#harness.sessionExists(sessionId)) { sessionId = await this.#harness.createSession(); await this.#state.setSession(key, sessionId); } let stream = null; let streamFinished = false; if (message.kind === "c2c" && target?.msgId && typeof this.#bot.openStream === "function") { try { stream = this.#bot.openStream({ target }); } catch (error) { this.#logger.warn?.("[dsh-im:qq] unable to start a QQ stream; using a text reply:", error); } } const answer = await this.#harness.ask(sessionId, text, { timeoutMs: this.#replyTimeoutMs, onUpdate: stream ? async (update) => { const progress = update.type === "text" ? update.text : update.type === "tool" ? `\u6B63\u5728\u4F7F\u7528${update.name}\u2026` : update.text; if (progress) await stream.update(progress); } : void 0 }); if (stream) { try { await stream.update(answer); await stream.complete(); streamFinished = true; } catch (error) { stream.cancel?.(); this.#logger.warn?.("[dsh-im:qq] QQ stream finalization failed; using a text reply:", error); } } if (!streamFinished) await this.#bot.sendText(target, answer); await this.#state.markSeen(messageId); this.#status.messagesReplied += 1; this.#status.lastReplyAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.lastError = null; } catch (error) { this.#status.lastError = error?.message ?? String(error); this.#logger.error?.("[dsh-im:qq] failed to process an inbound message:", error); try { await this.#bot.sendText(target, "\u6D88\u606F\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"); await this.#state.markSeen(messageId); } catch (sendError) { this.#logger.error?.("[dsh-im:qq] failed to send the safe error reply:", sendError); } } } }; // src/channels/qq/qq-runtime.mjs function timeoutError() { const error = new Error("QQ WebSocket did not become ready in time"); error.code = "connect-timeout"; return error; } function createQqRuntimeStatus() { return { startedAt: null, ready: false, qqConnectionState: "idle", harnessReachable: false, lastCheckedAt: null, lastConnectedAt: null, lastError: null, ...createQqBridgeStatus() }; } var QqRuntime = class { #config; #appSecret; #harness; #state; #logger; #replyTimeoutMs; #connectTimeoutMs; #createBot; #typingMiddleware; #status = createQqRuntimeStatus(); #bot = null; #bridge = null; #abortController = null; #runTask = null; #starting = null; constructor({ config, appSecret, harness, state, logger = console, replyTimeoutMs = 6e5, connectTimeoutMs = 2e4, createBot = (options) => new QQBot(options), typingMiddleware = typingIndicator }) { if (!config || !appSecret || !harness || !state) { throw new TypeError("QqRuntime requires config, app secret, Harness, and state"); } this.#config = config; this.#appSecret = appSecret; this.#harness = harness; this.#state = state; this.#logger = logger; this.#replyTimeoutMs = replyTimeoutMs; this.#connectTimeoutMs = connectTimeoutMs; this.#createBot = createBot; this.#typingMiddleware = typingMiddleware; } get status() { return structuredClone(this.#status); } async start() { if (this.#status.ready && this.#bot) return this.status; if (this.#starting) return this.#starting; this.#starting = this.#start().finally(() => { this.#starting = null; }); return this.#starting; } async #start() { await this.stop(); this.#status.startedAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.qqConnectionState = "connecting"; this.#status.lastError = null; await this.#harness.ensureRunning(); this.#status.harnessReachable = true; const sdkLogger = { error: (...args) => this.#logger.error?.(...args), warn: (...args) => this.#logger.warn?.(...args), info: (...args) => this.#logger.info?.(...args), debug: () => { } }; const bot = this.#createBot({ appId: this.#config.appId, appSecret: this.#appSecret, accountId: this.#config.botId, logger: sdkLogger, transport: "websocket", tokenPrefetch: "sync" }); if (!bot || typeof bot.start !== "function" || typeof bot.stop !== "function") { throw new TypeError("QQ bot factory returned an invalid client"); } this.#bot = bot; this.#bridge = new QqHarnessBridge({ bot, ownerUserOpenid: this.#config.ownerUserOpenid, harness: this.#harness, state: this.#state, status: this.#status, logger: this.#logger, replyTimeoutMs: this.#replyTimeoutMs }); bot.use?.(this.#typingMiddleware({ keepAlive: true, predicate: (ctx) => this.#config.ownerUserOpenid === "*" || ctx?.message?.senderId === this.#config.ownerUserOpenid })); const controller = new AbortController(); this.#abortController = controller; let readyResolve; let readyReject; const ready = new Promise((resolve7, reject) => { readyResolve = resolve7; readyReject = reject; }); const onReady = () => { const now = Date.now(); this.#status.ready = true; this.#status.qqConnectionState = "connected"; this.#status.lastCheckedAt = now; this.#status.lastConnectedAt = now; this.#status.lastError = null; readyResolve(); }; const onError = (error) => { if (!this.#status.ready) readyReject(error); else { this.#status.lastError = error?.message ?? String(error); this.#logger.warn?.(`[dsh-im:qq] bot ${this.#config.botId} connection error:`, error); } }; const onMessage = (_ctx, message) => this.#bridge?.accept(message); bot.on("ready", onReady); bot.on("resumed", onReady); bot.on("error", onError); bot.on("message", onMessage); const runTask = Promise.resolve().then(() => bot.start(controller.signal)); this.#runTask = runTask; runTask.catch((error) => { if (controller.signal.aborted) return; readyReject(error); this.#status.ready = false; this.#status.qqConnectionState = "failed"; this.#status.lastError = error?.message ?? String(error); this.#logger.error?.(`[dsh-im:qq] bot ${this.#config.botId} connection stopped:`, error); }); let timer; try { await Promise.race([ ready, new Promise((_, reject) => { timer = setTimeout(() => reject(timeoutError()), this.#connectTimeoutMs); }) ]); this.#status.ready = true; this.#status.qqConnectionState = "connected"; this.#status.lastCheckedAt = Date.now(); this.#status.lastConnectedAt = Date.now(); return this.status; } catch (error) { this.#status.ready = false; this.#status.qqConnectionState = "failed"; this.#status.lastError = error?.message ?? String(error); await this.stop(); throw error; } finally { clearTimeout(timer); } } async stop() { const bot = this.#bot; const bridge = this.#bridge; const runTask = this.#runTask; this.#abortController?.abort(); this.#abortController = null; this.#bot = null; this.#bridge = null; this.#runTask = null; try { bot?.stop(); } catch (error) { this.#logger.warn?.(`[dsh-im:qq] bot ${this.#config.botId} failed to stop cleanly:`, error); } await Promise.race([ runTask?.catch(() => void 0) ?? Promise.resolve(), new Promise((resolve7) => setTimeout(resolve7, 2e3)) ]); await bridge?.waitForIdle(); this.#status.ready = false; this.#status.qqConnectionState = "idle"; return this.status; } }; // src/channels/qq/qr-auth.mjs import { startQrConnect } from "@tencent-connect/qqbot-connector"; var QqQrAuth = class { #start; #source; constructor({ start = startQrConnect, source = "deepseek-harness" } = {}) { if (typeof start !== "function") throw new TypeError("QQ QR connector is required"); this.#start = start; this.#source = source; } start(callbacks, { signal } = {}) { if (!callbacks || typeof callbacks.onSuccess !== "function" || typeof callbacks.onFailure !== "function") { throw new TypeError("QQ QR callbacks are required"); } return this.#start(callbacks, { displayQrCodeToConsole: false, source: this.#source, signal }); } }; // src/channels/qq/state-store.mjs import { mkdir as mkdir8, readFile as readFile8, rename as rename8, unlink as unlink10, writeFile as writeFile8 } from "node:fs/promises"; import { dirname as dirname8 } from "node:path"; var EMPTY_STATE4 = Object.freeze({ version: 1, sessions: {}, seenMessageIds: [] }); function normalizeState3(value) { if (!value || typeof value !== "object") return structuredClone(EMPTY_STATE4); const sessions = {}; if (value.sessions && typeof value.sessions === "object" && !Array.isArray(value.sessions)) { for (const [key, sessionId] of Object.entries(value.sessions)) { if (typeof key === "string" && typeof sessionId === "string" && sessionId) { sessions[key] = sessionId; } } } return { version: 1, sessions, seenMessageIds: Array.isArray(value.seenMessageIds) ? value.seenMessageIds.filter((id) => typeof id === "string").slice(-1e3) : [] }; } var QqStateStore = class { #path; #state = structuredClone(EMPTY_STATE4); #writeQueue = Promise.resolve(); constructor(path) { this.#path = path; } async load() { try { this.#state = normalizeState3(JSON.parse(await readFile8(this.#path, "utf8"))); } catch (error) { if (error?.code !== "ENOENT") throw error; this.#state = structuredClone(EMPTY_STATE4); await this.#persist(); } return this; } sessionFor(key) { return this.#state.sessions[key] ?? null; } async setSession(key, sessionId) { this.#state.sessions[key] = sessionId; await this.#persist(); } async clearSession(key) { delete this.#state.sessions[key]; await this.#persist(); } hasSeen(messageId) { return this.#state.seenMessageIds.includes(messageId); } async markSeen(messageId) { if (this.hasSeen(messageId)) return; this.#state.seenMessageIds.push(messageId); if (this.#state.seenMessageIds.length > 1e3) { this.#state.seenMessageIds.splice(0, this.#state.seenMessageIds.length - 1e3); } await this.#persist(); } snapshot() { return structuredClone(this.#state); } async remove() { try { await unlink10(this.#path); } catch (error) { if (error?.code !== "ENOENT") throw error; } this.#state = structuredClone(EMPTY_STATE4); } async #persist() { const snapshot = `${JSON.stringify(this.#state, null, 2)} `; const operation = this.#writeQueue.then(async () => { await mkdir8(dirname8(this.#path), { recursive: true, mode: 448 }); const temporary = `${this.#path}.tmp`; await writeFile8(temporary, snapshot, { encoding: "utf8", mode: 384 }); await rename8(temporary, this.#path); }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; } }; // plugin-src/host/channels/qq/connection-supervisor.mjs var DEFAULT_RETRY_DELAYS_MS4 = Object.freeze([250, 1e3, 3e3, 5e3, 1e4, 3e4]); function retryDelays3(value) { if (!Array.isArray(value) || value.length === 0) return [...DEFAULT_RETRY_DELAYS_MS4]; const valid = value.filter((delay3) => Number.isFinite(delay3) && delay3 >= 0); return valid.length > 0 ? valid : [...DEFAULT_RETRY_DELAYS_MS4]; } var ConnectionSupervisor3 = class { #controller; #harness; #logger; #retryDelays; #healthyIntervalMs; #setTimeout; #clearTimeout; #timer = null; #running = null; #retryIndex = 0; #closed = false; #started = false; #ready; #resolveReady; constructor({ controller, harness, logger = console, retryDelaysMs, healthyIntervalMs = 15e3, setTimeoutImpl = setTimeout, clearTimeoutImpl = clearTimeout }) { if (!controller || typeof controller.initialize !== "function" || typeof controller.status !== "function") { throw new TypeError("ConnectionSupervisor requires a controller"); } if (!harness || typeof harness.ensureRunning !== "function") { throw new TypeError("ConnectionSupervisor requires a Harness client"); } this.#controller = controller; this.#harness = harness; this.#logger = logger; this.#retryDelays = retryDelays3(retryDelaysMs); this.#healthyIntervalMs = Number.isFinite(healthyIntervalMs) && healthyIntervalMs >= 0 ? healthyIntervalMs : 15e3; this.#setTimeout = setTimeoutImpl; this.#clearTimeout = clearTimeoutImpl; this.#ready = new Promise((resolve7) => { this.#resolveReady = resolve7; }); } get ready() { return this.#ready; } start() { if (this.#started || this.#closed) return this; this.#started = true; this.#schedule(0); return this; } async close() { if (this.#closed) return; this.#closed = true; if (this.#timer !== null) this.#clearTimeout(this.#timer); this.#timer = null; await this.#running?.catch(() => void 0); this.#resolveReady?.(null); this.#resolveReady = null; } #schedule(delayMs) { if (this.#closed) return; this.#timer = this.#setTimeout(() => { this.#timer = null; void this.#run(); }, delayMs); this.#timer?.unref?.(); } async #run() { if (this.#closed || this.#running) return; const operation = this.#reconcile(); this.#running = operation; try { await operation; } finally { if (this.#running === operation) this.#running = null; } } async #reconcile() { try { await this.#harness.ensureRunning(); if (this.#closed) return; const status = await this.#controller.initialize(); if (this.#closed) return; this.#resolveReady?.(status); this.#resolveReady = null; const { configured, connected } = status.totals; if (connected < configured) { const delay3 = this.#retryDelays[Math.min(this.#retryIndex, this.#retryDelays.length - 1)]; this.#retryIndex += 1; this.#logger.warn?.(`[dsh-im:qq] ${connected}/${configured} bots connected; retrying in ${delay3}ms`); this.#schedule(delay3); return; } this.#retryIndex = 0; this.#schedule(this.#healthyIntervalMs); } catch (error) { if (this.#closed) return; const delay3 = this.#retryDelays[Math.min(this.#retryIndex, this.#retryDelays.length - 1)]; this.#retryIndex += 1; this.#logger.warn?.(`[dsh-im:qq] connection reconciliation failed; retrying in ${delay3}ms`, error); this.#schedule(delay3); } } }; function createConnectionSupervisor3(options) { return new ConnectionSupervisor3(options); } // plugin-src/host/channels/qq/production.mjs function harnessOrigin4(webServer, configured) { if (configured !== void 0) return new URL(configured); const port = webServer?.port; if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error("dsh-im QQ requires an initialized DSH webServer port"); } return new URL(`http://127.0.0.1:${port}`); } function pluginPaths4(config) { const dshHome = resolve4(config.dshHome ?? process.env.DSH_HOME ?? join4(homedir4(), ".dsh")); const root = resolve4(config.dataDir ?? join4(dshHome, "integrations", "dsh-qq")); return { config: resolve4(config.configPath ?? join4(root, "config.json")), bots: resolve4(config.botsDir ?? join4(root, "bots")) }; } async function createProductionController4(ctx, config = {}, internals = {}) { if (!ctx?.credentials) throw new TypeError("dsh-im QQ requires ctx.credentials"); if (!ctx?.webServer) throw new TypeError("dsh-im QQ requires ctx.webServer"); const ConfigStore = internals.ConfigStore ?? QqConfigStore; const StateStore2 = internals.StateStore ?? QqStateStore; const Harness = internals.HarnessClient ?? QqHarnessClient; const Controller = internals.Controller ?? QqController; const Runtime = internals.Runtime ?? QqRuntime; const QrAuth = internals.QrAuth ?? QqQrAuth; const createSupervisor = internals.createConnectionSupervisor ?? createConnectionSupervisor3; const logger = typeof ctx.logger === "function" ? ctx.logger("dsh-im:qq") : ctx.logger ?? console; const paths = pluginPaths4(config); const configStore = await new ConfigStore(paths.config).load(); const qrAuth = internals.qrAuth ?? new QrAuth({ source: config.qrSource ?? "deepseek-harness" }); const stateStores = /* @__PURE__ */ new Map(); const statePath = (botId) => resolve4(paths.bots, botId, "state.json"); const stateFor = async (botId) => { let state = stateStores.get(botId); if (!state) { state = await new StateStore2(statePath(botId)).load(); stateStores.set(botId, state); } return state; }; const harness = new Harness({ baseUrl: harnessOrigin4(ctx.webServer, config.harnessBaseUrl), workspace: resolve4(config.workspace ?? process.cwd()), agentPreset: config.agentPreset ?? "standard", autostart: false, dshBin: config.dshBin ?? "dsh" }); const controller = new Controller({ qrAuth, credentials: ctx.credentials, configStore, logger, createRuntime: async ({ botId, config: botConfig, appSecret }) => new Runtime({ config: botConfig, appSecret, harness, state: await stateFor(botId), replyTimeoutMs: config.replyTimeoutMs ?? 6e5, connectTimeoutMs: config.connectTimeoutMs ?? 2e4, logger: { error: (...args) => logger.error?.(`[${botId}]`, ...args), warn: (...args) => logger.warn?.(`[${botId}]`, ...args), info: (...args) => logger.info?.(`[${botId}]`, ...args), debug: (...args) => logger.debug?.(`[${botId}]`, ...args) } }), deleteState: async ({ botId }) => { const state = stateStores.get(botId); stateStores.delete(botId); if (state && typeof state.remove === "function") return state.remove(); try { await unlink11(statePath(botId)); } catch (error) { if (error?.code !== "ENOENT") throw error; } } }); const supervisor = createSupervisor({ controller, harness, logger, retryDelaysMs: config.retryDelaysMs, healthyIntervalMs: config.healthyIntervalMs }).start(); return { controller, ready: supervisor.ready, async close() { await supervisor.close(); await controller.close(); harness.stopManagedProcess(); } }; } // plugin-src/host/channels/qq/rpc.mjs import QRCode3 from "qrcode"; var QQ_RPC_CHANNEL = "/qq"; var QQ_ENDPOINTS = Object.freeze({ status: "connection.status", beginProvisioning: "provision.begin", pollProvisioning: "provision.poll", cancelProvisioning: "provision.cancel", bindCredentials: "bot.bind-credentials", reconnectBot: "bot.reconnect", deleteBot: "bot.delete" }); var QQ_RPC_ENDPOINTS = Object.freeze(Object.values(QQ_ENDPOINTS)); var FORBIDDEN_PUBLIC_KEYS3 = /* @__PURE__ */ new Set([ "appSecret", "app_secret", "secretRef", "ownerUserOpenid", "userOpenid", "verificationUrl" ]); function isRecord3(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function exactKeys3(value, allowed) { return isRecord3(value) && Object.keys(value).every((key) => allowed.includes(key)); } function validId3(value) { return typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value); } function validCredential3(value, maxLength) { return typeof value === "string" && value.trim().length > 0 && value.length <= maxLength; } function payloadFailure3(endpoint, payload) { if (!isRecord3(payload)) return "Payload must be an object."; if (endpoint === QQ_ENDPOINTS.status) return exactKeys3(payload, []) ? null : "connection.status does not accept fields."; if (endpoint === QQ_ENDPOINTS.beginProvisioning) { return exactKeys3(payload, ["locale"]) && (payload.locale === void 0 || payload.locale === "zh-CN") ? null : "provision.begin received unsupported fields."; } if ([QQ_ENDPOINTS.pollProvisioning, QQ_ENDPOINTS.cancelProvisioning].includes(endpoint)) { return exactKeys3(payload, ["attemptId"]) && validId3(payload.attemptId) ? null : `${endpoint} requires an attemptId.`; } if (endpoint === QQ_ENDPOINTS.bindCredentials) { return exactKeys3(payload, ["appId", "appSecret"]) && validCredential3(payload.appId, 256) && validCredential3(payload.appSecret, 1024) ? null : "bot.bind-credentials requires AppID and AppSecret."; } if (endpoint === QQ_ENDPOINTS.reconnectBot) { return exactKeys3(payload, ["botId"]) && validId3(payload.botId) ? null : "bot.reconnect requires a botId."; } if (endpoint === QQ_ENDPOINTS.deleteBot) { return exactKeys3(payload, ["botId", "confirm"]) && validId3(payload.botId) && payload.confirm === true ? null : "bot.delete requires a botId and confirm=true."; } return "Unknown QQ endpoint."; } function sanitizePublic3(value) { if (Array.isArray(value)) return value.map(sanitizePublic3); if (!isRecord3(value)) return value; const safe = {}; for (const [key, child] of Object.entries(value)) { if (!FORBIDDEN_PUBLIC_KEYS3.has(key)) safe[key] = sanitizePublic3(child); } return safe; } async function qrDataUrl2(value) { return QRCode3.toDataURL(value, { type: "image/png", errorCorrectionLevel: "M", margin: 2, width: 320 }); } async function withEncodedQr2(value, encodeQr) { if (!value || typeof value.verificationUrl !== "string") return sanitizePublic3(value); return sanitizePublic3({ ...value, qrCodeDataUrl: await encodeQr(value.verificationUrl) }); } async function publicStatus2(status, encodeQr) { const value = structuredClone(status); if (value?.provisioning) value.provisioning = await withEncodedQr2(value.provisioning, encodeQr); return sanitizePublic3(value); } function createQqRpcHandler(controller, { encodeQr = qrDataUrl2 } = {}) { for (const method of ["status", "startProvisioning", "registrationStatus", "cancelProvisioning", "bindCredentials", "reconnectBot", "deleteBot"]) { if (typeof controller?.[method] !== "function") throw new TypeError(`A complete QQ controller is required (${method})`); } const qrCache = /* @__PURE__ */ new Map(); const cachedEncode = (url) => { let encoded = qrCache.get(url); if (!encoded) { if (qrCache.size >= 16) qrCache.delete(qrCache.keys().next().value); encoded = Promise.resolve().then(() => encodeQr(url)); qrCache.set(url, encoded); } return encoded; }; return async (endpoint, payload, signal) => { if (signal?.aborted) return { ok: false, error: { code: "cancelled", message: "The request was cancelled." } }; if (!QQ_RPC_ENDPOINTS.includes(endpoint)) return { ok: false, error: { code: "bad-request", message: "Unknown QQ endpoint." } }; const invalid = payloadFailure3(endpoint, payload); if (invalid) return { ok: false, error: { code: "bad-request", message: invalid } }; try { let value; if (endpoint === QQ_ENDPOINTS.status) value = await publicStatus2(await controller.status(), cachedEncode); else if (endpoint === QQ_ENDPOINTS.beginProvisioning) value = await withEncodedQr2(await controller.startProvisioning(), cachedEncode); else if (endpoint === QQ_ENDPOINTS.pollProvisioning) { const current = await controller.registrationStatus(payload.attemptId); if (!current) return { ok: false, error: { code: "bad-request", message: "The provisioning attempt no longer exists." } }; value = await withEncodedQr2(current, cachedEncode); } else if (endpoint === QQ_ENDPOINTS.cancelProvisioning) { value = sanitizePublic3(await controller.cancelProvisioning(payload.attemptId)); } else if (endpoint === QQ_ENDPOINTS.bindCredentials) { value = await publicStatus2(await controller.bindCredentials(payload), cachedEncode); } else if (endpoint === QQ_ENDPOINTS.reconnectBot) { value = await publicStatus2(await controller.reconnectBot(payload.botId), cachedEncode); } else { value = await publicStatus2(await controller.deleteBot(payload.botId), cachedEncode); } return signal?.aborted ? { ok: false, error: { code: "cancelled", message: "The request was cancelled." } } : { ok: true, value }; } catch { return signal?.aborted ? { ok: false, error: { code: "cancelled", message: "The request was cancelled." } } : { ok: false, error: { code: "qq-operation-failed", message: "QQ \u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" } }; } }; } function installQqRpc(ctx, controller, options, authority) { if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== "function") { throw new TypeError("DSH Host Connection RPC is required"); } return ctx.connection.rpc.handle( QQ_RPC_CHANNEL, createQqRpcHandler(controller, options), { authority: resolveRpcAuthority(authority) } ); } // plugin-src/host/channels/qq/index.mjs async function apply4(ctx, config = {}) { if (config?.controller) { return installQqRpc(ctx, config.controller, config.rpcOptions, config.rpcAuthority); } const production = await createProductionController4(ctx, config, config.internals); const disposeRpc = installQqRpc( ctx, production.controller, config.rpcOptions, config.rpcAuthority ); ctx.effect(() => async () => production.close(), "dsh-im: close QQ bot connections"); return disposeRpc; } // src/channels/telegram/config-store.mjs var IDENTITY_OPTIONS2 = Object.freeze({ botPrefix: "telegram", tokenRefPrefix: "DSH_TELEGRAM_BOT_TOKEN" }); function deriveTelegramBotIdentity(platformId) { return deriveTokenBotIdentity(platformId, IDENTITY_OPTIONS2); } function maskTelegramBotId(platformId) { return maskPlatformId(platformId, "Telegram\u673A\u5668\u4EBA"); } var TelegramConfigStore = class extends TokenBotConfigStore { constructor(path) { super(path, { channel: "Telegram", ...IDENTITY_OPTIONS2 }); } }; // src/channels/telegram/harness-client.mjs var TelegramHarnessClient = class extends HarnessClient2 { }; // src/channels/telegram/telegram-api.mjs var DEFAULT_BASE_URL2 = "https://api.telegram.org/"; function cleanString10(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function requestSignal2(signal, timeoutMs) { const timeout = AbortSignal.timeout(timeoutMs); return signal ? AbortSignal.any([signal, timeout]) : timeout; } function validTelegramToken(value) { return typeof value === "string" && /^\d{5,20}:[A-Za-z0-9_-]{20,}$/.test(value.trim()); } var TelegramApi = class { #token; #fetch; #baseUrl; constructor({ token, fetchImpl = fetch, baseUrl = DEFAULT_BASE_URL2 }) { if (!validTelegramToken(token)) throw new TypeError("Telegram Bot Token is invalid"); if (typeof fetchImpl !== "function") throw new TypeError("TelegramApi requires fetch"); this.#token = token.trim(); this.#fetch = fetchImpl; this.#baseUrl = new URL(baseUrl); } async getMe(options = {}) { return this.#call("getMe", {}, options); } async getWebhookInfo(options = {}) { return this.#call("getWebhookInfo", {}, options); } async getUpdates({ offset, timeout = 25, signal } = {}) { const payload = { timeout, limit: 100, allowed_updates: ["message"], ...Number.isSafeInteger(offset) ? { offset } : {} }; return this.#call("getUpdates", payload, { signal, timeoutMs: Math.max(1e4, (timeout + 10) * 1e3) }); } async sendMessage({ chatId, text, replyToMessageId, messageThreadId, signal }) { return this.#call("sendMessage", { chat_id: chatId, text, link_preview_options: { is_disabled: true }, ...replyToMessageId ? { reply_parameters: { message_id: replyToMessageId, allow_sending_without_reply: true } } : {}, ...messageThreadId ? { message_thread_id: messageThreadId } : {} }, { signal }); } async editMessageText({ chatId, messageId, text, signal }) { return this.#call("editMessageText", { chat_id: chatId, message_id: messageId, text, link_preview_options: { is_disabled: true } }, { signal }); } async sendChatAction({ chatId, messageThreadId, signal }) { return this.#call("sendChatAction", { chat_id: chatId, action: "typing", ...messageThreadId ? { message_thread_id: messageThreadId } : {} }, { signal }); } async #call(method, payload, { signal, timeoutMs = 15e3 } = {}) { const url = new URL(this.#baseUrl); url.pathname = `${url.pathname.replace(/\/$/, "")}/bot${this.#token}/${method}`; let response; try { response = await this.#fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload), signal: requestSignal2(signal, timeoutMs), redirect: "error" }); } catch (error) { if (error?.name === "AbortError" || error?.name === "TimeoutError") throw error; throw new Error(`Telegram ${method} transport failed`); } let body; try { body = await response.json(); } catch { throw new Error(`Telegram ${method} returned invalid JSON`); } if (!response.ok || body?.ok !== true) { const description = cleanString10(body?.description); const error = new Error(description ?? `Telegram ${method} failed`); error.code = Number.isInteger(body?.error_code) ? `telegram-${body.error_code}` : "telegram-api-error"; throw error; } return body.result; } }; async function inspectTelegramToken(token, options = {}) { const api = new TelegramApi({ token, ...options }); const bot = await api.getMe(); if (!bot?.id || bot?.is_bot !== true) throw new Error("Telegram token does not belong to a bot"); return { platformId: String(bot.id), name: cleanString10([bot.first_name, bot.last_name].filter(Boolean).join(" ")) ?? "Telegram\u673A\u5668\u4EBA", username: cleanString10(bot.username) }; } // src/channels/telegram/telegram-bridge.mjs var TELEGRAM_DESCRIPTOR = Object.freeze({ key: "telegram", label: "Telegram", connectionLabel: " Bot API \u957F\u8F6E\u8BE2" }); var TelegramHarnessBridge = class extends TextHarnessBridge { constructor(options) { super({ descriptor: TELEGRAM_DESCRIPTOR, ...options }); } }; // src/channels/telegram/telegram-controller.mjs var TelegramController = class extends TokenBotController { constructor(options) { super({ ...options, descriptor: TELEGRAM_DESCRIPTOR, inspectToken: options.inspectToken ?? inspectTelegramToken, deriveIdentity: deriveTelegramBotIdentity, maskPlatformId: maskTelegramBotId }); } }; // src/channels/telegram/telegram-runtime.mjs function escaped(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function mentionedUsername(message, username) { if (!username || typeof message?.text !== "string" || !Array.isArray(message.entities)) return false; return message.entities.some((entity) => { if (entity?.type !== "mention" || !Number.isInteger(entity.offset) || !Number.isInteger(entity.length)) { return false; } return message.text.slice(entity.offset, entity.offset + entity.length).toLowerCase() === `@${username.toLowerCase()}`; }); } function withoutBotMention(text, username) { if (!username || typeof text !== "string") return text; return text.replace(new RegExp(`@${escaped(username)}\\b`, "ig"), "").trim(); } function normalizeTelegramUpdate(update, { botId, username }) { const message = update?.message; const chatId = message?.chat?.id; const senderId = message?.from?.id; const messageId = message?.message_id; if (!Number.isSafeInteger(update?.update_id) || chatId === void 0 || senderId === void 0 || !Number.isSafeInteger(messageId)) return null; if (!["private", "group", "supergroup"].includes(message.chat?.type)) return null; const direct = message.chat.type === "private"; const addressed = direct || String(message.reply_to_message?.from?.id ?? "") === String(botId) || mentionedUsername(message, username); return { messageId: String(update.update_id), senderId: String(senderId), senderIsBot: message.from?.is_bot === true, kind: direct ? "direct" : "group", conversationId: String(chatId), content: withoutBotMention(message.text ?? message.caption ?? "", username), addressed, replyTarget: { chatId, replyToMessageId: messageId, messageThreadId: Number.isSafeInteger(message.message_thread_id) ? message.message_thread_id : void 0 } }; } var TelegramBotClient = class { #api; #signal; constructor({ api, signal }) { this.#api = api; this.#signal = signal; } async sendText(target, text) { const chunks = splitMessageText(text, 4e3); let result = null; for (const [index, chunk] of chunks.entries()) { result = await this.#api.sendMessage({ chatId: target.chatId, text: chunk, replyToMessageId: index === 0 ? target.replyToMessageId : void 0, messageThreadId: target.messageThreadId, signal: this.#signal }); } return result; } sendTyping(target) { return this.#api.sendChatAction({ chatId: target.chatId, messageThreadId: target.messageThreadId, signal: this.#signal }); } async openStream(target) { const stream = createEditableMessageStream({ limit: 4e3, create: async (text) => { const message = await this.#api.sendMessage({ chatId: target.chatId, text, replyToMessageId: target.replyToMessageId, messageThreadId: target.messageThreadId, signal: this.#signal }); return message.message_id; }, edit: (messageId, text) => this.#api.editMessageText({ chatId: target.chatId, messageId, text, signal: this.#signal }), sendRemainder: (text) => this.#api.sendMessage({ chatId: target.chatId, text, messageThreadId: target.messageThreadId, signal: this.#signal }) }); return stream.start(); } }; function createTelegramRuntimeStatus() { return { startedAt: null, ready: false, connectionState: "idle", harnessReachable: false, lastCheckedAt: null, lastConnectedAt: null, lastError: null, ...createTextBridgeStatus() }; } var TelegramRuntime = class { #config; #token; #harness; #state; #logger; #replyTimeoutMs; #createApi; #status = createTelegramRuntimeStatus(); #api = null; #bridge = null; #abortController = null; #pollTask = null; #starting = null; constructor({ config, token, harness, state, logger = console, replyTimeoutMs = 6e5, createApi = (options) => new TelegramApi(options) }) { if (!config || !token || !harness || !state) { throw new TypeError("TelegramRuntime requires config, token, Harness, and state"); } this.#config = config; this.#token = token; this.#harness = harness; this.#state = state; this.#logger = logger; this.#replyTimeoutMs = replyTimeoutMs; this.#createApi = createApi; } get status() { return structuredClone(this.#status); } async start() { if (this.#status.ready && this.#pollTask) return this.status; if (this.#starting) return this.#starting; this.#starting = this.#start().finally(() => { this.#starting = null; }); return this.#starting; } async #start() { await this.stop(); this.#status.startedAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.connectionState = "connecting"; this.#status.lastError = null; await this.#harness.ensureRunning(); this.#status.harnessReachable = true; const controller = new AbortController(); this.#abortController = controller; const api = this.#createApi({ token: this.#token }); this.#api = api; try { const bot = await api.getMe({ signal: controller.signal }); if (String(bot?.id ?? "") !== this.#config.platformId || bot?.is_bot !== true) { throw new Error("Telegram token identity does not match the saved bot"); } const webhook = await api.getWebhookInfo({ signal: controller.signal }); if (typeof webhook?.url === "string" && webhook.url) { const error = new Error("\u8BE5 Telegram \u673A\u5668\u4EBA\u5DF2\u914D\u7F6E Webhook\uFF0C\u8BF7\u5148\u5728\u539F\u670D\u52A1\u4E2D\u79FB\u9664 Webhook \u540E\u91CD\u8BD5\u3002"); error.code = "webhook-configured"; throw error; } const client = new TelegramBotClient({ api, signal: controller.signal }); this.#bridge = new TelegramHarnessBridge({ bot: client, harness: this.#harness, state: this.#state, status: this.#status, logger: this.#logger, replyTimeoutMs: this.#replyTimeoutMs }); let cursor = this.#state.cursor(); if (cursor === null) { const latest = await api.getUpdates({ offset: -1, timeout: 0, signal: controller.signal }); cursor = latest.length > 0 ? latest.at(-1).update_id + 1 : 0; await this.#state.setCursor(cursor); } const now = Date.now(); this.#status.ready = true; this.#status.connectionState = "connected"; this.#status.lastCheckedAt = now; this.#status.lastConnectedAt = now; this.#pollTask = this.#poll(cursor, controller.signal); this.#pollTask.catch((error) => { if (controller.signal.aborted) return; this.#status.ready = false; this.#status.connectionState = "failed"; this.#status.lastError = error?.message ?? String(error); this.#logger.error?.(`[dsh-im:telegram] bot ${this.#config.botId} polling stopped:`, error); }); return this.status; } catch (error) { this.#status.ready = false; this.#status.connectionState = "failed"; this.#status.lastError = error?.message ?? String(error); await this.stop(); throw error; } } async #poll(initialCursor, signal) { let cursor = initialCursor; while (!signal.aborted) { const updates = await this.#api.getUpdates({ offset: cursor, timeout: 25, signal }); this.#status.lastCheckedAt = Date.now(); for (const update of updates) { if (signal.aborted) return; const message = normalizeTelegramUpdate(update, { botId: this.#config.platformId, username: this.#config.username }); if (message) await this.#bridge.accept(message); cursor = update.update_id + 1; await this.#state.setCursor(cursor); } } } async stop() { const pollTask = this.#pollTask; const bridge = this.#bridge; this.#abortController?.abort(); this.#abortController = null; this.#pollTask = null; this.#api = null; this.#bridge = null; await Promise.race([ pollTask?.catch(() => void 0) ?? Promise.resolve(), new Promise((resolve7) => setTimeout(resolve7, 2e3)) ]); await Promise.race([ bridge?.waitForIdle() ?? Promise.resolve(), new Promise((resolve7) => setTimeout(resolve7, 2e3)) ]); this.#status.ready = false; this.#status.connectionState = "idle"; return this.status; } }; // src/channels/telegram/state-store.mjs var TelegramStateStore = class extends ConversationStateStore { }; // plugin-src/host/channels/telegram/production.mjs function createProductionController5(ctx, config = {}, internals = {}) { return createTokenProductionController(ctx, config, internals, { channel: "telegram", ConfigStore: TelegramConfigStore, StateStore: TelegramStateStore, HarnessClient: TelegramHarnessClient, Controller: TelegramController, Runtime: TelegramRuntime }); } // plugin-src/host/channels/telegram/rpc.mjs var TELEGRAM_RPC_CHANNEL = "/telegram"; var TELEGRAM_ENDPOINTS = TOKEN_BOT_ENDPOINTS; var TELEGRAM_RPC_ENDPOINTS = Object.freeze(Object.values(TELEGRAM_ENDPOINTS)); function installTelegramRpc(ctx, controller, authority) { return installTokenBotRpc(ctx, controller, { channel: "Telegram", rpcChannel: TELEGRAM_RPC_CHANNEL, authority }); } // plugin-src/host/channels/telegram/index.mjs async function apply5(ctx, config = {}) { if (config?.controller) { return installTelegramRpc(ctx, config.controller, config.rpcAuthority); } const production = await createProductionController5(ctx, config, config.internals ?? {}); const disposeRpc = installTelegramRpc(ctx, production.controller, config.rpcAuthority); ctx.effect(() => async () => production.close(), "dsh-im: close Telegram bot connections"); return disposeRpc; } // plugin-src/host/channels/wecom/production.mjs import { unlink as unlink14 } from "node:fs/promises"; import { homedir as homedir5 } from "node:os"; import { join as join5, resolve as resolve5 } from "node:path"; // src/channels/wecom/config-store.mjs import { createHash as createHash5 } from "node:crypto"; import { mkdir as mkdir9, readFile as readFile9, rename as rename9, unlink as unlink12, writeFile as writeFile9 } from "node:fs/promises"; import { dirname as dirname9 } from "node:path"; var EMPTY_DOCUMENT4 = Object.freeze({ version: 1, bots: Object.freeze([]) }); function cleanString11(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function safeIntegrationId(value) { const id = cleanString11(value); return id && /^wecom_[a-f0-9]{24}$/.test(id) ? id : null; } function safeSecretRef3(value) { const ref = cleanString11(value); return ref && /^DSH_WECOM_BOT_SECRET_[A-F0-9]{24}$/.test(ref) ? ref : null; } function deriveWecomBotIdentity(remoteBotId) { const raw = cleanString11(remoteBotId); if (!raw) throw new TypeError("Enterprise WeChat bot ID is required"); const digest2 = createHash5("sha256").update(raw).digest("hex").slice(0, 24); return { botId: `wecom_${digest2}`, secretRef: `DSH_WECOM_BOT_SECRET_${digest2.toUpperCase()}` }; } function maskWecomBotId(remoteBotId) { const value = cleanString11(remoteBotId) ?? ""; if (!value) return "\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA"; if (value.length <= 10) return `${value.slice(0, 3)}\u2022\u2022\u2022`; return `${value.slice(0, 6)}\u2022\u2022\u2022\u2022${value.slice(-4)}`; } function normalizeBot4(value) { if (!value || typeof value !== "object") return null; const botId = safeIntegrationId(value.botId); const remoteBotId = cleanString11(value.remoteBotId); const secretRef = safeSecretRef3(value.secretRef); if (!botId || !remoteBotId || !secretRef) return null; const derived = deriveWecomBotIdentity(remoteBotId); if (derived.botId !== botId || derived.secretRef !== secretRef) return null; return Object.freeze({ botId, remoteBotId, secretRef, createdAt: cleanString11(value.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString(), connectedAt: cleanString11(value.connectedAt) }); } function normalizeDocument4(value) { if (!value || value.version !== 1 || !Array.isArray(value.bots)) return null; const bots = value.bots.map(normalizeBot4); if (bots.some((bot) => bot === null)) return null; const ids = /* @__PURE__ */ new Set(); const remoteIds = /* @__PURE__ */ new Set(); const refs = /* @__PURE__ */ new Set(); for (const bot of bots) { if (ids.has(bot.botId) || remoteIds.has(bot.remoteBotId) || refs.has(bot.secretRef)) return null; ids.add(bot.botId); remoteIds.add(bot.remoteBotId); refs.add(bot.secretRef); } return Object.freeze({ version: 1, bots: Object.freeze(bots) }); } var WecomConfigStore = class { #path; #value = EMPTY_DOCUMENT4; #writeQueue = Promise.resolve(); constructor(path) { this.#path = path; } async load() { try { const normalized = normalizeDocument4(JSON.parse(await readFile9(this.#path, "utf8"))); if (!normalized) throw new Error("dsh-im Enterprise WeChat config contains invalid bot data"); this.#value = normalized; } catch (error) { if (error?.code !== "ENOENT") throw error; this.#value = EMPTY_DOCUMENT4; } return this; } list() { return structuredClone(this.#value.bots); } get(botId) { const bot = this.#value.bots.find((candidate) => candidate.botId === botId); return bot ? structuredClone(bot) : null; } getByRemoteBotId(remoteBotId) { const bot = this.#value.bots.find((candidate) => candidate.remoteBotId === remoteBotId); return bot ? structuredClone(bot) : null; } async save(value) { const normalized = normalizeBot4(value); if (!normalized) throw new Error("Refusing to persist incomplete Enterprise WeChat bot data"); return this.#mutate((bots) => { const remoteCollision = bots.find( (bot) => bot.remoteBotId === normalized.remoteBotId && bot.botId !== normalized.botId ); const refCollision = bots.find( (bot) => bot.secretRef === normalized.secretRef && bot.botId !== normalized.botId ); if (remoteCollision || refCollision) throw new Error("Duplicate Enterprise WeChat bot identity"); const index = bots.findIndex((bot) => bot.botId === normalized.botId); if (index === -1) bots.push(normalized); else bots[index] = normalized; return structuredClone(normalized); }); } async remove(botId) { if (!safeIntegrationId(botId)) throw new TypeError("Invalid Enterprise WeChat bot ID"); return this.#mutate((bots) => { const index = bots.findIndex((bot) => bot.botId === botId); if (index === -1) return null; const [removed] = bots.splice(index, 1); return structuredClone(removed); }); } async clear() { const operation = this.#writeQueue.then(async () => { try { await unlink12(this.#path); } catch (error) { if (error?.code !== "ENOENT") throw error; } this.#value = EMPTY_DOCUMENT4; }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; } async #mutate(mutator) { let result; const operation = this.#writeQueue.then(async () => { const bots = [...this.#value.bots]; result = mutator(bots); const document = Object.freeze({ version: 1, bots: Object.freeze(bots) }); await this.#write(document); this.#value = document; }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; return result; } async #write(document) { await mkdir9(dirname9(this.#path), { recursive: true, mode: 448 }); const temporary = `${this.#path}.tmp`; await writeFile9(temporary, `${JSON.stringify(document, null, 2)} `, { encoding: "utf8", mode: 384 }); await rename9(temporary, this.#path); } }; // src/channels/wecom/harness-client.mjs var WecomHarnessClient = class extends HarnessClient2 { }; // src/channels/wecom/qr-auth.mjs var GENERATE_URL = "https://work.weixin.qq.com/ai/qc/generate"; var POLL_URL = "https://work.weixin.qq.com/ai/qc/query_result"; var QR_TTL_MS2 = 5 * 6e4; var POLL_INTERVAL_MS = 3e3; function defaultPlatform() { if (process.platform === "win32") return 2; if (process.platform === "linux") return 3; return 1; } function cleanString12(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function safeVerificationUrl(value) { const raw = cleanString12(value); if (!raw) return null; try { const url = new URL(raw); return url.protocol === "https:" && url.hostname === "work.weixin.qq.com" && (!url.port || url.port === "443") ? url.href : null; } catch { return null; } } function combinedSignal(signal, timeoutMs) { return signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs); } async function requestJson2(fetchImpl, url, signal) { const response = await fetchImpl(url, { method: "GET", redirect: "error", signal: combinedSignal(signal, 1e4), headers: { accept: "application/json" } }); if (!response.ok) throw new Error(`Enterprise WeChat QR service returned HTTP ${response.status}`); return response.json(); } var WecomQrAuth = class { #fetch; #clock; #source; #platform; constructor({ fetch: fetchImpl = globalThis.fetch, clock = () => Date.now(), source = "deepseek-harness", platform = defaultPlatform() } = {}) { if (typeof fetchImpl !== "function") throw new TypeError("A fetch implementation is required"); this.#fetch = fetchImpl; this.#clock = clock; this.#source = source; this.#platform = [1, 2, 3].includes(platform) ? platform : defaultPlatform(); } async start({ signal } = {}) { const url = new URL(GENERATE_URL); url.searchParams.set("source", this.#source); url.searchParams.set("plat", String(this.#platform)); const body = await requestJson2(this.#fetch, url, signal); const scode = cleanString12(body?.data?.scode); const verificationUrl = safeVerificationUrl(body?.data?.auth_url); if (!scode || !verificationUrl) throw new Error("Enterprise WeChat QR service returned invalid data"); return { scode, verificationUrl, expiresAt: this.#clock() + QR_TTL_MS2, pollIntervalMs: POLL_INTERVAL_MS }; } async poll({ scode, signal } = {}) { const code = cleanString12(scode); if (!code) throw new TypeError("Enterprise WeChat QR poll code is required"); const url = new URL(POLL_URL); url.searchParams.set("scode", code); const body = await requestJson2(this.#fetch, url, signal); const state = cleanString12(body?.data?.status)?.toLowerCase(); if (state === "success") { const remoteBotId = cleanString12(body?.data?.bot_info?.botid); const secret = cleanString12(body?.data?.bot_info?.secret); if (!remoteBotId || !secret) throw new Error("Enterprise WeChat QR result omitted bot credentials"); return { status: "success", remoteBotId, secret }; } if (["expired", "timeout"].includes(state)) return { status: "expired" }; if (["fail", "failed", "error"].includes(state)) return { status: "failed" }; return { status: "waiting" }; } }; // src/channels/wecom/state-store.mjs import { mkdir as mkdir10, readFile as readFile10, rename as rename10, unlink as unlink13, writeFile as writeFile10 } from "node:fs/promises"; import { dirname as dirname10 } from "node:path"; var EMPTY_STATE5 = Object.freeze({ version: 1, sessions: {}, seenMessageIds: [] }); function normalizeState4(value) { if (!value || typeof value !== "object") return structuredClone(EMPTY_STATE5); const sessions = {}; if (value.sessions && typeof value.sessions === "object" && !Array.isArray(value.sessions)) { for (const [key, sessionId] of Object.entries(value.sessions)) { if (typeof key === "string" && typeof sessionId === "string" && sessionId) sessions[key] = sessionId; } } return { version: 1, sessions, seenMessageIds: Array.isArray(value.seenMessageIds) ? value.seenMessageIds.filter((id) => typeof id === "string").slice(-1e3) : [] }; } var WecomStateStore = class { #path; #state = structuredClone(EMPTY_STATE5); #writeQueue = Promise.resolve(); constructor(path) { this.#path = path; } async load() { try { this.#state = normalizeState4(JSON.parse(await readFile10(this.#path, "utf8"))); } catch (error) { if (error?.code !== "ENOENT") throw error; this.#state = structuredClone(EMPTY_STATE5); await this.#persist(); } return this; } sessionFor(key) { return this.#state.sessions[key] ?? null; } async setSession(key, sessionId) { this.#state.sessions[key] = sessionId; await this.#persist(); } async clearSession(key) { delete this.#state.sessions[key]; await this.#persist(); } hasSeen(messageId) { return this.#state.seenMessageIds.includes(messageId); } async markSeen(messageId) { if (this.hasSeen(messageId)) return; this.#state.seenMessageIds.push(messageId); if (this.#state.seenMessageIds.length > 1e3) { this.#state.seenMessageIds.splice(0, this.#state.seenMessageIds.length - 1e3); } await this.#persist(); } async remove() { try { await unlink13(this.#path); } catch (error) { if (error?.code !== "ENOENT") throw error; } this.#state = structuredClone(EMPTY_STATE5); } async #persist() { const snapshot = `${JSON.stringify(this.#state, null, 2)} `; const operation = this.#writeQueue.then(async () => { await mkdir10(dirname10(this.#path), { recursive: true, mode: 448 }); const temporary = `${this.#path}.tmp`; await writeFile10(temporary, snapshot, { encoding: "utf8", mode: 384 }); await rename10(temporary, this.#path); }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; } }; // src/channels/wecom/wecom-controller.mjs import { randomUUID as randomUUID10 } from "node:crypto"; var ACTIVE_ATTEMPT_STATES3 = /* @__PURE__ */ new Set(["pending", "connecting"]); var TERMINAL_ATTEMPT_STATES3 = /* @__PURE__ */ new Set(["connected", "failed", "cancelled", "expired"]); function cleanString13(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function safeError4(code, message) { return Object.freeze({ code, message }); } function publicAttempt3(record) { if (!record) return null; return { attemptId: record.id, status: record.state, pollIntervalMs: record.pollIntervalMs, qrRevision: record.qrRevision, ...record.verificationUrl ? { verificationUrl: record.verificationUrl } : {}, ...record.expiresAt ? { expiresAt: record.expiresAt } : {}, ...record.botId ? { botId: record.botId } : {}, ...record.error ? { error: structuredClone(record.error) } : {} }; } var WecomController = class { #qrAuth; #credentials; #configStore; #createRuntime; #deleteState; #logger; #runtimes = /* @__PURE__ */ new Map(); #errors = /* @__PURE__ */ new Map(); #attempts = /* @__PURE__ */ new Map(); #activeAttemptId = null; #transitions = /* @__PURE__ */ new Map(); #revision = 0; #closed = false; constructor({ qrAuth, credentials, configStore, createRuntime, deleteState = async () => { }, logger = console }) { if (!qrAuth || typeof qrAuth.start !== "function" || typeof qrAuth.poll !== "function") { throw new TypeError("Enterprise WeChat QR auth is required"); } if (!credentials || typeof credentials.resolve !== "function" || typeof credentials.set !== "function" || typeof credentials.unset !== "function") { throw new TypeError("WecomController requires the DSH credential provider"); } if (!configStore || typeof configStore.list !== "function" || typeof configStore.save !== "function" || typeof configStore.remove !== "function") { throw new TypeError("WecomController requires a config store"); } if (typeof createRuntime !== "function") throw new TypeError("createRuntime is required"); this.#qrAuth = qrAuth; this.#credentials = credentials; this.#configStore = configStore; this.#createRuntime = createRuntime; this.#deleteState = deleteState; this.#logger = logger; } async initialize() { if (this.#closed) return this.status(); for (const config of this.#configStore.list()) { await this.#withBotTransition(config.botId, async () => { const existing = this.#runtimes.get(config.botId)?.status; if (this.#closed || existing?.ready || existing?.wecomConnectionState === "connecting") return; const secret = await this.#resolveSecret(config.secretRef); if (!secret) { this.#errors.set(config.botId, safeError4("missing-secret", "\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA\u51ED\u636E\u7F3A\u5931\uFF0C\u8BF7\u79FB\u9664\u540E\u91CD\u65B0\u626B\u7801\u3002")); return; } try { await this.#startRuntime(config, secret); this.#errors.delete(config.botId); } catch (error) { this.#errors.set(config.botId, safeError4("connection-failed", "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u91CD\u8BD5\u3002")); this.#logger.warn?.(`[dsh-im:wecom] bot ${config.botId} failed to initialize`); } finally { this.#touch(); } }); } return this.status(); } async startProvisioning() { if (this.#closed) throw new Error("Enterprise WeChat controller is closed"); if (this.#activeAttemptId) await this.cancelProvisioning(this.#activeAttemptId); const record = { id: randomUUID10(), state: "pending", createdAt: Date.now(), expiresAt: null, pollIntervalMs: 3e3, qrRevision: 1, verificationUrl: null, scode: null, botId: null, error: null, controller: new AbortController(), polling: null, transition: null }; this.#attempts.set(record.id, record); this.#activeAttemptId = record.id; this.#touch(); try { const started = await this.#qrAuth.start({ signal: record.controller.signal }); record.scode = cleanString13(started.scode); record.verificationUrl = cleanString13(started.verificationUrl); record.expiresAt = Number(started.expiresAt); record.pollIntervalMs = Math.min(1e4, Math.max(500, Number(started.pollIntervalMs) || 3e3)); if (!record.scode || !record.verificationUrl || !Number.isFinite(record.expiresAt)) { throw new Error("Enterprise WeChat QR auth returned incomplete data"); } this.#touch(); return publicAttempt3(record); } catch (error) { record.state = record.controller.signal.aborted ? "cancelled" : "failed"; record.error = record.controller.signal.aborted ? safeError4("cancelled", "\u626B\u7801\u7ED1\u5B9A\u5DF2\u53D6\u6D88\u3002") : safeError4("qr-start-failed", "\u65E0\u6CD5\u751F\u6210\u4F01\u4E1A\u5FAE\u4FE1\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"); this.#finishAttempt(record); throw error; } } async registrationStatus(attemptId) { const record = this.#attempts.get(attemptId); if (!record || TERMINAL_ATTEMPT_STATES3.has(record.state)) return publicAttempt3(record); if (record.state === "connecting") { await record.transition?.catch(() => void 0); return publicAttempt3(record); } if (Date.now() >= record.expiresAt) { record.state = "expired"; record.error = safeError4("expired", "\u4F01\u4E1A\u5FAE\u4FE1\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u3002"); record.controller.abort(); this.#finishAttempt(record); return publicAttempt3(record); } if (!record.polling) { const polling = this.#pollAttempt(record).finally(() => { if (record.polling === polling) record.polling = null; }); record.polling = polling; } await record.polling.catch(() => void 0); return publicAttempt3(record); } async bindCredentials({ botId, secret } = {}) { if (this.#closed) throw new Error("Enterprise WeChat controller is closed"); const remoteBotId = cleanString13(botId); const normalizedSecret = cleanString13(secret); if (!remoteBotId || !normalizedSecret) { throw new TypeError("Enterprise WeChat Bot ID and Secret are required"); } if (this.#activeAttemptId) await this.cancelProvisioning(this.#activeAttemptId); if (this.#closed) throw new Error("Enterprise WeChat controller is closed"); const identity = deriveWecomBotIdentity(remoteBotId); await this.#withBotTransition(identity.botId, async () => { if (this.#closed) throw new Error("Enterprise WeChat controller is closed"); const previousConfig = this.#configStore.getByRemoteBotId(remoteBotId); const previousSecret = await this.#credentials.resolve(identity.secretRef).catch(() => void 0); if (this.#closed) throw new Error("Enterprise WeChat controller is closed"); const config = { botId: identity.botId, remoteBotId, secretRef: identity.secretRef, createdAt: previousConfig?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(), connectedAt: (/* @__PURE__ */ new Date()).toISOString() }; await this.#credentials.set(identity.secretRef, normalizedSecret); try { await this.#configStore.save(config); } catch (error) { await this.#restoreCredential(identity.secretRef, previousSecret); throw error; } try { await this.#startRuntime(config, normalizedSecret); this.#errors.delete(identity.botId); } catch { this.#errors.set( identity.botId, safeError4("connection-failed", "\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA\u5DF2\u7ED1\u5B9A\uFF0C\u6D88\u606F\u8FDE\u63A5\u6682\u672A\u5C31\u7EEA\u3002") ); this.#logger.warn?.(`[dsh-im:wecom] bot ${identity.botId} credential connection failed`); } this.#touch(); }); return this.status(); } async cancelProvisioning(attemptId) { const record = this.#attempts.get(attemptId); if (!record) return null; if (!TERMINAL_ATTEMPT_STATES3.has(record.state)) { record.controller.abort(); await Promise.allSettled([record.polling, record.transition].filter(Boolean)); if (!TERMINAL_ATTEMPT_STATES3.has(record.state)) record.state = "cancelled"; record.error ??= safeError4("cancelled", "\u626B\u7801\u7ED1\u5B9A\u5DF2\u53D6\u6D88\u3002"); this.#finishAttempt(record); } return publicAttempt3(record); } async reconnectBot(botId) { const config = this.#configStore.get(botId); if (!config) throw new Error("Unknown Enterprise WeChat bot"); await this.#withBotTransition(botId, async () => { const secret = await this.#resolveSecret(config.secretRef); if (!secret) throw new Error("Enterprise WeChat bot secret is missing"); try { await this.#startRuntime(config, secret); this.#errors.delete(botId); } catch (error) { this.#errors.set(botId, safeError4("connection-failed", "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u4ECD\u672A\u5C31\u7EEA\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002")); throw error; } finally { this.#touch(); } }); return this.status(); } async deleteBot(botId) { const config = this.#configStore.get(botId); if (!config) throw new Error("Unknown Enterprise WeChat bot"); await this.#withBotTransition(botId, async () => { const previous = await this.#credentials.resolve(config.secretRef).catch(() => void 0); await this.#stopRuntime(botId); try { await this.#credentials.unset(config.secretRef); await this.#configStore.remove(botId); } catch (error) { if (previous?.value) { await this.#credentials.set(config.secretRef, previous.value).catch(() => void 0); await this.#startRuntime(config, previous.value).catch(() => void 0); } throw new Error("Unable to remove the Enterprise WeChat bot safely.", { cause: error }); } await this.#deleteState({ botId, config }).catch((error) => { this.#logger.warn?.(`[dsh-im:wecom] bot ${botId} state cleanup failed:`, error); }); this.#errors.delete(botId); this.#touch(); }); return this.status(); } status() { const bots = this.#configStore.list().map((config) => { const runtimeStatus2 = this.#runtimes.get(config.botId)?.status ?? null; const connected = runtimeStatus2?.ready === true && runtimeStatus2.wecomConnectionState === "connected" && runtimeStatus2.harnessReachable === true; const state = connected ? "connected" : runtimeStatus2?.wecomConnectionState === "connecting" ? "connecting" : this.#errors.has(config.botId) || runtimeStatus2?.wecomConnectionState === "failed" ? "error" : "offline"; return { botId: config.botId, state, connected, configured: true, bot: { name: "\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA", appIdMasked: maskWecomBotId(config.remoteBotId) }, health: { status: connected ? "healthy" : state === "error" ? "error" : "offline", summary: connected ? "\u4F01\u4E1A\u5FAE\u4FE1 WebSocket \u957F\u8FDE\u63A5\u8FD0\u884C\u6B63\u5E38" : state === "error" ? "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u91CD\u8BD5" : "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u5F53\u524D\u79BB\u7EBF", lastCheckedAt: runtimeStatus2?.lastCheckedAt ?? null, lastConnectedAt: runtimeStatus2?.lastConnectedAt ?? null }, stats: { messagesReceived: runtimeStatus2?.messagesReceived ?? 0, messagesReplied: runtimeStatus2?.messagesReplied ?? 0 }, error: structuredClone(this.#errors.get(config.botId) ?? null) }; }); const connectedCount = bots.filter((bot) => bot.connected).length; const active = this.#activeAttemptId ? this.#attempts.get(this.#activeAttemptId) : null; return { schemaVersion: 1, revision: this.#revision, state: active && ACTIVE_ATTEMPT_STATES3.has(active.state) ? "provisioning" : bots.length === 0 ? "disconnected" : connectedCount === bots.length ? "connected" : connectedCount > 0 ? "degraded" : "offline", bots, totals: { configured: bots.length, connected: connectedCount }, ...active && ACTIVE_ATTEMPT_STATES3.has(active.state) ? { provisioning: publicAttempt3(active) } : {} }; } async close() { if (this.#closed) return; this.#closed = true; if (this.#activeAttemptId) await this.cancelProvisioning(this.#activeAttemptId); await Promise.allSettled([...this.#transitions.values()]); await Promise.allSettled([...this.#runtimes.keys()].map((botId) => this.#stopRuntime(botId))); } async #pollAttempt(record) { try { const result = await this.#qrAuth.poll({ scode: record.scode, signal: record.controller.signal }); if (record.controller.signal.aborted || TERMINAL_ATTEMPT_STATES3.has(record.state)) return; if (result.status === "waiting") return; if (result.status === "expired") { record.state = "expired"; record.error = safeError4("expired", "\u4F01\u4E1A\u5FAE\u4FE1\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u3002"); this.#finishAttempt(record); return; } if (result.status !== "success") { record.state = "failed"; record.error = safeError4("qr-connect-failed", "\u4F01\u4E1A\u5FAE\u4FE1\u626B\u7801\u6CA1\u6709\u5B8C\u6210\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801\u3002"); this.#finishAttempt(record); return; } record.state = "connecting"; record.verificationUrl = null; record.expiresAt = null; record.scode = null; this.#touch(); const transition = this.#completeProvisioning(record, result); record.transition = transition; await transition; } catch (error) { if (record.controller.signal.aborted) return; record.state = "failed"; record.error = safeError4("qr-connect-failed", "\u4F01\u4E1A\u5FAE\u4FE1\u626B\u7801\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801\u3002"); this.#logger.warn?.("[dsh-im:wecom] QR polling failed"); this.#finishAttempt(record); } } async #completeProvisioning(record, result) { try { const remoteBotId = cleanString13(result.remoteBotId); const secret = cleanString13(result.secret); if (!remoteBotId || !secret) throw new Error("Enterprise WeChat authorization returned incomplete credentials"); record.botId = await this.#activateBot(record, { remoteBotId, secret }); record.state = "connected"; record.error = null; } catch (error) { if (record.controller.signal.aborted) { record.state = "cancelled"; record.error = safeError4("cancelled", "\u626B\u7801\u7ED1\u5B9A\u5DF2\u53D6\u6D88\u3002"); } else { record.state = "failed"; record.error = safeError4("activation-failed", "\u4F01\u4E1A\u5FAE\u4FE1\u5DF2\u6388\u6743\uFF0C\u4F46\u65E0\u6CD5\u5B89\u5168\u4FDD\u5B58\u63A5\u5165\u914D\u7F6E\u3002"); this.#logger.error?.("[dsh-im:wecom] provisioning failed"); } } finally { this.#finishAttempt(record); } } async #activateBot(record, { remoteBotId, secret }) { const identity = deriveWecomBotIdentity(remoteBotId); const previousConfig = this.#configStore.getByRemoteBotId(remoteBotId); const previousSecret = await this.#credentials.resolve(identity.secretRef).catch(() => void 0); const config = { botId: identity.botId, remoteBotId, secretRef: identity.secretRef, createdAt: previousConfig?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(), connectedAt: (/* @__PURE__ */ new Date()).toISOString() }; return this.#withBotTransition(identity.botId, async () => { await this.#credentials.set(identity.secretRef, secret); try { if (record.controller.signal.aborted) throw new DOMException("Cancelled", "AbortError"); await this.#configStore.save(config); } catch (error) { await this.#restoreCredential(identity.secretRef, previousSecret); throw error; } try { if (record.controller.signal.aborted) throw new DOMException("Cancelled", "AbortError"); await this.#startRuntime(config, secret); this.#errors.delete(identity.botId); } catch (error) { if (record.controller.signal.aborted) { await this.#stopRuntime(identity.botId); if (previousConfig) await this.#configStore.save(previousConfig).catch(() => void 0); else await this.#configStore.remove(identity.botId).catch(() => void 0); await this.#restoreCredential(identity.secretRef, previousSecret); throw error; } this.#errors.set(identity.botId, safeError4("connection-failed", "\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA\u5DF2\u7ED1\u5B9A\uFF0C\u6D88\u606F\u8FDE\u63A5\u6682\u672A\u5C31\u7EEA\u3002")); this.#logger.warn?.(`[dsh-im:wecom] bot ${identity.botId} activation connection failed`); } this.#touch(); return identity.botId; }); } async #startRuntime(config, secret) { if (this.#closed) throw new Error("Enterprise WeChat controller is closed"); await this.#stopRuntime(config.botId); if (this.#closed) throw new Error("Enterprise WeChat controller is closed"); const runtime = await this.#createRuntime({ botId: config.botId, config, secret }); if (!runtime || typeof runtime.start !== "function" || typeof runtime.stop !== "function") { throw new TypeError("createRuntime returned an invalid Enterprise WeChat runtime"); } this.#runtimes.set(config.botId, runtime); try { await runtime.start(); } catch (error) { await runtime.stop().catch(() => void 0); this.#runtimes.delete(config.botId); throw error; } } async #stopRuntime(botId) { const runtime = this.#runtimes.get(botId); this.#runtimes.delete(botId); await runtime?.stop().catch((error) => { this.#logger.warn?.(`[dsh-im:wecom] bot ${botId} failed to stop cleanly:`, error); }); } async #resolveSecret(ref) { const result = await this.#credentials.resolve(ref).catch(() => void 0); return cleanString13(result?.value); } async #restoreCredential(ref, previous) { if (previous?.value) await this.#credentials.set(ref, previous.value).catch(() => void 0); else await this.#credentials.unset(ref).catch(() => void 0); } #withBotTransition(botId, operation) { const previous = this.#transitions.get(botId) ?? Promise.resolve(); const current = previous.catch(() => void 0).then(operation); const settled = current.finally(() => { if (this.#transitions.get(botId) === settled) this.#transitions.delete(botId); }); this.#transitions.set(botId, settled); return settled; } #finishAttempt(record) { record.scode = null; record.verificationUrl = null; record.expiresAt = null; if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; this.#touch(); const terminal = [...this.#attempts.values()].filter((attempt) => TERMINAL_ATTEMPT_STATES3.has(attempt.state)); while (terminal.length > 16) this.#attempts.delete(terminal.shift().id); } #touch() { this.#revision += 1; } }; // src/channels/wecom/wecom-runtime.mjs import { WSAuthFailureError, WSClient, WSReconnectExhaustedError } from "@wecom/aibot-node-sdk"; // src/channels/wecom/wecom-bridge.mjs import { generateReqId } from "@wecom/aibot-node-sdk"; var HELP_TEXT4 = [ "\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA\u5DF2\u8FDE\u63A5 DeepSeek Harness\u3002", "", "\u76F4\u63A5\u53D1\u9001\u6587\u5B57\u5373\u53EF\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\u3002", "/new \u5F00\u542F\u4E00\u4E2A\u5168\u65B0\u4F1A\u8BDD", "/status \u68C0\u67E5\u8FDE\u63A5\u72B6\u6001", "/help \u663E\u793A\u672C\u5E2E\u52A9" ].join("\n"); var MAX_REPLY_BYTES = 18e3; function bodyOf(frame) { return frame?.body && typeof frame.body === "object" ? frame.body : {}; } function conversationKey4(frame) { const body = bodyOf(frame); return body.chattype === "group" ? `group:${body.chatid}` : `direct:${body.from?.userid}`; } function messageText2(frame) { const body = bodyOf(frame); if (body.msgtype === "text") return typeof body.text?.content === "string" ? body.text.content.trim() : ""; if (body.msgtype === "voice") return typeof body.voice?.content === "string" ? body.voice.content.trim() : ""; if (body.msgtype === "mixed" && Array.isArray(body.mixed?.msg_item)) { return body.mixed.msg_item.filter((item) => item?.msgtype === "text" && typeof item.text?.content === "string").map((item) => item.text.content).join("\n").trim(); } return ""; } function splitUtf8(text, maxBytes = MAX_REPLY_BYTES) { const source = String(text ?? "").trim(); if (!source) return []; const chunks = []; let current = ""; let bytes = 0; for (const character of source) { const size = Buffer.byteLength(character); if (current && bytes + size > maxBytes) { chunks.push(current); current = character; bytes = size; } else { current += character; bytes += size; } } if (current) chunks.push(current); return chunks; } function progressText2(update) { if (update?.type === "text") return update.text; if (update?.type === "tool") return `\u6B63\u5728\u4F7F\u7528${update.name}\u2026`; return update?.text; } function createWecomBridgeStatus() { return { messagesReceived: 0, messagesReplied: 0, messagesRejected: 0, lastMessageAt: null, lastReplyAt: null, lastRejectedAt: null, lastError: null }; } var WecomHarnessBridge = class { #client; #harness; #state; #status; #logger; #replyTimeoutMs; #generateReqId; #queues = /* @__PURE__ */ new Map(); constructor({ client, harness, state, status = createWecomBridgeStatus(), logger = console, replyTimeoutMs = 6e5, generateStreamId = generateReqId }) { if (!client || typeof client.replyStream !== "function" || typeof client.sendMessage !== "function") { throw new TypeError("Enterprise WeChat client is required"); } if (!harness || !state) throw new TypeError("Harness client and state store are required"); this.#client = client; this.#harness = harness; this.#state = state; this.#status = status; this.#logger = logger; this.#replyTimeoutMs = replyTimeoutMs; this.#generateReqId = generateStreamId; } get status() { return structuredClone(this.#status); } accept(frame) { const key = conversationKey4(frame); const previous = this.#queues.get(key) ?? Promise.resolve(); const current = previous.catch(() => void 0).then(() => this.#process(frame)).finally(() => { if (this.#queues.get(key) === current) this.#queues.delete(key); }); this.#queues.set(key, current); return current; } async waitForIdle() { await Promise.allSettled([...this.#queues.values()]); } async #sendActive(chatId, text) { for (const chunk of splitUtf8(text)) { await this.#client.sendMessage(chatId, { msgtype: "markdown", markdown: { content: chunk } }); } } async #sendImmediate(frame, chatId, text) { const chunks = splitUtf8(text); if (chunks.length === 0) return; try { await this.#client.replyStream(frame, this.#generateReqId("stream"), chunks[0], true); for (const chunk of chunks.slice(1)) { await this.#client.sendMessage(chatId, { msgtype: "markdown", markdown: { content: chunk } }); } } catch { await this.#sendActive(chatId, text); } } async #process(frame) { const body = bodyOf(frame); const messageId = typeof body.msgid === "string" ? body.msgid : ""; const senderId = typeof body.from?.userid === "string" ? body.from.userid : ""; const chatId = body.chattype === "group" ? body.chatid : senderId; if (!messageId || !senderId || !chatId || !["single", "group"].includes(body.chattype)) return; if (this.#state.hasSeen(messageId)) return; this.#status.messagesReceived += 1; this.#status.lastMessageAt = (/* @__PURE__ */ new Date()).toISOString(); const text = messageText2(frame); let streamId = null; let streamStarted = false; try { if (!text) { await this.#sendImmediate(frame, chatId, "\u76EE\u524D\u652F\u6301\u6587\u5B57\u3001\u8BED\u97F3\u8F6C\u5199\u548C\u56FE\u6587\u6DF7\u6392\u4E2D\u7684\u6587\u5B57\u6D88\u606F\u3002"); await this.#state.markSeen(messageId); return; } const command = text.toLowerCase(); if (command === "/help") { await this.#sendImmediate(frame, chatId, HELP_TEXT4); await this.#state.markSeen(messageId); return; } if (command === "/status") { await this.#harness.ensureRunning(); await this.#sendImmediate(frame, chatId, "\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA\u4E0E DeepSeek Harness \u8FDE\u63A5\u6B63\u5E38\u3002"); await this.#state.markSeen(messageId); return; } const key = conversationKey4(frame); if (command === "/new") { await this.#state.clearSession(key); await this.#sendImmediate(frame, chatId, "\u5DF2\u5F00\u542F\u65B0\u4F1A\u8BDD\u3002\u8BF7\u53D1\u9001\u4F60\u7684\u95EE\u9898\u3002"); await this.#state.markSeen(messageId); return; } let sessionId = this.#state.sessionFor(key); if (!sessionId || !await this.#harness.sessionExists(sessionId)) { sessionId = await this.#harness.createSession(); await this.#state.setSession(key, sessionId); } streamId = this.#generateReqId("stream"); try { await this.#client.replyStream(frame, streamId, "\u6B63\u5728\u601D\u8003\u4E2D\u2026", false); streamStarted = true; } catch (error) { this.#logger.warn?.("[dsh-im:wecom] unable to start a stream; using an active reply:", error); } const answer = await this.#harness.ask(sessionId, text, { timeoutMs: this.#replyTimeoutMs, onUpdate: streamStarted && typeof this.#client.replyStreamNonBlocking === "function" ? async (update) => { const progress = splitUtf8(progressText2(update))[0]; if (progress) await this.#client.replyStreamNonBlocking(frame, streamId, progress, false); } : void 0 }); const chunks = splitUtf8(answer || "\u4EFB\u52A1\u5DF2\u5B8C\u6210\uFF0C\u4F46\u6CA1\u6709\u751F\u6210\u53EF\u663E\u793A\u7684\u6587\u672C\u3002"); let finalSent = false; if (streamStarted && chunks.length > 0) { try { await this.#client.replyStream(frame, streamId, chunks[0], true); for (const chunk of chunks.slice(1)) { await this.#client.sendMessage(chatId, { msgtype: "markdown", markdown: { content: chunk } }); } finalSent = true; } catch (error) { this.#logger.warn?.("[dsh-im:wecom] stream finalization failed; using an active reply:", error); } } if (!finalSent) await this.#sendActive(chatId, answer); await this.#state.markSeen(messageId); this.#status.messagesReplied += 1; this.#status.lastReplyAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.lastError = null; } catch (error) { this.#status.lastError = error?.message ?? String(error); this.#logger.error?.("[dsh-im:wecom] failed to process an inbound message"); try { if (streamStarted && streamId) { await this.#client.replyStream(frame, streamId, "\u6D88\u606F\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", true); } else { await this.#sendImmediate(frame, chatId, "\u6D88\u606F\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"); } await this.#state.markSeen(messageId); } catch { this.#logger.error?.("[dsh-im:wecom] failed to send the safe error reply"); } } } }; // src/channels/wecom/wecom-runtime.mjs function timeoutError2() { const error = new Error("Enterprise WeChat WebSocket authentication timed out"); error.code = "connect-timeout"; return error; } function createWecomRuntimeStatus() { return { startedAt: null, ready: false, wecomConnectionState: "idle", harnessReachable: false, lastCheckedAt: null, lastConnectedAt: null, lastError: null, ...createWecomBridgeStatus() }; } var WecomRuntime = class { #config; #secret; #harness; #state; #logger; #replyTimeoutMs; #connectTimeoutMs; #maxReconnectAttempts; #createClient; #status = createWecomRuntimeStatus(); #client = null; #bridge = null; #starting = null; #startController = null; constructor({ config, secret, harness, state, logger = console, replyTimeoutMs = 6e5, connectTimeoutMs = 2e4, maxReconnectAttempts = 10, createClient = (options) => new WSClient(options) }) { if (!config || !secret || !harness || !state) { throw new TypeError("WecomRuntime requires config, secret, Harness, and state"); } this.#config = config; this.#secret = secret; this.#harness = harness; this.#state = state; this.#logger = logger; this.#replyTimeoutMs = replyTimeoutMs; this.#connectTimeoutMs = connectTimeoutMs; this.#maxReconnectAttempts = maxReconnectAttempts; this.#createClient = createClient; } get status() { return structuredClone(this.#status); } async start() { if (this.#status.ready && this.#client) return this.status; if (this.#starting) return this.#starting; const controller = new AbortController(); this.#startController = controller; this.#starting = this.#start(controller.signal).finally(() => { if (this.#startController === controller) this.#startController = null; this.#starting = null; }); return this.#starting; } async #start(signal) { await this.#stopActive(); signal.throwIfAborted(); this.#status.startedAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.wecomConnectionState = "connecting"; this.#status.lastError = null; await this.#harness.ensureRunning(); this.#status.harnessReachable = true; const silentSdkLogger = { debug() { }, info() { }, warn() { }, error() { } }; const client = this.#createClient({ botId: this.#config.remoteBotId, secret: this.#secret, logger: silentSdkLogger, maxReconnectAttempts: this.#maxReconnectAttempts }); if (!client || typeof client.connect !== "function" || typeof client.disconnect !== "function") { throw new TypeError("Enterprise WeChat client factory returned an invalid client"); } this.#client = client; this.#bridge = new WecomHarnessBridge({ client, harness: this.#harness, state: this.#state, status: this.#status, logger: this.#logger, replyTimeoutMs: this.#replyTimeoutMs }); let readyResolve; let readyReject; let authenticated = false; const ready = new Promise((resolve7, reject) => { readyResolve = resolve7; readyReject = reject; }); const onAuthenticated = () => { if (this.#client !== client || signal.aborted) return; authenticated = true; const now = Date.now(); this.#status.ready = true; this.#status.wecomConnectionState = "connected"; this.#status.lastCheckedAt = now; this.#status.lastConnectedAt = now; this.#status.lastError = null; readyResolve(); }; const onDisconnected = () => { if (this.#client !== client) return; this.#status.ready = false; this.#status.wecomConnectionState = "connecting"; this.#status.lastCheckedAt = Date.now(); }; const onReconnecting = () => { if (this.#client !== client) return; this.#status.ready = false; this.#status.wecomConnectionState = "connecting"; this.#status.lastCheckedAt = Date.now(); }; const onError = (error) => { if (this.#client !== client) return; const terminal = error instanceof WSAuthFailureError || error instanceof WSReconnectExhaustedError; if (!authenticated && terminal) readyReject(error); if (terminal) { this.#status.ready = false; this.#status.wecomConnectionState = "failed"; } this.#status.lastError = terminal ? error.name : "connection-error"; this.#logger.warn?.(`[dsh-im:wecom] bot ${this.#config.botId} connection error`); }; const onMessage = (frame) => this.#bridge?.accept(frame); client.on("authenticated", onAuthenticated); client.on("disconnected", onDisconnected); client.on("reconnecting", onReconnecting); client.on("error", onError); client.on("message", onMessage); let timer; try { client.connect(); await Promise.race([ ready, new Promise((_, reject) => { signal.addEventListener("abort", () => reject(signal.reason), { once: true }); }), new Promise((_, reject) => { timer = setTimeout(() => reject(timeoutError2()), this.#connectTimeoutMs); }) ]); return this.status; } catch (error) { if (signal.aborted) { await this.#stopActive(); throw error; } this.#status.ready = false; this.#status.wecomConnectionState = "failed"; this.#status.lastError = error?.message ?? String(error); await this.#stopActive(); throw error; } finally { clearTimeout(timer); } } async stop() { const starting = this.#starting; this.#startController?.abort(new DOMException("Enterprise WeChat runtime stopped", "AbortError")); await this.#stopActive(); await starting?.catch(() => void 0); return this.status; } async #stopActive() { const client = this.#client; const bridge = this.#bridge; this.#client = null; this.#bridge = null; try { client?.disconnect(); client?.removeAllListeners?.(); } catch (error) { this.#logger.warn?.(`[dsh-im:wecom] bot ${this.#config.botId} failed to stop cleanly`); } await bridge?.waitForIdle(); this.#status.ready = false; this.#status.wecomConnectionState = "idle"; } }; // plugin-src/host/channels/wecom/connection-supervisor.mjs var DEFAULT_RETRY_DELAYS_MS5 = Object.freeze([250, 1e3, 3e3, 5e3, 1e4, 3e4]); function retryDelays4(value) { if (!Array.isArray(value) || value.length === 0) return [...DEFAULT_RETRY_DELAYS_MS5]; const valid = value.filter((delay3) => Number.isFinite(delay3) && delay3 >= 0); return valid.length > 0 ? valid : [...DEFAULT_RETRY_DELAYS_MS5]; } var ConnectionSupervisor4 = class { #controller; #harness; #logger; #retryDelays; #healthyIntervalMs; #setTimeout; #clearTimeout; #timer = null; #running = null; #retryIndex = 0; #closed = false; #started = false; #ready; #resolveReady; constructor({ controller, harness, logger = console, retryDelaysMs, healthyIntervalMs = 15e3, setTimeoutImpl = setTimeout, clearTimeoutImpl = clearTimeout }) { if (!controller || typeof controller.initialize !== "function" || typeof controller.status !== "function") { throw new TypeError("ConnectionSupervisor requires a controller"); } if (!harness || typeof harness.ensureRunning !== "function") { throw new TypeError("ConnectionSupervisor requires a Harness client"); } this.#controller = controller; this.#harness = harness; this.#logger = logger; this.#retryDelays = retryDelays4(retryDelaysMs); this.#healthyIntervalMs = Number.isFinite(healthyIntervalMs) && healthyIntervalMs >= 0 ? healthyIntervalMs : 15e3; this.#setTimeout = setTimeoutImpl; this.#clearTimeout = clearTimeoutImpl; this.#ready = new Promise((resolve7) => { this.#resolveReady = resolve7; }); } get ready() { return this.#ready; } start() { if (this.#started || this.#closed) return this; this.#started = true; this.#schedule(0); return this; } async close() { if (this.#closed) return; this.#closed = true; if (this.#timer !== null) this.#clearTimeout(this.#timer); this.#timer = null; await this.#running?.catch(() => void 0); this.#resolveReady?.(null); this.#resolveReady = null; } #schedule(delayMs) { if (this.#closed) return; this.#timer = this.#setTimeout(() => { this.#timer = null; void this.#run(); }, delayMs); this.#timer?.unref?.(); } async #run() { if (this.#closed || this.#running) return; const operation = this.#reconcile(); this.#running = operation; try { await operation; } finally { if (this.#running === operation) this.#running = null; } } async #reconcile() { try { await this.#harness.ensureRunning(); if (this.#closed) return; const status = await this.#controller.initialize(); if (this.#closed) return; this.#resolveReady?.(status); this.#resolveReady = null; const { configured, connected } = status.totals; if (connected < configured) { const delay3 = this.#retryDelays[Math.min(this.#retryIndex, this.#retryDelays.length - 1)]; this.#retryIndex += 1; this.#logger.warn?.(`[dsh-im:wecom] ${connected}/${configured} bots connected; retrying in ${delay3}ms`); this.#schedule(delay3); return; } this.#retryIndex = 0; this.#schedule(this.#healthyIntervalMs); } catch (error) { if (this.#closed) return; const delay3 = this.#retryDelays[Math.min(this.#retryIndex, this.#retryDelays.length - 1)]; this.#retryIndex += 1; this.#logger.warn?.(`[dsh-im:wecom] connection reconciliation failed; retrying in ${delay3}ms`, error); this.#schedule(delay3); } } }; function createConnectionSupervisor4(options) { return new ConnectionSupervisor4(options); } // plugin-src/host/channels/wecom/production.mjs function harnessOrigin5(webServer, configured) { if (configured !== void 0) return new URL(configured); const port = webServer?.port; if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error("dsh-im Enterprise WeChat requires an initialized DSH webServer port"); } return new URL(`http://127.0.0.1:${port}`); } function pluginPaths5(config) { const dshHome = resolve5(config.dshHome ?? process.env.DSH_HOME ?? join5(homedir5(), ".dsh")); const root = resolve5(config.dataDir ?? join5(dshHome, "integrations", "dsh-wecom")); return { config: resolve5(config.configPath ?? join5(root, "config.json")), bots: resolve5(config.botsDir ?? join5(root, "bots")) }; } async function createProductionController6(ctx, config = {}, internals = {}) { if (!ctx?.credentials) throw new TypeError("dsh-im Enterprise WeChat requires ctx.credentials"); if (!ctx?.webServer) throw new TypeError("dsh-im Enterprise WeChat requires ctx.webServer"); const ConfigStore = internals.ConfigStore ?? WecomConfigStore; const StateStore2 = internals.StateStore ?? WecomStateStore; const Harness = internals.HarnessClient ?? WecomHarnessClient; const Controller = internals.Controller ?? WecomController; const Runtime = internals.Runtime ?? WecomRuntime; const QrAuth = internals.QrAuth ?? WecomQrAuth; const createSupervisor = internals.createConnectionSupervisor ?? createConnectionSupervisor4; const logger = typeof ctx.logger === "function" ? ctx.logger("dsh-im:wecom") : ctx.logger ?? console; const paths = pluginPaths5(config); const configStore = await new ConfigStore(paths.config).load(); const qrAuth = internals.qrAuth ?? new QrAuth({ source: config.qrSource ?? "deepseek-harness", platform: config.qrPlatform }); const stateStores = /* @__PURE__ */ new Map(); const statePath = (botId) => resolve5(paths.bots, botId, "state.json"); const stateFor = async (botId) => { let state = stateStores.get(botId); if (!state) { state = await new StateStore2(statePath(botId)).load(); stateStores.set(botId, state); } return state; }; const harness = new Harness({ baseUrl: harnessOrigin5(ctx.webServer, config.harnessBaseUrl), workspace: resolve5(config.workspace ?? process.cwd()), agentPreset: config.agentPreset ?? "standard", autostart: false, dshBin: config.dshBin ?? "dsh" }); const controller = new Controller({ qrAuth, credentials: ctx.credentials, configStore, logger, createRuntime: async ({ botId, config: botConfig, secret }) => new Runtime({ config: botConfig, secret, harness, state: await stateFor(botId), replyTimeoutMs: config.replyTimeoutMs ?? 6e5, connectTimeoutMs: config.connectTimeoutMs ?? 2e4, maxReconnectAttempts: config.maxReconnectAttempts ?? 10, logger: { error: (...args) => logger.error?.(`[${botId}]`, ...args), warn: (...args) => logger.warn?.(`[${botId}]`, ...args), info: (...args) => logger.info?.(`[${botId}]`, ...args), debug: (...args) => logger.debug?.(`[${botId}]`, ...args) } }), deleteState: async ({ botId }) => { const state = stateStores.get(botId); stateStores.delete(botId); if (state && typeof state.remove === "function") return state.remove(); try { await unlink14(statePath(botId)); } catch (error) { if (error?.code !== "ENOENT") throw error; } } }); const supervisor = createSupervisor({ controller, harness, logger, retryDelaysMs: config.retryDelaysMs, healthyIntervalMs: config.healthyIntervalMs }).start(); return { controller, ready: supervisor.ready, async close() { await supervisor.close(); await controller.close(); harness.stopManagedProcess(); } }; } // plugin-src/host/channels/wecom/rpc.mjs import QRCode4 from "qrcode"; var WECOM_RPC_CHANNEL = "/wecom"; var WECOM_ENDPOINTS = Object.freeze({ status: "connection.status", beginProvisioning: "provision.begin", pollProvisioning: "provision.poll", cancelProvisioning: "provision.cancel", bindCredentials: "bot.bind-credentials", reconnectBot: "bot.reconnect", deleteBot: "bot.delete" }); var WECOM_RPC_ENDPOINTS = Object.freeze(Object.values(WECOM_ENDPOINTS)); var FORBIDDEN_PUBLIC_KEYS4 = /* @__PURE__ */ new Set([ "secret", "secretRef", "scode", "remoteBotId", "verificationUrl", "bot_info", "botid" ]); function isRecord4(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function exactKeys4(value, allowed) { return isRecord4(value) && Object.keys(value).every((key) => allowed.includes(key)); } function validId4(value) { return typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value); } function validCredential4(value, maxLength) { return typeof value === "string" && value.trim().length > 0 && value.length <= maxLength; } function payloadFailure4(endpoint, payload) { if (!isRecord4(payload)) return "Payload must be an object."; if (endpoint === WECOM_ENDPOINTS.status) return exactKeys4(payload, []) ? null : "connection.status does not accept fields."; if (endpoint === WECOM_ENDPOINTS.beginProvisioning) { return exactKeys4(payload, ["locale"]) && (payload.locale === void 0 || payload.locale === "zh-CN") ? null : "provision.begin received unsupported fields."; } if ([WECOM_ENDPOINTS.pollProvisioning, WECOM_ENDPOINTS.cancelProvisioning].includes(endpoint)) { return exactKeys4(payload, ["attemptId"]) && validId4(payload.attemptId) ? null : `${endpoint} requires an attemptId.`; } if (endpoint === WECOM_ENDPOINTS.bindCredentials) { return exactKeys4(payload, ["botId", "secret"]) && validCredential4(payload.botId, 512) && validCredential4(payload.secret, 1024) ? null : "bot.bind-credentials requires Bot ID and Secret."; } if (endpoint === WECOM_ENDPOINTS.reconnectBot) { return exactKeys4(payload, ["botId"]) && validId4(payload.botId) ? null : "bot.reconnect requires a botId."; } if (endpoint === WECOM_ENDPOINTS.deleteBot) { return exactKeys4(payload, ["botId", "confirm"]) && validId4(payload.botId) && payload.confirm === true ? null : "bot.delete requires a botId and confirm=true."; } return "Unknown Enterprise WeChat endpoint."; } function sanitizePublic4(value) { if (Array.isArray(value)) return value.map(sanitizePublic4); if (!isRecord4(value)) return value; const safe = {}; for (const [key, child] of Object.entries(value)) { if (!FORBIDDEN_PUBLIC_KEYS4.has(key)) safe[key] = sanitizePublic4(child); } return safe; } async function qrDataUrl3(value) { return QRCode4.toDataURL(value, { type: "image/png", errorCorrectionLevel: "M", margin: 2, width: 320 }); } async function withEncodedQr3(value, encodeQr) { if (!value || typeof value.verificationUrl !== "string") return sanitizePublic4(value); return sanitizePublic4({ ...value, qrCodeDataUrl: await encodeQr(value.verificationUrl) }); } async function publicStatus3(status, encodeQr) { const value = structuredClone(status); if (value?.provisioning) value.provisioning = await withEncodedQr3(value.provisioning, encodeQr); return sanitizePublic4(value); } function createWecomRpcHandler(controller, { encodeQr = qrDataUrl3 } = {}) { for (const method of ["status", "startProvisioning", "registrationStatus", "cancelProvisioning", "bindCredentials", "reconnectBot", "deleteBot"]) { if (typeof controller?.[method] !== "function") { throw new TypeError(`A complete Enterprise WeChat controller is required (${method})`); } } const qrCache = /* @__PURE__ */ new Map(); const cachedEncode = (url) => { let encoded = qrCache.get(url); if (!encoded) { if (qrCache.size >= 16) qrCache.delete(qrCache.keys().next().value); encoded = Promise.resolve().then(() => encodeQr(url)); qrCache.set(url, encoded); } return encoded; }; return async (endpoint, payload, signal) => { if (signal?.aborted) return { ok: false, error: { code: "cancelled", message: "The request was cancelled." } }; if (!WECOM_RPC_ENDPOINTS.includes(endpoint)) { return { ok: false, error: { code: "bad-request", message: "Unknown Enterprise WeChat endpoint." } }; } const invalid = payloadFailure4(endpoint, payload); if (invalid) return { ok: false, error: { code: "bad-request", message: invalid } }; try { let value; if (endpoint === WECOM_ENDPOINTS.status) value = await publicStatus3(await controller.status(), cachedEncode); else if (endpoint === WECOM_ENDPOINTS.beginProvisioning) { value = await withEncodedQr3(await controller.startProvisioning(), cachedEncode); } else if (endpoint === WECOM_ENDPOINTS.pollProvisioning) { const current = await controller.registrationStatus(payload.attemptId); if (!current) return { ok: false, error: { code: "bad-request", message: "The provisioning attempt no longer exists." } }; value = await withEncodedQr3(current, cachedEncode); } else if (endpoint === WECOM_ENDPOINTS.cancelProvisioning) { value = sanitizePublic4(await controller.cancelProvisioning(payload.attemptId)); } else if (endpoint === WECOM_ENDPOINTS.bindCredentials) { value = await publicStatus3(await controller.bindCredentials(payload), cachedEncode); } else if (endpoint === WECOM_ENDPOINTS.reconnectBot) { value = await publicStatus3(await controller.reconnectBot(payload.botId), cachedEncode); } else { value = await publicStatus3(await controller.deleteBot(payload.botId), cachedEncode); } return signal?.aborted ? { ok: false, error: { code: "cancelled", message: "The request was cancelled." } } : { ok: true, value }; } catch { return signal?.aborted ? { ok: false, error: { code: "cancelled", message: "The request was cancelled." } } : { ok: false, error: { code: "wecom-operation-failed", message: "\u4F01\u4E1A\u5FAE\u4FE1\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" } }; } }; } function installWecomRpc(ctx, controller, options, authority) { if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== "function") { throw new TypeError("DSH Host Connection RPC is required"); } return ctx.connection.rpc.handle( WECOM_RPC_CHANNEL, createWecomRpcHandler(controller, options), { authority: resolveRpcAuthority(authority) } ); } // plugin-src/host/channels/wecom/index.mjs async function apply6(ctx, config = {}) { if (config?.controller) { return installWecomRpc(ctx, config.controller, config.rpcOptions, config.rpcAuthority); } const production = await createProductionController6(ctx, config, config.internals); const disposeRpc = installWecomRpc( ctx, production.controller, config.rpcOptions, config.rpcAuthority ); ctx.effect(() => async () => production.close(), "dsh-im: close Enterprise WeChat bot connections"); return disposeRpc; } // plugin-src/host/channels/weixin/production.mjs import { unlink as unlink17 } from "node:fs/promises"; import { homedir as homedir6 } from "node:os"; import { join as join6, resolve as resolve6 } from "node:path"; // src/channels/weixin/config-store.mjs import { createHash as createHash6 } from "node:crypto"; import { mkdir as mkdir11, readFile as readFile11, rename as rename11, unlink as unlink15, writeFile as writeFile11 } from "node:fs/promises"; import { dirname as dirname11 } from "node:path"; // src/channels/weixin/weixin-api.mjs import { randomBytes, randomUUID as randomUUID11 } from "node:crypto"; var WEIXIN_QR_BASE_URL = "https://ilinkai.weixin.qq.com/"; var WEIXIN_PROTOCOL_VERSION = "2.4.6"; var DEFAULT_BOT_TYPE = "3"; var ILINK_APP_ID = "bot"; var ILINK_CLIENT_VERSION = 2 << 16 | 4 << 8 | 6; var DEFAULT_TIMEOUT_MS2 = 15e3; var DEFAULT_LONG_POLL_TIMEOUT_MS = 35e3; var LOGIN_STATUSES = /* @__PURE__ */ new Set([ "wait", "scaned", "confirmed", "expired", "scaned_but_redirect", "need_verifycode", "verify_code_blocked", "binded_redirect" ]); var WeixinApiError = class extends Error { constructor(code, message, options = {}) { super(message, options); this.name = "WeixinApiError"; this.code = code; this.status = options.status; } }; function nonEmptyString5(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function isWeixinHost(hostname) { const normalized = hostname.toLowerCase().replace(/\.$/, ""); return normalized === "weixin.qq.com" || normalized.endsWith(".weixin.qq.com"); } function normalizeWeixinApiBaseUrl(value) { let url; try { url = new URL(value); } catch { throw new WeixinApiError("invalid-base-url", "\u5FAE\u4FE1\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6548\u7684\u8FDE\u63A5\u5730\u5740\u3002"); } if (url.protocol !== "https:" || !isWeixinHost(url.hostname) || url.port !== "" && url.port !== "443") { throw new WeixinApiError("untrusted-base-url", "\u5FAE\u4FE1\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u53D7\u4FE1\u4EFB\u7684\u8FDE\u63A5\u5730\u5740\u3002"); } url.username = ""; url.password = ""; url.search = ""; url.hash = ""; if (!url.pathname.endsWith("/")) url.pathname += "/"; return url.toString(); } function normalizeWeixinQrUrl(value) { const text = nonEmptyString5(value); if (!text) throw new WeixinApiError("invalid-qr", "\u5FAE\u4FE1\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u626B\u7801\u5730\u5740\u3002"); let url; try { url = new URL(text); } catch { throw new WeixinApiError("invalid-qr", "\u5FAE\u4FE1\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6548\u7684\u626B\u7801\u5730\u5740\u3002"); } if (url.protocol !== "https:" || !isWeixinHost(url.hostname)) { throw new WeixinApiError("untrusted-qr", "\u5FAE\u4FE1\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u53D7\u4FE1\u4EFB\u7684\u626B\u7801\u5730\u5740\u3002"); } return url.toString(); } function commonHeaders() { return { "iLink-App-Id": ILINK_APP_ID, "iLink-App-ClientVersion": String(ILINK_CLIENT_VERSION) }; } function authenticatedHeaders(token) { const headers = { ...commonHeaders(), "content-type": "application/json", AuthorizationType: "ilink_bot_token", "X-WECHAT-UIN": Buffer.from(String(randomBytes(4).readUInt32BE(0)), "utf8").toString("base64") }; if (nonEmptyString5(token)) headers.Authorization = `Bearer ${token.trim()}`; return headers; } function baseInfo() { return { channel_version: WEIXIN_PROTOCOL_VERSION, bot_agent: "DeepSeekHarness/0.1.0" }; } function abortError3(signal) { if (signal?.reason instanceof Error) return signal.reason; return new DOMException("The operation was aborted", "AbortError"); } async function requestJson3(fetchImpl, { method, baseUrl, endpoint, body, token, timeoutMs = DEFAULT_TIMEOUT_MS2, signal, authenticated = true }) { const trustedBase = normalizeWeixinApiBaseUrl(baseUrl); const url = new URL(endpoint, trustedBase); if (!isWeixinHost(url.hostname)) { throw new WeixinApiError("untrusted-endpoint", "\u62D2\u7EDD\u8BBF\u95EE\u4E0D\u53D7\u4FE1\u4EFB\u7684\u5FAE\u4FE1\u670D\u52A1\u5730\u5740\u3002"); } const controller = new AbortController(); let timedOut = false; const onAbort = () => controller.abort(signal?.reason); if (signal?.aborted) throw abortError3(signal); signal?.addEventListener("abort", onAbort, { once: true }); const timer = timeoutMs > 0 ? setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs) : null; try { const response = await fetchImpl(url, { method, headers: authenticated ? authenticatedHeaders(token) : commonHeaders(), ...body === void 0 ? {} : { body: JSON.stringify(body) }, signal: controller.signal }); if (!response.ok) { throw new WeixinApiError( "http-error", `\u5FAE\u4FE1\u670D\u52A1\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${response.status}\uFF09\u3002`, { status: response.status } ); } try { return await response.json(); } catch (error) { throw new WeixinApiError("invalid-response", "\u5FAE\u4FE1\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\u3002", { cause: error }); } } catch (error) { if (signal?.aborted) throw abortError3(signal); if (timedOut) { throw new WeixinApiError("timeout", "\u5FAE\u4FE1\u670D\u52A1\u8BF7\u6C42\u8D85\u65F6\u3002", { cause: error }); } if (error instanceof WeixinApiError) throw error; throw new WeixinApiError("network-error", "\u6682\u65F6\u65E0\u6CD5\u8BBF\u95EE\u5FAE\u4FE1\u670D\u52A1\u3002", { cause: error }); } finally { if (timer) clearTimeout(timer); signal?.removeEventListener("abort", onAbort); } } function validateLoginResponse(value) { if (!value || typeof value !== "object" || !LOGIN_STATUSES.has(value.status)) { throw new WeixinApiError("invalid-login-status", "\u5FAE\u4FE1\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u626B\u7801\u72B6\u6001\u3002"); } return value; } function createWeixinApi({ fetchImpl = fetch } = {}) { if (typeof fetchImpl !== "function") throw new TypeError("fetchImpl must be a function"); return Object.freeze({ async beginLogin({ localTokens = [], botType = DEFAULT_BOT_TYPE, signal } = {}) { const tokens = [...new Set(localTokens.map(nonEmptyString5).filter(Boolean))].slice(-10); const response = await requestJson3(fetchImpl, { method: "POST", baseUrl: WEIXIN_QR_BASE_URL, endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`, body: { local_token_list: tokens }, timeoutMs: 1e4, signal }); const qrcode = nonEmptyString5(response?.qrcode); if (!qrcode) throw new WeixinApiError("invalid-qr", "\u5FAE\u4FE1\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u4E8C\u7EF4\u7801\u4EE4\u724C\u3002"); return { qrcode, qrcodeUrl: normalizeWeixinQrUrl(response.qrcode_img_content) }; }, async pollLogin({ qrcode, baseUrl = WEIXIN_QR_BASE_URL, verifyCode, signal }) { const qr = nonEmptyString5(qrcode); if (!qr) throw new TypeError("qrcode is required"); let endpoint = `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qr)}`; if (nonEmptyString5(verifyCode)) endpoint += `&verify_code=${encodeURIComponent(verifyCode.trim())}`; const response = await requestJson3(fetchImpl, { method: "GET", baseUrl, endpoint, timeoutMs: DEFAULT_LONG_POLL_TIMEOUT_MS, signal, authenticated: false }); return validateLoginResponse(response); }, async getUpdates({ baseUrl, token, getUpdatesBuf = "", timeoutMs, signal }) { try { return await requestJson3(fetchImpl, { method: "POST", baseUrl, endpoint: "ilink/bot/getupdates", body: { get_updates_buf: getUpdatesBuf, base_info: baseInfo() }, token, timeoutMs: timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS, signal }); } catch (error) { if (error instanceof WeixinApiError && error.code === "timeout") { return { ret: 0, msgs: [], get_updates_buf: getUpdatesBuf }; } throw error; } }, async sendText({ baseUrl, token, toUserId, text, contextToken, runId, signal }) { const recipient = nonEmptyString5(toUserId); const content = nonEmptyString5(text); if (!recipient || !content) throw new TypeError("toUserId and text are required"); const response = await requestJson3(fetchImpl, { method: "POST", baseUrl, endpoint: "ilink/bot/sendmessage", token, signal, body: { msg: { from_user_id: "", to_user_id: recipient, client_id: `dsh-weixin-${randomUUID11()}`, message_type: 2, message_state: 2, item_list: [{ type: 1, text_item: { text: content } }], ...nonEmptyString5(contextToken) ? { context_token: contextToken.trim() } : {}, ...nonEmptyString5(runId) ? { run_id: runId.trim() } : {} }, base_info: baseInfo() } }); if (response?.ret !== void 0 && response.ret !== 0) { throw new WeixinApiError("send-rejected", "\u5FAE\u4FE1\u670D\u52A1\u62D2\u7EDD\u4E86\u56DE\u590D\u6D88\u606F\u3002"); } return true; }, async notifyStart({ baseUrl, token, signal }) { const response = await requestJson3(fetchImpl, { method: "POST", baseUrl, endpoint: "ilink/bot/msg/notifystart", token, signal, timeoutMs: 1e4, body: { base_info: baseInfo() } }); if (response?.ret !== void 0 && response.ret !== 0) { throw new WeixinApiError("start-rejected", "\u5FAE\u4FE1\u8D26\u53F7\u8FDE\u63A5\u542F\u52A8\u5931\u8D25\u3002"); } return response; }, async notifyStop({ baseUrl, token, signal }) { return requestJson3(fetchImpl, { method: "POST", baseUrl, endpoint: "ilink/bot/msg/notifystop", token, signal, timeoutMs: 1e4, body: { base_info: baseInfo() } }); } }); } function extractWeixinText(message) { for (const item of message?.item_list ?? []) { if (item?.type === 1 && typeof item.text_item?.text === "string") { const text = item.text_item.text.trim(); if (text) return text; } if (item?.type === 3 && typeof item.voice_item?.text === "string") { const text = item.voice_item.text.trim(); if (text) return text; } } return null; } function weixinMessageId(message) { if (message?.message_id !== void 0 && message.message_id !== null) { return String(message.message_id); } return nonEmptyString5(message?.client_id); } function splitWeixinText(text, maxChars = 4e3) { if (text.length <= maxChars) return [text]; const chunks = []; let remaining = text; while (remaining.length > maxChars) { let splitAt = remaining.lastIndexOf("\n", maxChars); if (splitAt < Math.floor(maxChars * 0.6)) splitAt = maxChars; chunks.push(remaining.slice(0, splitAt)); remaining = remaining.slice(splitAt).replace(/^\n+/, ""); } if (remaining) chunks.push(remaining); return chunks; } // src/channels/weixin/config-store.mjs var EMPTY_DOCUMENT5 = Object.freeze({ version: 1, accounts: Object.freeze([]) }); function cleanString14(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function safeBotId3(value) { const id = cleanString14(value); return id && /^wx_[a-f0-9]{24}$/.test(id) ? id : null; } function safeTokenRef(value) { const ref = cleanString14(value); return ref && /^DSH_WEIXIN_BOT_TOKEN_[A-F0-9]{24}$/.test(ref) ? ref : null; } function deriveWeixinBotIdentity(accountId) { const raw = cleanString14(accountId); if (!raw) throw new TypeError("accountId is required"); const digest2 = createHash6("sha256").update(raw).digest("hex").slice(0, 24); return { botId: `wx_${digest2}`, tokenRef: `DSH_WEIXIN_BOT_TOKEN_${digest2.toUpperCase()}` }; } function maskWeixinAccountId(accountId) { const value = cleanString14(accountId) ?? ""; if (value.length <= 10) return value ? `${value.slice(0, 3)}\u2022\u2022\u2022` : "\u5FAE\u4FE1\u673A\u5668\u4EBA"; return `${value.slice(0, 6)}\u2022\u2022\u2022\u2022${value.slice(-4)}`; } function normalizeAccount(value) { if (!value || typeof value !== "object") return null; const accountId = cleanString14(value.accountId); const ownerUserId = cleanString14(value.ownerUserId); const botId = safeBotId3(value.botId); const tokenRef = safeTokenRef(value.tokenRef); if (!accountId || !ownerUserId || !botId || !tokenRef) return null; const derived = deriveWeixinBotIdentity(accountId); if (derived.botId !== botId || derived.tokenRef !== tokenRef) return null; let baseUrl; try { baseUrl = normalizeWeixinApiBaseUrl(value.baseUrl); } catch { return null; } return Object.freeze({ botId, accountId, tokenRef, ownerUserId, baseUrl, createdAt: cleanString14(value.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString(), connectedAt: cleanString14(value.connectedAt) }); } function normalizeDocument5(value) { if (!value || value.version !== 1 || !Array.isArray(value.accounts)) return null; const accounts = value.accounts.map(normalizeAccount); if (accounts.some((account) => account === null)) return null; const ids = /* @__PURE__ */ new Set(); const accountIds = /* @__PURE__ */ new Set(); const refs = /* @__PURE__ */ new Set(); for (const account of accounts) { if (ids.has(account.botId) || accountIds.has(account.accountId) || refs.has(account.tokenRef)) { return null; } ids.add(account.botId); accountIds.add(account.accountId); refs.add(account.tokenRef); } return Object.freeze({ version: 1, accounts: Object.freeze(accounts) }); } var WeixinConfigStore = class { #path; #value = EMPTY_DOCUMENT5; #writeQueue = Promise.resolve(); constructor(path) { this.#path = path; } async load() { try { const normalized = normalizeDocument5(JSON.parse(await readFile11(this.#path, "utf8"))); if (!normalized) throw new Error("dsh-weixin config contains invalid account data"); this.#value = normalized; } catch (error) { if (error?.code !== "ENOENT") throw error; this.#value = EMPTY_DOCUMENT5; } return this; } list() { return structuredClone(this.#value.accounts); } get(botId) { const account = this.#value.accounts.find((candidate) => candidate.botId === botId); return account ? structuredClone(account) : null; } getByAccountId(accountId) { const account = this.#value.accounts.find((candidate) => candidate.accountId === accountId); return account ? structuredClone(account) : null; } async save(value) { const normalized = normalizeAccount(value); if (!normalized) throw new Error("Refusing to persist incomplete dsh-weixin account data"); return this.#mutate((accounts) => { const accountCollision = accounts.find( (account) => account.accountId === normalized.accountId && account.botId !== normalized.botId ); const refCollision = accounts.find( (account) => account.tokenRef === normalized.tokenRef && account.botId !== normalized.botId ); if (accountCollision || refCollision) throw new Error("Duplicate Weixin account identity"); const index = accounts.findIndex((account) => account.botId === normalized.botId); if (index === -1) accounts.push(normalized); else accounts[index] = normalized; return structuredClone(normalized); }); } async remove(botId) { if (!safeBotId3(botId)) throw new TypeError("Invalid Weixin bot id"); return this.#mutate((accounts) => { const index = accounts.findIndex((account) => account.botId === botId); if (index === -1) return null; const [removed] = accounts.splice(index, 1); return structuredClone(removed); }); } async clear() { const operation = this.#writeQueue.then(async () => { try { await unlink15(this.#path); } catch (error) { if (error?.code !== "ENOENT") throw error; } this.#value = EMPTY_DOCUMENT5; }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; } async #mutate(mutator) { let result; const operation = this.#writeQueue.then(async () => { const accounts = [...this.#value.accounts]; result = mutator(accounts); const document = Object.freeze({ version: 1, accounts: Object.freeze(accounts) }); await this.#write(document); this.#value = document; }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; return result; } async #write(document) { await mkdir11(dirname11(this.#path), { recursive: true, mode: 448 }); const temporary = `${this.#path}.tmp`; await writeFile11(temporary, `${JSON.stringify(document, null, 2)} `, { encoding: "utf8", mode: 384 }); await rename11(temporary, this.#path); } }; // src/channels/weixin/state-store.mjs import { mkdir as mkdir12, readFile as readFile12, rename as rename12, unlink as unlink16, writeFile as writeFile12 } from "node:fs/promises"; import { dirname as dirname12 } from "node:path"; var EMPTY_STATE6 = Object.freeze({ version: 1, sessions: {}, seenMessageIds: [], getUpdatesBuf: "" }); function normalizeState5(value) { if (!value || typeof value !== "object") return structuredClone(EMPTY_STATE6); const sessions = {}; if (value.sessions && typeof value.sessions === "object" && !Array.isArray(value.sessions)) { for (const [key, sessionId] of Object.entries(value.sessions)) { if (typeof key === "string" && typeof sessionId === "string" && sessionId) { sessions[key] = sessionId; } } } return { version: 1, sessions, seenMessageIds: Array.isArray(value.seenMessageIds) ? value.seenMessageIds.filter((id) => typeof id === "string").slice(-1e3) : [], getUpdatesBuf: typeof value.getUpdatesBuf === "string" ? value.getUpdatesBuf : "" }; } var WeixinStateStore = class { #path; #state = structuredClone(EMPTY_STATE6); #writeQueue = Promise.resolve(); constructor(path) { this.#path = path; } async load() { try { this.#state = normalizeState5(JSON.parse(await readFile12(this.#path, "utf8"))); } catch (error) { if (error?.code !== "ENOENT") throw error; this.#state = structuredClone(EMPTY_STATE6); await this.#persist(); } return this; } sessionFor(key) { return this.#state.sessions[key] ?? null; } async setSession(key, sessionId) { this.#state.sessions[key] = sessionId; await this.#persist(); } async clearSession(key) { delete this.#state.sessions[key]; await this.#persist(); } hasSeen(messageId) { return this.#state.seenMessageIds.includes(messageId); } async markSeen(messageId) { if (this.hasSeen(messageId)) return; this.#state.seenMessageIds.push(messageId); if (this.#state.seenMessageIds.length > 1e3) { this.#state.seenMessageIds.splice(0, this.#state.seenMessageIds.length - 1e3); } await this.#persist(); } getUpdatesBuf() { return this.#state.getUpdatesBuf; } async setGetUpdatesBuf(value) { if (typeof value !== "string" || value === this.#state.getUpdatesBuf) return; this.#state.getUpdatesBuf = value; await this.#persist(); } snapshot() { return structuredClone(this.#state); } async remove() { try { await unlink16(this.#path); } catch (error) { if (error?.code !== "ENOENT") throw error; } this.#state = structuredClone(EMPTY_STATE6); } async #persist() { const snapshot = `${JSON.stringify(this.#state, null, 2)} `; const operation = this.#writeQueue.then(async () => { await mkdir12(dirname12(this.#path), { recursive: true, mode: 448 }); const temporary = `${this.#path}.tmp`; await writeFile12(temporary, snapshot, { encoding: "utf8", mode: 384 }); await rename12(temporary, this.#path); }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; } }; // src/channels/weixin/weixin-controller.mjs import { randomUUID as randomUUID12 } from "node:crypto"; var ACTIVE_ATTEMPT_STATES4 = /* @__PURE__ */ new Set([ "starting", "pending", "scanned", "needs_verification", "connecting" ]); var TERMINAL_ATTEMPT_STATES4 = /* @__PURE__ */ new Set(["connected", "expired", "failed", "cancelled"]); var QR_TTL_MS3 = 5 * 6e4; function cleanString15(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function abortError4() { return new DOMException("Provisioning was cancelled", "AbortError"); } function apiBaseFromServer(value, fallback) { const raw = cleanString15(value); if (!raw) return normalizeWeixinApiBaseUrl(fallback); return normalizeWeixinApiBaseUrl(raw.includes("://") ? raw : `https://${raw}`); } function publicAttempt4(record) { if (!record) return null; return { attemptId: record.id, status: record.state, ...record.verificationUrl ? { verificationUrl: record.verificationUrl } : {}, ...record.expiresAt ? { expiresAt: record.expiresAt } : {}, pollIntervalMs: 1e3, ...record.state === "needs_verification" ? { verificationRequired: true } : {}, ...record.botId ? { botId: record.botId } : {}, ...record.alreadyConnected ? { alreadyConnected: true } : {}, ...record.error ? { error: structuredClone(record.error) } : {} }; } function safeAccountError(code, message) { return Object.freeze({ code, message }); } var WeixinController = class { #api; #credentials; #configStore; #createRuntime; #deleteState; #logger; #runtimes = /* @__PURE__ */ new Map(); #errors = /* @__PURE__ */ new Map(); #attempts = /* @__PURE__ */ new Map(); #activeAttemptId = null; #transitions = /* @__PURE__ */ new Map(); #revision = 0; #closed = false; constructor({ api, credentials, configStore, createRuntime, deleteState = async () => { }, logger = console }) { if (!api || typeof api.beginLogin !== "function" || typeof api.pollLogin !== "function") { throw new TypeError("WeixinController requires a Weixin API client"); } if (!credentials || typeof credentials.resolve !== "function" || typeof credentials.set !== "function" || typeof credentials.unset !== "function") { throw new TypeError("WeixinController requires the DSH credential provider"); } if (!configStore || typeof configStore.list !== "function" || typeof configStore.save !== "function" || typeof configStore.remove !== "function") { throw new TypeError("WeixinController requires a config store"); } if (typeof createRuntime !== "function") throw new TypeError("createRuntime is required"); this.#api = api; this.#credentials = credentials; this.#configStore = configStore; this.#createRuntime = createRuntime; this.#deleteState = deleteState; this.#logger = logger; } async initialize() { if (this.#closed) return this.status(); for (const config of this.#configStore.list()) { const current = this.#runtimes.get(config.botId); if (current?.status?.ready === true) continue; await this.#withBotTransition(config.botId, async () => { const latest = this.#configStore.get(config.botId); if (!latest || this.#closed) return; try { const token = await this.#resolveToken(latest.tokenRef); if (!token) { this.#errors.set( latest.botId, safeAccountError("missing-token", "\u767B\u5F55\u51ED\u636E\u7F3A\u5931\uFF0C\u8BF7\u79FB\u9664\u8D26\u53F7\u540E\u91CD\u65B0\u626B\u7801\u3002") ); return; } await this.#startRuntime(latest, token); this.#errors.delete(latest.botId); } catch (error) { this.#errors.set( latest.botId, safeAccountError("connection-failed", "\u5FAE\u4FE1\u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u91CD\u8BD5\u3002") ); this.#logger.warn?.(`[dsh-weixin] account ${latest.botId} failed to initialize:`, error); } finally { this.#touch(); } }); } return this.status(); } async startProvisioning() { if (this.#closed) throw new Error("dsh-weixin controller is closed"); if (this.#activeAttemptId) await this.cancelProvisioning(this.#activeAttemptId); const record = { id: randomUUID12(), state: "starting", createdAt: Date.now(), expiresAt: Date.now() + QR_TTL_MS3, controller: new AbortController(), pendingVerifyCode: null, verifyResolve: null, currentBaseUrl: WEIXIN_QR_BASE_URL, error: null, botId: null, task: null }; this.#attempts.set(record.id, record); this.#activeAttemptId = record.id; this.#touch(); try { const localTokens = (await Promise.all( this.#configStore.list().slice(-10).map(async (config) => this.#resolveToken(config.tokenRef)) )).filter(Boolean); const login = await this.#api.beginLogin({ localTokens, signal: record.controller.signal }); this.#assertAttemptActive(record); record.qrcode = login.qrcode; record.verificationUrl = login.qrcodeUrl; record.state = "pending"; record.expiresAt = Date.now() + QR_TTL_MS3; this.#touch(); record.task = this.#runProvisioning(record); return publicAttempt4(record); } catch (error) { if (record.controller.signal.aborted) { record.state = "cancelled"; record.error = safeAccountError("cancelled", "\u626B\u7801\u7ED1\u5B9A\u5DF2\u53D6\u6D88\u3002"); } else { record.state = "failed"; record.error = safeAccountError( error instanceof WeixinApiError ? error.code : "qr-start-failed", error instanceof WeixinApiError ? error.message : "\u65E0\u6CD5\u751F\u6210\u5FAE\u4FE1\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" ); } if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; this.#touch(); throw error; } } registrationStatus(attemptId) { return publicAttempt4(this.#attempts.get(attemptId)); } async submitVerification(attemptId, verifyCode) { const record = this.#attempts.get(attemptId); if (!record || record.state !== "needs_verification") { throw new Error("The provisioning attempt is not waiting for a verification code"); } const code = cleanString15(verifyCode); if (!code || !/^\d{4,8}$/.test(code)) { throw new TypeError("Verification code must contain 4 to 8 digits"); } record.pendingVerifyCode = code; record.state = "scanned"; record.verifyResolve?.(); record.verifyResolve = null; this.#touch(); return publicAttempt4(record); } async cancelProvisioning(attemptId) { const record = this.#attempts.get(attemptId); if (!record) return null; if (!TERMINAL_ATTEMPT_STATES4.has(record.state)) { record.controller.abort(); record.verifyResolve?.(); record.verifyResolve = null; await record.task?.catch(() => void 0); if (!TERMINAL_ATTEMPT_STATES4.has(record.state)) record.state = "cancelled"; record.error ??= safeAccountError("cancelled", "\u626B\u7801\u7ED1\u5B9A\u5DF2\u53D6\u6D88\u3002"); } if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; this.#touch(); return publicAttempt4(record); } async reconnectBot(botId) { const config = this.#configStore.get(botId); if (!config) throw new Error("Unknown Weixin account"); await this.#withBotTransition(botId, async () => { const token = await this.#resolveToken(config.tokenRef); if (!token) throw new Error("The Weixin token is missing"); try { await this.#startRuntime(config, token); this.#errors.delete(botId); } catch (error) { this.#errors.set(botId, safeAccountError("connection-failed", "\u5FAE\u4FE1\u8FDE\u63A5\u4ECD\u672A\u5C31\u7EEA\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002")); throw error; } finally { this.#touch(); } }); return this.status(); } async deleteBot(botId) { const config = this.#configStore.get(botId); if (!config) throw new Error("Unknown Weixin account"); await this.#withBotTransition(botId, async () => { const previousToken = await this.#credentials.resolve(config.tokenRef).catch(() => void 0); await this.#stopRuntime(botId); try { await this.#credentials.unset(config.tokenRef); await this.#configStore.remove(botId); } catch (error) { if (previousToken?.value) { await this.#credentials.set(config.tokenRef, previousToken.value).catch(() => void 0); await this.#startRuntime(config, previousToken.value).catch(() => void 0); } throw new Error("Unable to remove the Weixin account safely.", { cause: error }); } try { await this.#deleteState({ botId, config }); } catch (error) { this.#logger.warn?.(`[dsh-weixin] account ${botId} state cleanup failed:`, error); } this.#errors.delete(botId); this.#touch(); }); return this.status(); } status() { const accounts = this.#configStore.list().map((config) => { const runtimeStatus2 = this.#runtimes.get(config.botId)?.status ?? null; const connected = runtimeStatus2?.ready === true && runtimeStatus2.weixinConnectionState === "connected" && runtimeStatus2.harnessReachable === true; const state = connected ? "connected" : runtimeStatus2?.weixinConnectionState === "connecting" ? "connecting" : this.#errors.has(config.botId) || runtimeStatus2?.weixinConnectionState === "failed" ? "error" : "offline"; const error = this.#errors.get(config.botId) ?? (state === "error" ? safeAccountError("connection-failed", "\u5FAE\u4FE1\u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u91CD\u8BD5\u3002") : null); return { botId: config.botId, state, connected, configured: true, bot: { name: "\u5FAE\u4FE1\u673A\u5668\u4EBA", accountIdMasked: maskWeixinAccountId(config.accountId) }, health: { status: connected ? "healthy" : state === "error" ? "error" : "offline", summary: connected ? "\u5FAE\u4FE1\u6D88\u606F\u957F\u8F6E\u8BE2\u8FD0\u884C\u6B63\u5E38" : state === "error" ? "\u5FAE\u4FE1\u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u91CD\u8BD5" : "\u5FAE\u4FE1\u8FDE\u63A5\u5F53\u524D\u79BB\u7EBF", lastCheckedAt: runtimeStatus2?.lastCheckedAt ?? null }, stats: { messagesReceived: runtimeStatus2?.messagesReceived ?? 0, messagesReplied: runtimeStatus2?.messagesReplied ?? 0 }, error: error ? structuredClone(error) : null }; }); const connectedCount = accounts.filter((account) => account.connected).length; const active = this.#activeAttemptId ? this.#attempts.get(this.#activeAttemptId) : null; return { schemaVersion: 1, revision: this.#revision, state: active && ACTIVE_ATTEMPT_STATES4.has(active.state) ? "provisioning" : accounts.length === 0 ? "disconnected" : connectedCount === accounts.length ? "connected" : connectedCount > 0 ? "degraded" : "offline", bots: accounts, totals: { configured: accounts.length, connected: connectedCount }, ...active && ACTIVE_ATTEMPT_STATES4.has(active.state) ? { provisioning: publicAttempt4(active) } : {} }; } async close() { if (this.#closed) return; this.#closed = true; if (this.#activeAttemptId) await this.cancelProvisioning(this.#activeAttemptId); await Promise.allSettled([...this.#runtimes.keys()].map((botId) => this.#stopRuntime(botId))); } async #runProvisioning(record) { try { while (!record.controller.signal.aborted && Date.now() < record.expiresAt) { if (record.state === "needs_verification" && !record.pendingVerifyCode) { await new Promise((resolve7) => { record.verifyResolve = resolve7; if (record.controller.signal.aborted) resolve7(); }); record.verifyResolve = null; this.#assertAttemptActive(record); } const response = await this.#api.pollLogin({ qrcode: record.qrcode, baseUrl: record.currentBaseUrl, verifyCode: record.pendingVerifyCode, signal: record.controller.signal }); this.#assertAttemptActive(record); if (response.status === "wait") { record.state = "pending"; } else if (response.status === "scaned") { record.pendingVerifyCode = null; record.state = "scanned"; } else if (response.status === "need_verifycode") { record.pendingVerifyCode = null; record.state = "needs_verification"; } else if (response.status === "verify_code_blocked") { record.state = "failed"; record.error = safeAccountError("verification-blocked", "\u914D\u5BF9\u7801\u591A\u6B21\u9519\u8BEF\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801\u3002"); break; } else if (response.status === "expired") { record.state = "expired"; record.error = safeAccountError("expired", "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u3002"); break; } else if (response.status === "scaned_but_redirect") { record.currentBaseUrl = apiBaseFromServer(response.redirect_host, record.currentBaseUrl); record.state = "scanned"; } else if (response.status === "binded_redirect") { const existing = this.#configStore.list().find( (config) => this.#runtimes.get(config.botId)?.status?.ready === true ) ?? this.#configStore.list()[0]; if (!existing) { record.state = "failed"; record.error = safeAccountError("already-bound", "\u8BE5\u5FAE\u4FE1\u8D26\u53F7\u5DF2\u7ED1\u5B9A\uFF0C\u4F46\u672C\u673A\u6CA1\u6709\u53EF\u6062\u590D\u7684\u51ED\u636E\u3002"); } else { record.state = "connected"; record.botId = existing.botId; record.alreadyConnected = true; } break; } else if (response.status === "confirmed") { const token = cleanString15(response.bot_token); const accountId = cleanString15(response.ilink_bot_id); const ownerUserId = cleanString15(response.ilink_user_id); if (!token || !accountId || !ownerUserId) { throw new WeixinApiError("incomplete-login", "\u5FAE\u4FE1\u6388\u6743\u6210\u529F\uFF0C\u4F46\u8FD4\u56DE\u7684\u8D26\u53F7\u51ED\u636E\u4E0D\u5B8C\u6574\u3002"); } record.state = "connecting"; this.#touch(); const baseUrl = apiBaseFromServer(response.baseurl, record.currentBaseUrl); record.botId = await this.#activateAccount(record, { token, accountId, ownerUserId, baseUrl }); record.state = "connected"; record.error = null; break; } this.#touch(); } if (!record.controller.signal.aborted && Date.now() >= record.expiresAt && !TERMINAL_ATTEMPT_STATES4.has(record.state)) { record.state = "expired"; record.error = safeAccountError("expired", "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u3002"); } } catch (error) { if (record.controller.signal.aborted || error?.name === "AbortError") { record.state = "cancelled"; record.error = safeAccountError("cancelled", "\u626B\u7801\u7ED1\u5B9A\u5DF2\u53D6\u6D88\u3002"); } else { record.state = "failed"; record.error = safeAccountError( error instanceof WeixinApiError ? error.code : "activation-failed", error instanceof WeixinApiError ? error.message : "\u5FAE\u4FE1\u5DF2\u6388\u6743\uFF0C\u4F46\u65E0\u6CD5\u4FDD\u5B58\u51ED\u636E\u6216\u542F\u52A8\u6D88\u606F\u8FDE\u63A5\u3002" ); this.#logger.error?.("[dsh-weixin] provisioning failed:", error); } } finally { record.pendingVerifyCode = null; record.verifyResolve?.(); record.verifyResolve = null; if (this.#activeAttemptId === record.id) this.#activeAttemptId = null; this.#touch(); this.#pruneAttempts(); } } async #activateAccount(record, { token, accountId, ownerUserId, baseUrl }) { const identity = deriveWeixinBotIdentity(accountId); const previousConfig = this.#configStore.getByAccountId(accountId); const config = { botId: identity.botId, accountId, tokenRef: identity.tokenRef, ownerUserId, baseUrl, createdAt: previousConfig?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(), connectedAt: (/* @__PURE__ */ new Date()).toISOString() }; const previousToken = await this.#credentials.resolve(identity.tokenRef).catch(() => void 0); return this.#withBotTransition(identity.botId, async () => { await this.#credentials.set(identity.tokenRef, token); try { this.#assertAttemptActive(record); await this.#configStore.save(config); this.#assertAttemptActive(record); await this.#startRuntime(config, token); this.#assertAttemptActive(record); this.#errors.delete(identity.botId); this.#touch(); return identity.botId; } catch (error) { await this.#stopRuntime(identity.botId); if (previousConfig) await this.#configStore.save(previousConfig).catch(() => void 0); else if (this.#configStore.get(identity.botId)) { await this.#configStore.remove(identity.botId).catch(() => void 0); } await this.#restoreCredential(identity.tokenRef, previousToken); if (previousConfig && previousToken?.value) { await this.#startRuntime(previousConfig, previousToken.value).catch(() => void 0); } throw error; } }); } async #startRuntime(config, token) { await this.#stopRuntime(config.botId); const runtime = await this.#createRuntime({ botId: config.botId, config, token }); if (!runtime || typeof runtime.start !== "function" || typeof runtime.stop !== "function") { throw new TypeError("createRuntime returned an invalid Weixin runtime"); } try { await runtime.start(); this.#runtimes.set(config.botId, runtime); } catch (error) { await runtime.stop().catch(() => void 0); throw error; } } async #stopRuntime(botId) { const runtime = this.#runtimes.get(botId); this.#runtimes.delete(botId); await runtime?.stop().catch((error) => { this.#logger.warn?.(`[dsh-weixin] account ${botId} failed to stop cleanly:`, error); }); } async #resolveToken(ref) { const result = await this.#credentials.resolve(ref).catch(() => void 0); return cleanString15(result?.value); } async #restoreCredential(ref, previous) { try { if (previous?.value) await this.#credentials.set(ref, previous.value); else await this.#credentials.unset(ref); } catch (error) { this.#logger.error?.(`[dsh-weixin] failed to restore credential ${ref}:`, error); } } #assertAttemptActive(record) { if (record.controller.signal.aborted || this.#activeAttemptId !== record.id) throw abortError4(); } #withBotTransition(botId, operation) { const previous = this.#transitions.get(botId) ?? Promise.resolve(); const current = previous.catch(() => void 0).then(operation); const settled = current.finally(() => { if (this.#transitions.get(botId) === settled) this.#transitions.delete(botId); }); this.#transitions.set(botId, settled); return settled; } #pruneAttempts() { for (const [id, record] of this.#attempts) { if (id !== this.#activeAttemptId && TERMINAL_ATTEMPT_STATES4.has(record.state) && this.#attempts.size > 16) { this.#attempts.delete(id); } } } #touch() { this.#revision += 1; } }; // src/channels/weixin/weixin-bridge.mjs var HELP_TEXT5 = [ "\u5FAE\u4FE1\u5DF2\u8FDE\u63A5 DeepSeek Harness\u3002", "", "\u76F4\u63A5\u53D1\u9001\u6587\u5B57\u6216\u5E26\u6587\u5B57\u8BC6\u522B\u7ED3\u679C\u7684\u8BED\u97F3\u5373\u53EF\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\u3002", "/new \u5F00\u542F\u4E00\u4E2A\u5168\u65B0\u4F1A\u8BDD", "/status \u68C0\u67E5\u8FDE\u63A5\u72B6\u6001", "/help \u663E\u793A\u672C\u5E2E\u52A9" ].join("\n"); function conversationKey5(userId) { return `p2p:${userId}`; } function createWeixinBridgeStatus() { return { messagesReceived: 0, messagesReplied: 0, messagesRejected: 0, lastMessageAt: null, lastReplyAt: null, lastRejectedAt: null, lastError: null }; } var WeixinHarnessBridge = class { #api; #baseUrl; #token; #ownerUserId; #harness; #state; #status; #logger; #replyTimeoutMs; #maxMessageChars; #queues = /* @__PURE__ */ new Map(); constructor({ api, baseUrl, token, ownerUserId, harness, state, status = createWeixinBridgeStatus(), logger = console, replyTimeoutMs = 6e5, maxMessageChars = 4e3 }) { if (!api || typeof api.sendText !== "function") throw new TypeError("Weixin API is required"); if (!baseUrl || !token || !ownerUserId) throw new TypeError("Weixin account credentials are required"); if (!harness || !state) throw new TypeError("Harness client and state store are required"); this.#api = api; this.#baseUrl = baseUrl; this.#token = token; this.#ownerUserId = ownerUserId; this.#harness = harness; this.#state = state; this.#status = status; this.#logger = logger; this.#replyTimeoutMs = replyTimeoutMs; this.#maxMessageChars = maxMessageChars; } get status() { return structuredClone(this.#status); } accept(message) { const sender = typeof message?.from_user_id === "string" ? message.from_user_id : ""; const previous = this.#queues.get(sender) ?? Promise.resolve(); const current = previous.catch(() => void 0).then(() => this.#process(message)).finally(() => { if (this.#queues.get(sender) === current) this.#queues.delete(sender); }); this.#queues.set(sender, current); return current; } async waitForIdle() { await Promise.allSettled([...this.#queues.values()]); } async #process(message) { if (message?.message_type === 2) return; const messageId = weixinMessageId(message); const sender = typeof message?.from_user_id === "string" ? message.from_user_id : ""; if (!messageId || !sender) return; if (this.#state.hasSeen(messageId)) return; this.#status.messagesReceived += 1; this.#status.lastMessageAt = (/* @__PURE__ */ new Date()).toISOString(); if (sender !== this.#ownerUserId) { this.#status.messagesRejected += 1; this.#status.lastRejectedAt = (/* @__PURE__ */ new Date()).toISOString(); return; } const contextToken = typeof message.context_token === "string" ? message.context_token : void 0; const runId = typeof message.run_id === "string" ? message.run_id : void 0; const text = extractWeixinText(message); try { if (!text) { await this.#send(sender, "\u76EE\u524D\u4EC5\u652F\u6301\u6587\u5B57\u6D88\u606F\uFF0C\u4EE5\u53CA\u5FAE\u4FE1\u5DF2\u8F6C\u6210\u6587\u5B57\u7684\u8BED\u97F3\u6D88\u606F\u3002", contextToken, runId); await this.#state.markSeen(messageId); return; } const command = text.trim().toLowerCase(); if (command === "/help") { await this.#send(sender, HELP_TEXT5, contextToken, runId); await this.#state.markSeen(messageId); return; } if (command === "/status") { await this.#harness.ensureRunning(); await this.#send(sender, "\u5FAE\u4FE1\u4E0E DeepSeek Harness \u8FDE\u63A5\u6B63\u5E38\u3002", contextToken, runId); await this.#state.markSeen(messageId); return; } if (command === "/new") { await this.#state.clearSession(conversationKey5(sender)); await this.#send(sender, "\u5DF2\u5F00\u542F\u65B0\u4F1A\u8BDD\u3002\u8BF7\u53D1\u9001\u4F60\u7684\u95EE\u9898\u3002", contextToken, runId); await this.#state.markSeen(messageId); return; } const key = conversationKey5(sender); let sessionId = this.#state.sessionFor(key); if (!sessionId || !await this.#harness.sessionExists(sessionId)) { sessionId = await this.#harness.createSession(); await this.#state.setSession(key, sessionId); } const answer = await this.#harness.ask(sessionId, text, { timeoutMs: this.#replyTimeoutMs }); await this.#send(sender, answer, contextToken, runId); await this.#state.markSeen(messageId); this.#status.messagesReplied += 1; this.#status.lastReplyAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.lastError = null; } catch (error) { this.#status.lastError = error?.message ?? String(error); this.#logger.error?.("[dsh-weixin] failed to process an inbound message:", error); try { await this.#send(sender, "\u6D88\u606F\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", contextToken, runId); await this.#state.markSeen(messageId); } catch (sendError) { this.#logger.error?.("[dsh-weixin] failed to send the safe error reply:", sendError); } } } async #send(toUserId, text, contextToken, runId) { for (const chunk of splitWeixinText(text, this.#maxMessageChars)) { await this.#api.sendText({ baseUrl: this.#baseUrl, token: this.#token, toUserId, text: chunk, contextToken, runId }); } } }; // src/channels/weixin/weixin-runtime.mjs function delay2(ms, signal) { return new Promise((resolve7, reject) => { if (signal?.aborted) { reject(signal.reason ?? new DOMException("Aborted", "AbortError")); return; } const finish = () => { signal?.removeEventListener("abort", onAbort); resolve7(); }; const timer = setTimeout(finish, ms); const onAbort = () => { clearTimeout(timer); signal?.removeEventListener("abort", onAbort); reject(signal.reason ?? new DOMException("Aborted", "AbortError")); }; signal?.addEventListener("abort", onAbort, { once: true }); }); } function createWeixinRuntimeStatus() { return { startedAt: null, ready: false, weixinConnectionState: "idle", harnessReachable: false, lastCheckedAt: null, lastError: null, ...createWeixinBridgeStatus() }; } var WeixinRuntime = class { #api; #config; #token; #harness; #state; #logger; #replyTimeoutMs; #maxMessageChars; #status = createWeixinRuntimeStatus(); #bridge = null; #abortController = null; #monitor = null; #starting = null; constructor({ api, config, token, harness, state, logger = console, replyTimeoutMs = 6e5, maxMessageChars = 4e3 }) { if (!api || !config || !token || !harness || !state) { throw new TypeError("WeixinRuntime requires API, account, token, Harness, and state"); } this.#api = api; this.#config = config; this.#token = token; this.#harness = harness; this.#state = state; this.#logger = logger; this.#replyTimeoutMs = replyTimeoutMs; this.#maxMessageChars = maxMessageChars; } get status() { return structuredClone(this.#status); } async start() { if (this.#status.ready && this.#monitor) return this.status; if (this.#starting) return this.#starting; this.#starting = this.#start().finally(() => { this.#starting = null; }); return this.#starting; } async #start() { await this.stop(); this.#status.startedAt = (/* @__PURE__ */ new Date()).toISOString(); this.#status.weixinConnectionState = "connecting"; this.#status.lastError = null; try { await this.#harness.ensureRunning(); this.#status.harnessReachable = true; await this.#api.notifyStart({ baseUrl: this.#config.baseUrl, token: this.#token }); this.#bridge = new WeixinHarnessBridge({ api: this.#api, baseUrl: this.#config.baseUrl, token: this.#token, ownerUserId: this.#config.ownerUserId, harness: this.#harness, state: this.#state, status: this.#status, logger: this.#logger, replyTimeoutMs: this.#replyTimeoutMs, maxMessageChars: this.#maxMessageChars }); this.#abortController = new AbortController(); this.#status.ready = true; this.#status.weixinConnectionState = "connected"; this.#status.lastCheckedAt = Date.now(); const signal = this.#abortController.signal; this.#monitor = this.#runMonitor(signal).catch((error) => { if (signal.aborted) return; this.#status.ready = false; this.#status.weixinConnectionState = "failed"; this.#status.lastError = error?.message ?? String(error); this.#logger.error?.(`[dsh-weixin] account ${this.#config.botId} monitor stopped:`, error); }); return this.status; } catch (error) { this.#status.ready = false; this.#status.weixinConnectionState = "failed"; this.#status.lastError = error?.message ?? String(error); throw error; } } async #runMonitor(signal) { let consecutiveFailures = 0; while (!signal.aborted) { try { const response = await this.#api.getUpdates({ baseUrl: this.#config.baseUrl, token: this.#token, getUpdatesBuf: this.#state.getUpdatesBuf(), signal }); if (signal.aborted) return; const rejected = response?.ret !== void 0 && response.ret !== 0 || response?.errcode !== void 0 && response.errcode !== 0; if (rejected) { const code = response.errcode ?? response.ret; throw new WeixinApiError( code === -14 ? "stale-token" : "updates-rejected", code === -14 ? "\u5FAE\u4FE1\u767B\u5F55\u51ED\u636E\u5DF2\u5931\u6548\uFF0C\u8BF7\u79FB\u9664\u8D26\u53F7\u540E\u91CD\u65B0\u626B\u7801\u3002" : "\u5FAE\u4FE1\u6D88\u606F\u540C\u6B65\u8BF7\u6C42\u88AB\u62D2\u7EDD\u3002" ); } consecutiveFailures = 0; this.#status.ready = true; this.#status.weixinConnectionState = "connected"; this.#status.lastCheckedAt = Date.now(); this.#status.lastError = null; for (const message of response?.msgs ?? []) { await this.#bridge.accept(message); } if (typeof response?.get_updates_buf === "string" && response.get_updates_buf) { await this.#state.setGetUpdatesBuf(response.get_updates_buf); } } catch (error) { if (signal.aborted) return; consecutiveFailures += 1; this.#status.lastError = error?.message ?? String(error); this.#logger.warn?.( `[dsh-weixin] account ${this.#config.botId} poll failed (${consecutiveFailures}/3):`, error ); if (error instanceof WeixinApiError && error.code === "stale-token") throw error; if (consecutiveFailures >= 3) throw error; await delay2(Math.min(2e3 * 2 ** (consecutiveFailures - 1), 1e4), signal); } } } async stop() { const monitor = this.#monitor; const bridge = this.#bridge; const wasStarted = Boolean(this.#abortController || monitor || this.#status.ready); this.#abortController?.abort(); this.#abortController = null; this.#monitor = null; await monitor?.catch(() => void 0); await bridge?.waitForIdle(); this.#bridge = null; if (wasStarted) { try { await this.#api.notifyStop({ baseUrl: this.#config.baseUrl, token: this.#token, signal: AbortSignal.timeout(1e4) }); } catch (error) { this.#logger.warn?.(`[dsh-weixin] account ${this.#config.botId} stop notification failed:`, error); } } this.#status.ready = false; this.#status.weixinConnectionState = "idle"; return this.status; } }; // plugin-src/host/channels/weixin/connection-supervisor.mjs var DEFAULT_RETRY_DELAYS_MS6 = Object.freeze([250, 1e3, 3e3, 5e3, 1e4, 3e4]); function retryDelays5(value) { if (!Array.isArray(value) || value.length === 0) return [...DEFAULT_RETRY_DELAYS_MS6]; const valid = value.filter((delay3) => Number.isFinite(delay3) && delay3 >= 0); return valid.length > 0 ? valid : [...DEFAULT_RETRY_DELAYS_MS6]; } var ConnectionSupervisor5 = class { #controller; #harness; #logger; #retryDelays; #healthyIntervalMs; #setTimeout; #clearTimeout; #timer = null; #running = null; #retryIndex = 0; #closed = false; #started = false; #ready; #resolveReady; constructor({ controller, harness, logger = console, retryDelaysMs, healthyIntervalMs = 15e3, setTimeoutImpl = setTimeout, clearTimeoutImpl = clearTimeout }) { if (!controller || typeof controller.initialize !== "function" || typeof controller.status !== "function") { throw new TypeError("ConnectionSupervisor requires a controller"); } if (!harness || typeof harness.ensureRunning !== "function") { throw new TypeError("ConnectionSupervisor requires a Harness client"); } this.#controller = controller; this.#harness = harness; this.#logger = logger; this.#retryDelays = retryDelays5(retryDelaysMs); this.#healthyIntervalMs = Number.isFinite(healthyIntervalMs) && healthyIntervalMs >= 0 ? healthyIntervalMs : 15e3; this.#setTimeout = setTimeoutImpl; this.#clearTimeout = clearTimeoutImpl; this.#ready = new Promise((resolve7) => { this.#resolveReady = resolve7; }); } get ready() { return this.#ready; } start() { if (this.#started || this.#closed) return this; this.#started = true; this.#schedule(0); return this; } async close() { if (this.#closed) return; this.#closed = true; if (this.#timer !== null) this.#clearTimeout(this.#timer); this.#timer = null; await this.#running?.catch(() => void 0); this.#resolveReady?.(null); this.#resolveReady = null; } #schedule(delayMs) { if (this.#closed) return; this.#timer = this.#setTimeout(() => { this.#timer = null; void this.#run(); }, delayMs); this.#timer?.unref?.(); } async #run() { if (this.#closed || this.#running) return; const operation = this.#reconcile(); this.#running = operation; try { await operation; } finally { if (this.#running === operation) this.#running = null; } } async #reconcile() { try { await this.#harness.ensureRunning(); if (this.#closed) return; const status = await this.#controller.initialize(); if (this.#closed) return; this.#resolveReady?.(status); this.#resolveReady = null; const { configured, connected } = status.totals; if (connected < configured) { const delayMs = this.#retryDelays[Math.min(this.#retryIndex, this.#retryDelays.length - 1)]; this.#retryIndex += 1; this.#logger.warn?.( `[dsh-weixin] ${connected}/${configured} accounts connected; retrying in ${delayMs}ms` ); this.#schedule(delayMs); return; } this.#retryIndex = 0; this.#schedule(this.#healthyIntervalMs); } catch (error) { if (this.#closed) return; const delayMs = this.#retryDelays[Math.min(this.#retryIndex, this.#retryDelays.length - 1)]; this.#retryIndex += 1; this.#logger.warn?.(`[dsh-weixin] connection reconciliation failed; retrying in ${delayMs}ms`, error); this.#schedule(delayMs); } } }; function createConnectionSupervisor5(options) { return new ConnectionSupervisor5(options); } // plugin-src/host/channels/weixin/production.mjs function harnessOrigin6(webServer, configured) { if (configured !== void 0) return new URL(configured); const port = webServer?.port; if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error("dsh-weixin requires an initialized DSH webServer port"); } return new URL(`http://127.0.0.1:${port}`); } function pluginPaths6(config) { const dshHome = resolve6(config.dshHome ?? process.env.DSH_HOME ?? join6(homedir6(), ".dsh")); const root = resolve6(config.dataDir ?? join6(dshHome, "integrations", "dsh-weixin")); return { root, config: resolve6(config.configPath ?? join6(root, "config.json")), accounts: resolve6(config.accountsDir ?? join6(root, "accounts")) }; } async function createProductionController7(ctx, config = {}, internals = {}) { if (!ctx?.credentials) throw new TypeError("dsh-weixin requires ctx.credentials"); if (!ctx?.webServer) throw new TypeError("dsh-weixin requires ctx.webServer"); const ConfigStore = internals.ConfigStore ?? WeixinConfigStore; const StateStore2 = internals.StateStore ?? WeixinStateStore; const Harness = internals.HarnessClient ?? HarnessClient2; const Controller = internals.Controller ?? WeixinController; const Runtime = internals.Runtime ?? WeixinRuntime; const api = internals.api ?? createWeixinApi(); const createSupervisor = internals.createConnectionSupervisor ?? createConnectionSupervisor5; const logger = typeof ctx.logger === "function" ? ctx.logger("dsh-weixin") : ctx.logger ?? console; const paths = pluginPaths6(config); const configStore = await new ConfigStore(paths.config).load(); const stateStores = /* @__PURE__ */ new Map(); const statePath = (botId) => resolve6(paths.accounts, botId, "state.json"); const stateFor = async (botId) => { let state = stateStores.get(botId); if (!state) { state = await new StateStore2(statePath(botId)).load(); stateStores.set(botId, state); } return state; }; const harness = new Harness({ baseUrl: harnessOrigin6(ctx.webServer, config.harnessBaseUrl), workspace: resolve6(config.workspace ?? process.cwd()), agentPreset: config.agentPreset ?? "standard", autostart: false, dshBin: config.dshBin ?? "dsh" }); const controller = new Controller({ api, credentials: ctx.credentials, configStore, logger, createRuntime: async ({ botId, config: accountConfig, token }) => { const state = await stateFor(botId); return new Runtime({ api, config: accountConfig, token, harness, state, replyTimeoutMs: config.replyTimeoutMs ?? 6e5, maxMessageChars: config.maxMessageChars ?? 4e3, logger: { error: (...args) => logger.error?.(`[${botId}]`, ...args), warn: (...args) => logger.warn?.(`[${botId}]`, ...args), info: (...args) => logger.info?.(`[${botId}]`, ...args), debug: (...args) => logger.debug?.(`[${botId}]`, ...args) } }); }, deleteState: async ({ botId }) => { const state = stateStores.get(botId); stateStores.delete(botId); if (state && typeof state.remove === "function") { await state.remove(); return; } try { await unlink17(statePath(botId)); } catch (error) { if (error?.code !== "ENOENT") throw error; } } }); const supervisor = createSupervisor({ controller, harness, logger, retryDelaysMs: config.retryDelaysMs, healthyIntervalMs: config.healthyIntervalMs }).start(); return { controller, ready: supervisor.ready, async close() { await supervisor.close(); await controller.close(); harness.stopManagedProcess(); } }; } // plugin-src/host/channels/weixin/rpc.mjs import QRCode5 from "qrcode"; var WEIXIN_RPC_CHANNEL = "/weixin"; var WEIXIN_ENDPOINTS = Object.freeze({ status: "connection.status", beginProvisioning: "provision.begin", pollProvisioning: "provision.poll", submitVerification: "provision.verify", cancelProvisioning: "provision.cancel", reconnectBot: "bot.reconnect", deleteBot: "bot.delete" }); var WEIXIN_RPC_ENDPOINTS = Object.freeze(Object.values(WEIXIN_ENDPOINTS)); function isRecord5(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function exactKeys5(value, allowed) { return isRecord5(value) && Object.keys(value).every((key) => allowed.includes(key)); } function validId5(value) { return typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value); } function payloadFailure5(endpoint, payload) { if (!isRecord5(payload)) return "Payload must be an object."; if (endpoint === WEIXIN_ENDPOINTS.status) { return exactKeys5(payload, []) ? null : "connection.status does not accept fields."; } if (endpoint === WEIXIN_ENDPOINTS.beginProvisioning) { return exactKeys5(payload, ["locale"]) && (payload.locale === void 0 || payload.locale === "zh-CN") ? null : "provision.begin received unsupported fields."; } if ([WEIXIN_ENDPOINTS.pollProvisioning, WEIXIN_ENDPOINTS.cancelProvisioning].includes(endpoint)) { return exactKeys5(payload, ["attemptId"]) && validId5(payload.attemptId) ? null : `${endpoint} requires an attemptId.`; } if (endpoint === WEIXIN_ENDPOINTS.submitVerification) { return exactKeys5(payload, ["attemptId", "verifyCode"]) && validId5(payload.attemptId) && typeof payload.verifyCode === "string" && /^\d{4,8}$/.test(payload.verifyCode) ? null : "provision.verify requires an attemptId and a 4-to-8-digit code."; } if (endpoint === WEIXIN_ENDPOINTS.reconnectBot) { return exactKeys5(payload, ["botId"]) && validId5(payload.botId) ? null : "bot.reconnect requires a botId."; } if (endpoint === WEIXIN_ENDPOINTS.deleteBot) { return exactKeys5(payload, ["botId", "confirm"]) && validId5(payload.botId) && payload.confirm === true ? null : "bot.delete requires a botId and confirm=true."; } return "Unknown Weixin endpoint."; } function badRequest3(message) { return { ok: false, error: { code: "bad-request", message } }; } function cancelled3() { return { ok: false, error: { code: "cancelled", message: "The request was cancelled." } }; } function internalFailure3() { return { ok: false, error: { code: "weixin-operation-failed", message: "\u5FAE\u4FE1\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" } }; } async function qrDataUrl4(value) { return QRCode5.toDataURL(value, { type: "image/png", errorCorrectionLevel: "M", margin: 2, width: 320 }); } async function withEncodedQr4(value, encodeQr) { if (!value || !value.verificationUrl) return value; return { ...value, qrCodeDataUrl: await encodeQr(value.verificationUrl) }; } async function publicStatus4(status, encodeQr) { const safe = structuredClone(status); if (safe.provisioning) safe.provisioning = await withEncodedQr4(safe.provisioning, encodeQr); return safe; } function assertController3(controller) { if (!controller || typeof controller.status !== "function" || typeof controller.startProvisioning !== "function" || typeof controller.registrationStatus !== "function" || typeof controller.submitVerification !== "function" || typeof controller.cancelProvisioning !== "function" || typeof controller.reconnectBot !== "function" || typeof controller.deleteBot !== "function") { throw new TypeError("A complete Weixin controller is required"); } } function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl4 } = {}) { assertController3(controller); const qrCache = /* @__PURE__ */ new Map(); const cachedEncode = (url) => { let encoded = qrCache.get(url); if (!encoded) { if (qrCache.size >= 16) qrCache.delete(qrCache.keys().next().value); encoded = Promise.resolve().then(() => encodeQr(url)); qrCache.set(url, encoded); } return encoded; }; return async (endpoint, payload, signal) => { if (signal?.aborted) return cancelled3(); if (!WEIXIN_RPC_ENDPOINTS.includes(endpoint)) return badRequest3("Unknown Weixin endpoint."); const invalid = payloadFailure5(endpoint, payload); if (invalid) return badRequest3(invalid); try { let value; if (endpoint === WEIXIN_ENDPOINTS.status) { value = await publicStatus4(await controller.status(), cachedEncode); } else if (endpoint === WEIXIN_ENDPOINTS.beginProvisioning) { const started = await controller.startProvisioning(); if (signal?.aborted) { await controller.cancelProvisioning(started.attemptId); return cancelled3(); } value = await withEncodedQr4(started, cachedEncode); } else if (endpoint === WEIXIN_ENDPOINTS.pollProvisioning) { const current = await controller.registrationStatus(payload.attemptId); if (!current) return badRequest3("The provisioning attempt no longer exists."); value = await withEncodedQr4(current, cachedEncode); } else if (endpoint === WEIXIN_ENDPOINTS.submitVerification) { value = await withEncodedQr4( await controller.submitVerification(payload.attemptId, payload.verifyCode), cachedEncode ); } else if (endpoint === WEIXIN_ENDPOINTS.cancelProvisioning) { value = await controller.cancelProvisioning(payload.attemptId); if (!value) return badRequest3("The provisioning attempt no longer exists."); } else if (endpoint === WEIXIN_ENDPOINTS.reconnectBot) { value = await publicStatus4(await controller.reconnectBot(payload.botId), cachedEncode); } else { value = await publicStatus4(await controller.deleteBot(payload.botId), cachedEncode); } return signal?.aborted ? cancelled3() : { ok: true, value }; } catch { return signal?.aborted ? cancelled3() : internalFailure3(); } }; } function installWeixinRpc(ctx, controller, options, authority) { if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== "function") { throw new TypeError("DSH Host Connection RPC is required"); } return ctx.connection.rpc.handle( WEIXIN_RPC_CHANNEL, createWeixinRpcHandler(controller, options), { authority: resolveRpcAuthority(authority) } ); } // plugin-src/host/channels/weixin/index.mjs async function apply7(ctx, config = {}) { if (config?.controller) { return installWeixinRpc(ctx, config.controller, config.rpcOptions, config.rpcAuthority); } const production = await createProductionController7(ctx, config, config.internals); const disposeRpc = installWeixinRpc( ctx, production.controller, config.rpcOptions, config.rpcAuthority ); ctx.effect(() => async () => { await production.close(); }, "dsh-weixin: close account connections"); return disposeRpc; } // plugin-src/host/index.mjs var name = "dsh-im-host"; var inject = ["connection", "credentials", "webServer"]; function channelConfig(config, name2) { const channel = config[name2] ?? {}; return config.rpcAuthority === void 0 ? channel : { ...channel, rpcAuthority: config.rpcAuthority }; } function createImHostPlugin(internals = {}) { const startFeishu = internals.applyFeishu ?? apply3; const startWeixin = internals.applyWeixin ?? apply7; const startDingtalk = internals.applyDingtalk ?? apply; const startWecom = internals.applyWecom ?? apply6; const startQq = internals.applyQq ?? apply4; const startTelegram = internals.applyTelegram ?? apply5; const startDiscord = internals.applyDiscord ?? apply2; return Object.freeze({ name, inject, async apply(ctx, config = {}) { await startFeishu(ctx, channelConfig(config, "feishu")); await startWeixin(ctx, channelConfig(config, "weixin")); await startDingtalk(ctx, channelConfig(config, "dingtalk")); await startWecom(ctx, channelConfig(config, "wecom")); await startQq(ctx, channelConfig(config, "qq")); await startTelegram(ctx, channelConfig(config, "telegram")); await startDiscord(ctx, channelConfig(config, "discord")); } }); } async function apply8(ctx, config = {}) { return createImHostPlugin().apply(ctx, config); } export { apply8 as apply, createImHostPlugin, inject, name };