/** * The `connect` service (`ctx.connect`): adapter registry, inbound routing, * authorization, and the proactive `notify` entry point for work arrangement. * @module dsh-connect/service */ import { Service, type Context } from "@deepseek-ai/cordis"; import type { ChannelAdapter, InboundMessage } from "./types.js"; import { type ConnectConfig, type ResolvedConnectConfig } from "./runner.js"; import { BindingStore } from "./binding.js"; import { ReminderStore } from "./scheduler.js"; /** Shared configuration loaded from dsh.shared.config.json */ interface SharedConfig { workspace?: { defaultWorkDir?: string; additionalWorkspaces?: string[]; }; state?: { stateDir?: string; }; mirror?: { autoCreate?: boolean; }; language?: "zh" | "en"; } /** * Load shared configuration from project root or current directory. * * Exported for the settings pane's provenance note: it needs to say *which* * keys a shared config is overriding, and answering that from a second copy of * this lookup would drift from the merge below the moment either changed. */ export declare function loadSharedConfig(): SharedConfig; export declare class ConnectService extends Service { readonly config: ResolvedConnectConfig; /** * General-settings keys a `dsh.shared.config.json` on this machine actually * sets, and which therefore win over whatever the profile says. Computed once * beside the merge so the two cannot disagree. */ private readonly overriddenKeys; private readonly adapters; private readonly runners; private readonly bindings; private readonly interaction; /** Sliding-window duplicate guard for re-delivered inbound events. */ private readonly dedup; /** Persistent chat-level reminders (`/remind`) + the delivery loop. */ readonly reminders: ReminderStore; private reminderTimer?; private workspacesRegistered; constructor(ctx: Context, config?: ConnectConfig | null); /** * General-settings keys a shared config overrides on this machine. The pane * shows these rows as editable still, but annotates them, because a save that * appears to do nothing and explains nothing is the one outcome to avoid. */ sharedOverrideKeys(): string[]; /** Poll the reminder store and deliver anything that has come due. */ private startReminderLoop; /** Deliver every due reminder and mark it fired (kept unfired on failure to retry). */ private fireDueReminders; /** Expose the binding store so adapters (e.g. the web channel) can monitor mirror sessions. */ get bindingStore(): BindingStore; /** * Register every connect work directory as a DSH workspace (idempotent). * * DSH's Web GUI groups sessions by workspace: a session is only visible * under a workspace when its header `cwd` matches the workspace path AND it * is present in the workspace's session account. Without this step the * Feishu-created `connect-*` sessions never appear in the Web GUI's * workspace browser, even though they live in the shared session store. * * This method also back-fills the workspace session account with every * session the bindings file already knows about, so history created before * this feature shows up too. */ ensureWorkspacesRegistered(): Promise; /** Register a channel adapter; its inbound messages route through here. */ registerAdapter(adapter: ChannelAdapter): void; /** * Tear a channel down: stop its adapter and dispose every runner it owns. * * Used when a settings change drops a channel from `channels[]`, so the old * transport must release its connection before a replacement (a reconfigured * version of the same channel) starts — two live Feishu long connections * would double-deliver every event. * * The adapter is removed from the map *first*: a teardown that throws must * still leave the channel unregistered, otherwise a partially stopped * adapter keeps receiving inbound traffic. * * Session locks are released by the runners; chat bindings and history are * left alone, so re-enabling the channel resumes the same conversations. */ unregisterAdapter(id: string): Promise; getAdapter(id: string): ChannelAdapter | undefined; /** Allowlist gate for inbound messages (users + chats). Empty list = allow all. */ isAllowed(msg: InboundMessage): boolean; /** * Adapter-side pre-check (channel + chat key + sender key only, no message * object). Channel adapters call this *before* downloading message resources * so rejected senders' files never touch disk. */ isChatAllowed(channel: string, chatKey: string, senderKey: string): boolean; /** Entry point every adapter forwards normalized messages into. */ handleInbound(msg: InboundMessage): Promise; /** * Route a message to the runner of its (channel, chatKey) pair, creating * the runner lazily. Also used to replay queued mirror-lock messages into * the runner of their own channel instead of whichever runner released * the lock. */ private routeInbound; /** * Proactive push for work arrangement: goals/jobs/schedule hooks call this to * deliver progress or completion notices back to a bound chat. */ notify(channel: string, chatKey: string, chatType: "p2p" | "group", markdown: string): Promise; /** * Broadcast a message to every bound chat. Admin-gated: requires a * non-empty `allowUsers` and a sender listed in it. Returns how many * chats received the message; per-chat delivery failures are skipped. */ broadcast(senderKey: string, markdown: string): Promise<{ ok: boolean; reason?: "disabled" | "not-admin"; count?: number; }>; } export {}; //# sourceMappingURL=service.d.ts.map