// 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 requestSignal = 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: requestSignal }); } 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); } } /** 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((resolve4, reject) => { if (signal?.aborted) { reject(abortError2(signal)); return; } const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve4(); }, 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: delay2 = 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 delay2 !== "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 delay2(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 delay2(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, delay2) => globalThis.setTimeout(callback, delay2), 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 delay2 = Math.max(0, lastUpdateAt + updateIntervalMs - readClock()); if (delay2 > 0) { scheduledUpdate = timer.setTimeout(() => { scheduledUpdate = null; launchUpdate(); }, delay2); 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 = (progressText2) => { requiredText(progressText2, "progressText"); if (phase !== "active") return; pendingText = progressText2; 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((resolve4, 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); resolve4(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((resolve4, reject) => { const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve4(); }, 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((resolve4, reject) => { if (signal?.aborted) { reject(signal.reason ?? new DOMException("Aborted", "AbortError")); return; } const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve4(); }, 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((delay2) => Number.isFinite(delay2) && delay2 >= 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((resolve4) => { this.#resolveReady = resolve4; }); } 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"; var DINGTALK_RPC_CHANNEL = "/dingtalk"; var DINGTALK_ENDPOINTS = Object.freeze({ status: "connection.status", beginProvisioning: "provision.begin", pollProvisioning: "provision.poll", cancelProvisioning: "provision.cancel", 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 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.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", "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.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) { 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: "loopback" } ); } // plugin-src/host/channels/dingtalk/index.mjs async function apply(ctx, config = {}) { if (config?.controller) return installDingtalkRpc(ctx, config.controller, config.rpcOptions); const production = await createProductionController(ctx, config, config.internals); const disposeRpc = installDingtalkRpc(ctx, production.controller, config.rpcOptions); ctx.effect(() => async () => { await production.close(); }, "dsh-dingtalk: close 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 homedir2 } from "node:os"; import { join as join2, resolve as resolve2 } from "node:path"; import { unlink as unlink5 } from "node:fs/promises"; import * as Lark from "@larksuiteoapi/node-sdk"; // plugin-src/host/channels/feishu/connection-supervisor.mjs var DEFAULT_RETRY_DELAYS_MS2 = 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_MS2]; const delays = value.map((delay2) => safeDelay(delay2, -1)).filter((delay2) => delay2 >= 0); return delays.length > 0 ? delays : [...DEFAULT_RETRY_DELAYS_MS2]; } 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((resolve4) => { this.#resolveReady = resolve4; }); } 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 delay2 = 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 ${delay2}ms` ); this.#schedule(delay2); 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 delay2 = this.#retryDelaysMs[Math.min(this.#retryIndex, this.#retryDelaysMs.length - 1)]; this.#retryIndex += 1; this.#logger.warn?.( `[dsh-feishu] ${message}; retrying automatically in ${delay2}ms`, error ); this.#schedule(delay2); } }; 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; 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((resolve4, 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); resolve4(); }; 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 spawn2 } from "node:child_process"; import { randomUUID as randomUUID6 } from "node:crypto"; var sleep2 = (ms) => new Promise((resolve4) => setTimeout(resolve4, 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 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 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 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, 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-${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("[bridge] 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 = `feishu-${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("[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 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 LEGACY_FEISHU_SECRET_REF = "DSH_FEISHU_APP_SECRET"; function cleanString4(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function safeId(value) { const id = cleanString4(value); return id && /^[A-Za-z0-9_-]{1,128}$/.test(id) ? id : null; } function legacyBotId(appId) { return `bot_${createHash2("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(cleanString4).filter(Boolean))]; } function normalizeBot2(value, { legacy = false } = {}) { if (!value || typeof value !== "object") return null; const appId = cleanString4(value.appId); const ownerOpenIds = normalizeOwners(value); if (!appId || ownerOpenIds.length === 0) return null; const id = safeId(value.id) ?? (legacy ? legacyBotId(appId) : null); const secretRef = cleanString4(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: cleanString4(value.botName), botOpenId: cleanString4(value.botOpenId), activated: value.activated ?? null, deletionPending: value.deletionPending === true, connectedAt: cleanString4(value.connectedAt), createdAt: cleanString4(value.createdAt) ?? cleanString4(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 readFile3(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(cleanString4(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 unlink4(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 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/feishu/multi-bot-controller.mjs import { randomUUID as randomUUID7 } 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"]); 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_${randomUUID7().replaceAll("-", "")}`; } function makeRegistrationId() { return `reg_${randomUUID7().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 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 mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "node:fs/promises"; import { dirname as dirname4 } from "node:path"; var EMPTY_STATE2 = Object.freeze({ version: 1, sessions: {}, seenMessageIds: [] }); var StateStore = class { #path; #state = structuredClone(EMPTY_STATE2); #writeQueue = Promise.resolve(); constructor(path) { this.#path = path; } async load() { try { const parsed = JSON.parse(await readFile4(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 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); }); await this.#writeQueue; } }; // plugin-src/host/channels/feishu/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-feishu requires an initialized DSH webServer port"); } return new URL(`http://127.0.0.1:${port}`); } function pluginPaths2(config) { const dshHome = resolve2(config.dshHome ?? process.env.DSH_HOME ?? join2(homedir2(), ".dsh")); const root = resolve2(config.dataDir ?? join2(dshHome, "integrations", "dsh-feishu")); return { root, config: resolve2(config.configPath ?? join2(root, "config.json")), legacyState: resolve2(config.statePath ?? join2(root, "state.json")), bots: resolve2(config.botsDir ?? join2(root, "bots")) }; } async function createProductionController2(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 ?? HarnessClient2; 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 = pluginPaths2(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 : resolve2(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: harnessOrigin2(ctx.webServer, config.harnessBaseUrl), workspace: resolve2(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 unlink5(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", 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 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.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((resolve4, reject) => { if (signal?.aborted) { reject(signal.reason ?? new Error("aborted")); return; } const timer = setTimeout(done, milliseconds); timer.unref?.(); function done() { signal?.removeEventListener("abort", aborted); resolve4(); } 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 payloadFailure3 = validPayload(endpoint, payload); if (payloadFailure3) return badRequest2(payloadFailure3); 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.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) { 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: "loopback" } ); } // 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 apply2(ctx, config = {}) { const controller = controllerFrom(ctx, config); if (controller) return installFeishuRpc(ctx, controller); const production = await createProductionController2(ctx, config); const disposeRpc = installFeishuRpc(ctx, production.controller); ctx.effect(() => async () => { await production.close(); }, "dsh-feishu: close controller and live connection"); return disposeRpc; } // plugin-src/host/channels/weixin/production.mjs import { unlink as unlink8 } from "node:fs/promises"; import { homedir as homedir3 } from "node:os"; import { join as join3, resolve as resolve3 } from "node:path"; // src/channels/weixin/config-store.mjs import { createHash as createHash3 } from "node:crypto"; import { mkdir as mkdir5, readFile as readFile5, rename as rename5, unlink as unlink6, writeFile as writeFile5 } from "node:fs/promises"; import { dirname as dirname5 } from "node:path"; // src/channels/weixin/weixin-api.mjs import { randomBytes, randomUUID as randomUUID8 } 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 requestJson2(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 requestJson2(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 requestJson2(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 requestJson2(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 requestJson2(fetchImpl, { method: "POST", baseUrl, endpoint: "ilink/bot/sendmessage", token, signal, body: { msg: { from_user_id: "", to_user_id: recipient, client_id: `dsh-weixin-${randomUUID8()}`, 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 requestJson2(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 requestJson2(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_DOCUMENT2 = Object.freeze({ version: 1, accounts: Object.freeze([]) }); function cleanString5(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function safeBotId2(value) { const id = cleanString5(value); return id && /^wx_[a-f0-9]{24}$/.test(id) ? id : null; } function safeTokenRef(value) { const ref = cleanString5(value); return ref && /^DSH_WEIXIN_BOT_TOKEN_[A-F0-9]{24}$/.test(ref) ? ref : null; } function deriveWeixinBotIdentity(accountId) { const raw = cleanString5(accountId); if (!raw) throw new TypeError("accountId is required"); const digest2 = createHash3("sha256").update(raw).digest("hex").slice(0, 24); return { botId: `wx_${digest2}`, tokenRef: `DSH_WEIXIN_BOT_TOKEN_${digest2.toUpperCase()}` }; } function maskWeixinAccountId(accountId) { const value = cleanString5(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 = cleanString5(value.accountId); const ownerUserId = cleanString5(value.ownerUserId); const botId = safeBotId2(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: cleanString5(value.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString(), connectedAt: cleanString5(value.connectedAt) }); } function normalizeDocument3(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_DOCUMENT2; #writeQueue = Promise.resolve(); constructor(path) { this.#path = path; } async load() { try { const normalized = normalizeDocument3(JSON.parse(await readFile5(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_DOCUMENT2; } 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 (!safeBotId2(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 unlink6(this.#path); } catch (error) { if (error?.code !== "ENOENT") throw error; } this.#value = EMPTY_DOCUMENT2; }); 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 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/weixin/harness-client.mjs import { spawn as spawn3 } from "node:child_process"; import { randomUUID as randomUUID9 } from "node:crypto"; var sleep3 = (ms) => new Promise((resolve4) => setTimeout(resolve4, 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 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 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 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 = "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-${randomUUID9()}`; 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("[dsh-weixin] 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 = `weixin-${randomUUID9()}`; 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("[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/weixin/state-store.mjs import { mkdir as mkdir6, readFile as readFile6, rename as rename6, unlink as unlink7, writeFile as writeFile6 } from "node:fs/promises"; import { dirname as dirname6 } from "node:path"; var EMPTY_STATE3 = Object.freeze({ version: 1, sessions: {}, seenMessageIds: [], getUpdatesBuf: "" }); function normalizeState2(value) { if (!value || typeof value !== "object") return structuredClone(EMPTY_STATE3); 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_STATE3); #writeQueue = Promise.resolve(); constructor(path) { this.#path = path; } async load() { try { this.#state = normalizeState2(JSON.parse(await readFile6(this.#path, "utf8"))); } catch (error) { if (error?.code !== "ENOENT") throw error; this.#state = structuredClone(EMPTY_STATE3); 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 unlink7(this.#path); } catch (error) { if (error?.code !== "ENOENT") throw error; } this.#state = structuredClone(EMPTY_STATE3); } async #persist() { const snapshot = `${JSON.stringify(this.#state, null, 2)} `; const operation = 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); }); this.#writeQueue = operation.then(() => void 0, () => void 0); await operation; } }; // src/channels/weixin/weixin-controller.mjs import { randomUUID as randomUUID10 } from "node:crypto"; var ACTIVE_ATTEMPT_STATES2 = /* @__PURE__ */ new Set([ "starting", "pending", "scanned", "needs_verification", "connecting" ]); var TERMINAL_ATTEMPT_STATES2 = /* @__PURE__ */ new Set(["connected", "expired", "failed", "cancelled"]); var QR_TTL_MS = 5 * 6e4; function cleanString6(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 = cleanString6(value); if (!raw) return normalizeWeixinApiBaseUrl(fallback); return normalizeWeixinApiBaseUrl(raw.includes("://") ? raw : `https://${raw}`); } function publicAttempt2(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: randomUUID10(), state: "starting", createdAt: Date.now(), expiresAt: Date.now() + QR_TTL_MS, 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_MS; this.#touch(); record.task = this.#runProvisioning(record); return publicAttempt2(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 publicAttempt2(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 = cleanString6(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 publicAttempt2(record); } async cancelProvisioning(attemptId) { const record = this.#attempts.get(attemptId); if (!record) return null; if (!TERMINAL_ATTEMPT_STATES2.has(record.state)) { record.controller.abort(); record.verifyResolve?.(); record.verifyResolve = null; await record.task?.catch(() => void 0); if (!TERMINAL_ATTEMPT_STATES2.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 publicAttempt2(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_STATES2.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_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.#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((resolve4) => { record.verifyResolve = resolve4; if (record.controller.signal.aborted) resolve4(); }); 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 = cleanString6(response.bot_token); const accountId = cleanString6(response.ilink_bot_id); const ownerUserId = cleanString6(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_STATES2.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 cleanString6(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_STATES2.has(record.state) && this.#attempts.size > 16) { this.#attempts.delete(id); } } } #touch() { this.#revision += 1; } }; // src/channels/weixin/weixin-bridge.mjs var HELP_TEXT3 = [ "\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 conversationKey3(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_TEXT3, 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(conversationKey3(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 = conversationKey3(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 delay(ms, signal) { return new Promise((resolve4, reject) => { if (signal?.aborted) { reject(signal.reason ?? new DOMException("Aborted", "AbortError")); return; } const finish = () => { signal?.removeEventListener("abort", onAbort); resolve4(); }; 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 delay(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_MS3 = Object.freeze([250, 1e3, 3e3, 5e3, 1e4, 3e4]); function retryDelays2(value) { if (!Array.isArray(value) || value.length === 0) return [...DEFAULT_RETRY_DELAYS_MS3]; const valid = value.filter((delay2) => Number.isFinite(delay2) && delay2 >= 0); return valid.length > 0 ? valid : [...DEFAULT_RETRY_DELAYS_MS3]; } 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 = retryDelays2(retryDelaysMs); this.#healthyIntervalMs = Number.isFinite(healthyIntervalMs) && healthyIntervalMs >= 0 ? healthyIntervalMs : 15e3; this.#setTimeout = setTimeoutImpl; this.#clearTimeout = clearTimeoutImpl; this.#ready = new Promise((resolve4) => { this.#resolveReady = resolve4; }); } 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 createConnectionSupervisor3(options) { return new ConnectionSupervisor3(options); } // plugin-src/host/channels/weixin/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-weixin 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-weixin")); return { root, config: resolve3(config.configPath ?? join3(root, "config.json")), accounts: resolve3(config.accountsDir ?? join3(root, "accounts")) }; } async function createProductionController3(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 ?? HarnessClient3; const Controller = internals.Controller ?? WeixinController; const Runtime = internals.Runtime ?? WeixinRuntime; const api = internals.api ?? createWeixinApi(); const createSupervisor = internals.createConnectionSupervisor ?? createConnectionSupervisor3; const logger = typeof ctx.logger === "function" ? ctx.logger("dsh-weixin") : ctx.logger ?? console; const paths = pluginPaths3(config); const configStore = await new ConfigStore(paths.config).load(); const stateStores = /* @__PURE__ */ new Map(); const statePath = (botId) => resolve3(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: harnessOrigin3(ctx.webServer, config.harnessBaseUrl), workspace: resolve3(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 unlink8(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 QRCode3 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 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 payloadFailure2(endpoint, payload) { if (!isRecord2(payload)) return "Payload must be an object."; if (endpoint === WEIXIN_ENDPOINTS.status) { return exactKeys2(payload, []) ? null : "connection.status does not accept fields."; } if (endpoint === WEIXIN_ENDPOINTS.beginProvisioning) { return exactKeys2(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 exactKeys2(payload, ["attemptId"]) && validId2(payload.attemptId) ? null : `${endpoint} requires an attemptId.`; } if (endpoint === WEIXIN_ENDPOINTS.submitVerification) { return exactKeys2(payload, ["attemptId", "verifyCode"]) && validId2(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 exactKeys2(payload, ["botId"]) && validId2(payload.botId) ? null : "bot.reconnect requires a botId."; } if (endpoint === WEIXIN_ENDPOINTS.deleteBot) { return exactKeys2(payload, ["botId", "confirm"]) && validId2(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 qrDataUrl2(value) { return QRCode3.toDataURL(value, { type: "image/png", errorCorrectionLevel: "M", margin: 2, width: 320 }); } async function withEncodedQr2(value, encodeQr) { if (!value || !value.verificationUrl) return value; return { ...value, qrCodeDataUrl: await encodeQr(value.verificationUrl) }; } async function publicStatus2(status, encodeQr) { const safe = structuredClone(status); if (safe.provisioning) safe.provisioning = await withEncodedQr2(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 = qrDataUrl2 } = {}) { 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 = payloadFailure2(endpoint, payload); if (invalid) return badRequest3(invalid); try { let value; if (endpoint === WEIXIN_ENDPOINTS.status) { value = await publicStatus2(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 withEncodedQr2(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 withEncodedQr2(current, cachedEncode); } else if (endpoint === WEIXIN_ENDPOINTS.submitVerification) { value = await withEncodedQr2( 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 publicStatus2(await controller.reconnectBot(payload.botId), cachedEncode); } else { value = await publicStatus2(await controller.deleteBot(payload.botId), cachedEncode); } return signal?.aborted ? cancelled3() : { ok: true, value }; } catch { return signal?.aborted ? cancelled3() : internalFailure3(); } }; } function installWeixinRpc(ctx, controller, options) { 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: "loopback" } ); } // plugin-src/host/channels/weixin/index.mjs async function apply3(ctx, config = {}) { if (config?.controller) return installWeixinRpc(ctx, config.controller, config.rpcOptions); const production = await createProductionController3(ctx, config, config.internals); const disposeRpc = installWeixinRpc(ctx, production.controller, config.rpcOptions); 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 createImHostPlugin(internals = {}) { const startFeishu = internals.applyFeishu ?? apply2; const startWeixin = internals.applyWeixin ?? apply3; const startDingtalk = internals.applyDingtalk ?? apply; return Object.freeze({ name, inject, async apply(ctx, config = {}) { await startFeishu(ctx, config.feishu ?? {}); await startWeixin(ctx, config.weixin ?? {}); await startDingtalk(ctx, config.dingtalk ?? {}); } }); } async function apply4(ctx, config = {}) { return createImHostPlugin().apply(ctx, config); } export { apply4 as apply, createImHostPlugin, inject, name };