@lijian-ui/dsh-im-gateway 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,304 @@
1
+ import { Context, Context as Context$1, Service } from "@deepseek-ai/cordis";
2
+ import Schema from "@deepseek-ai/schemastery";
3
+ import * as _deepseek_ai_cosmokit0 from "@deepseek-ai/cosmokit";
4
+
5
+ //#region src/gateway/types.d.ts
6
+ /** An attached image. `data` is raw base64 WITHOUT the `data:` prefix. */
7
+ interface ImImage {
8
+ mediaType: string;
9
+ data: string;
10
+ }
11
+ /** A normalized inbound message coming from any channel. */
12
+ interface ImInboundMessage {
13
+ /** Channel id, e.g. 'dingtalk'. Must match the adapter's `id`. */
14
+ channelId: string;
15
+ /** Channel-specific conversation / chat id. */
16
+ conversationId: string;
17
+ /** End-user id inside the channel. */
18
+ userId: string;
19
+ /** Text body (slash-command prefixes are still present here; the gateway strips them). */
20
+ text: string;
21
+ /** Attached images, if any. */
22
+ images?: ImImage[];
23
+ /** Display name of the sender (group chats) — used to attribute who spoke. */
24
+ senderNick?: string;
25
+ /** Whether the inbound arrived in a group conversation. */
26
+ isGroup?: boolean;
27
+ }
28
+ /** Runtime status of a channel connection, surfaced to the settings UI. */
29
+ interface ChannelStatus {
30
+ /** Connection state. */
31
+ status: 'online' | 'offline' | 'error';
32
+ /** Present when `status === 'error'`. */
33
+ error?: string;
34
+ /** Epoch ms of the last state change. */
35
+ lastChange: number;
36
+ }
37
+ /**
38
+ * A channel adapter. Each IM backend (DingTalk / QQ / Weixin) implements this
39
+ * and registers itself via `ctx.imGateway.registerChannel(adapter)`.
40
+ *
41
+ * Lifecycle: the gateway calls `start()` once the core service is active and
42
+ * `stop()` on teardown. The adapter pushes inbound messages back through
43
+ * `ctx.imGateway.handleInbound(message)`.
44
+ */
45
+ interface ImChannelAdapter {
46
+ /** Stable channel id (e.g. 'dingtalk'). Used as the registry key and in session mapping. */
47
+ readonly id: string;
48
+ /** Human-readable label for logs / settings UI. */
49
+ readonly label: string;
50
+ /** Connect to the backend (open WS / start polling). */
51
+ start(): Promise<void> | void;
52
+ /** Disconnect and release resources. */
53
+ stop(): Promise<void> | void;
54
+ /** Push a text reply to a conversation. */
55
+ sendText(conversationId: string, text: string): Promise<void>;
56
+ /** Push an image reply. Optional — channels without media support may omit. */
57
+ sendImage?(conversationId: string, image: ImImage): Promise<void>;
58
+ /** Whether the adapter is currently connected. */
59
+ isActive(): boolean;
60
+ /**
61
+ * Optional streaming hook. The gateway calls this with incremental text
62
+ * (throttled by `streamThrottleMs`) so card-based channels (DingTalk) can
63
+ * update an AI card instead of sending discrete messages. Channels without
64
+ * this hook receive the full reply via `sendText` on `turn/end`.
65
+ */
66
+ updateCard?(conversationId: string, partialText: string): Promise<void>;
67
+ /**
68
+ * Optional AI-card streaming lifecycle — the richer sibling of `updateCard`.
69
+ * A channel that implements these three methods gets a full streaming card:
70
+ * beginStream (create + deliver the card, once per reply cycle)
71
+ * streamText (incremental content updates, throttled by the gateway)
72
+ * endStream (finalize FINISHED; must be called even with no text)
73
+ * Channels without them fall back to a single `sendText` at `turn/end`.
74
+ */
75
+ beginStream?(conversationId: string): Promise<void>;
76
+ streamText?(conversationId: string, text: string, finished?: boolean): Promise<void>;
77
+ endStream?(conversationId: string, text: string): Promise<void>;
78
+ /**
79
+ * Optional pre-filter. Called by the gateway for every inbound message
80
+ * BEFORE it is forwarded to the agent. Return `true` to suppress forwarding
81
+ * (e.g. the message was a slash command handled locally).
82
+ */
83
+ onInbound?(message: ImInboundMessage): boolean | Promise<boolean>;
84
+ /**
85
+ * Optional status sink. The gateway assigns this on registration; the adapter
86
+ * must call it whenever its connection state changes (connected / closed /
87
+ * error) so the settings page can render online / offline / error badges.
88
+ */
89
+ statusListener?(status: ChannelStatus): void;
90
+ }
91
+ /** DingTalk channel credentials (per instance). */
92
+ interface DingtalkChannelConfig {
93
+ clientId: string;
94
+ clientSecret: string;
95
+ callbackBaseUrl: string;
96
+ }
97
+ /** QQ channel credentials (per instance). */
98
+ interface QqChannelConfig {
99
+ appId: string;
100
+ clientSecret: string;
101
+ botAppId: string;
102
+ }
103
+ /** 微信(iLink) channel credentials (per instance). */
104
+ interface WeixinChannelConfig {
105
+ /** iLink bot token — obtained via QR scan login (weixin-login.ts). */
106
+ token: string;
107
+ /** iLink bot id — obtained via QR scan login (weixin-login.ts). */
108
+ botId: string;
109
+ /** API base URL (defaults to https://ilinkai.weixin.qq.com). */
110
+ baseUrl: string;
111
+ /** CDN base URL for media download/upload (defaults to https://cdn.ilinkai.weixin.qq.com). */
112
+ cdnBaseUrl: string;
113
+ pollIntervalMs: number;
114
+ }
115
+ /**
116
+ * One IM robot instance. The same channel TYPE may appear many times — e.g.
117
+ * two DingTalk bots with different credentials — each with its own unique
118
+ * `id` (e.g. `dingtalk-main`, `qq-7f3a`). `id` MUST NOT contain ':' because
119
+ * the gateway builds `convKey = channelId:conversationId`.
120
+ */
121
+ type ChannelType = 'dingtalk' | 'qq' | 'weixin';
122
+ interface ImChannelInstance {
123
+ /** Unique instance id, `<type>-<slug>` — used as the adapter registry key. */
124
+ id: string;
125
+ /** Channel type (drives which adapter class + which credential fields). */
126
+ type: ChannelType;
127
+ /** Human-readable display name (e.g. '主机器人'). */
128
+ name: string;
129
+ enabled: boolean;
130
+ /** Credential fields for this instance (shape depends on `type`). */
131
+ config: DingtalkChannelConfig & Partial<QqChannelConfig> & Partial<WeixinChannelConfig>;
132
+ }
133
+ interface ImGatewayConfig {
134
+ /** Working directory passed to host sessions created for IM conversations. */
135
+ cwd: string;
136
+ /** Throttle (ms) for incremental `updateCard` pushes. Ignored when a channel has no `updateCard`. */
137
+ streamThrottleMs: number;
138
+ /** Enable built-in slash commands (/help, /reset, /clear). */
139
+ slashCommands: boolean;
140
+ /** All channel instances. Only `enabled` ones connect out. Empty by default. */
141
+ channels: ImChannelInstance[];
142
+ }
143
+ /** Public surface of the gateway service, exposed as `ctx.imGateway`. */
144
+ interface ImGateway {
145
+ registerChannel(adapter: ImChannelAdapter): void;
146
+ getChannel(id: string): ImChannelAdapter | undefined;
147
+ listChannels(): string[];
148
+ /** Entry point channel adapters call for every inbound user message. */
149
+ handleInbound(message: ImInboundMessage): Promise<void>;
150
+ /** Snapshot of every channel's runtime status (for the settings UI). */
151
+ getChannelStatuses(): Record<string, ChannelStatus>;
152
+ }
153
+ sideEffect();
154
+
155
+ //#endregion
156
+ //#region src/gateway/im-gateway.d.ts
157
+ declare const Config: Schema<Schemastery.ObjectS<{
158
+ cwd: Schema<string, string>;
159
+ streamThrottleMs: Schema<number, number>;
160
+ slashCommands: Schema<boolean, boolean>;
161
+ channels: Schema<({
162
+ id?: string | null | undefined;
163
+ type?: "dingtalk" | "qq" | "weixin" | null | undefined;
164
+ name?: string | null | undefined;
165
+ enabled?: boolean | null | undefined;
166
+ config?: ({
167
+ clientId?: string | null | undefined;
168
+ clientSecret?: string | null | undefined;
169
+ callbackBaseUrl?: string | null | undefined;
170
+ appId?: string | null | undefined;
171
+ botAppId?: string | null | undefined;
172
+ baseUrl?: string | null | undefined;
173
+ token?: string | null | undefined;
174
+ botId?: string | null | undefined;
175
+ cdnBaseUrl?: string | null | undefined;
176
+ pollIntervalMs?: number | null | undefined;
177
+ } & _deepseek_ai_cosmokit0.Dict) | null | undefined;
178
+ } & _deepseek_ai_cosmokit0.Dict)[], Schemastery.ObjectT<{
179
+ id: Schema<string, string>;
180
+ type: Schema<"dingtalk" | "qq" | "weixin", "dingtalk" | "qq" | "weixin">;
181
+ name: Schema<string, string>;
182
+ enabled: Schema<boolean, boolean>;
183
+ config: Schema<Schemastery.ObjectS<{
184
+ clientId: Schema<string, string>;
185
+ clientSecret: Schema<string, string>;
186
+ callbackBaseUrl: Schema<string, string>;
187
+ appId: Schema<string, string>;
188
+ botAppId: Schema<string, string>;
189
+ baseUrl: Schema<string, string>;
190
+ token: Schema<string, string>;
191
+ botId: Schema<string, string>;
192
+ cdnBaseUrl: Schema<string, string>;
193
+ pollIntervalMs: Schema<number, number>;
194
+ }>, Schemastery.ObjectT<{
195
+ clientId: Schema<string, string>;
196
+ clientSecret: Schema<string, string>;
197
+ callbackBaseUrl: Schema<string, string>;
198
+ appId: Schema<string, string>;
199
+ botAppId: Schema<string, string>;
200
+ baseUrl: Schema<string, string>;
201
+ token: Schema<string, string>;
202
+ botId: Schema<string, string>;
203
+ cdnBaseUrl: Schema<string, string>;
204
+ pollIntervalMs: Schema<number, number>;
205
+ }>>;
206
+ }>[]>;
207
+ }>, Schemastery.ObjectT<{
208
+ cwd: Schema<string, string>;
209
+ streamThrottleMs: Schema<number, number>;
210
+ slashCommands: Schema<boolean, boolean>;
211
+ channels: Schema<({
212
+ id?: string | null | undefined;
213
+ type?: "dingtalk" | "qq" | "weixin" | null | undefined;
214
+ name?: string | null | undefined;
215
+ enabled?: boolean | null | undefined;
216
+ config?: ({
217
+ clientId?: string | null | undefined;
218
+ clientSecret?: string | null | undefined;
219
+ callbackBaseUrl?: string | null | undefined;
220
+ appId?: string | null | undefined;
221
+ botAppId?: string | null | undefined;
222
+ baseUrl?: string | null | undefined;
223
+ token?: string | null | undefined;
224
+ botId?: string | null | undefined;
225
+ cdnBaseUrl?: string | null | undefined;
226
+ pollIntervalMs?: number | null | undefined;
227
+ } & _deepseek_ai_cosmokit0.Dict) | null | undefined;
228
+ } & _deepseek_ai_cosmokit0.Dict)[], Schemastery.ObjectT<{
229
+ id: Schema<string, string>;
230
+ type: Schema<"dingtalk" | "qq" | "weixin", "dingtalk" | "qq" | "weixin">;
231
+ name: Schema<string, string>;
232
+ enabled: Schema<boolean, boolean>;
233
+ config: Schema<Schemastery.ObjectS<{
234
+ clientId: Schema<string, string>;
235
+ clientSecret: Schema<string, string>;
236
+ callbackBaseUrl: Schema<string, string>;
237
+ appId: Schema<string, string>;
238
+ botAppId: Schema<string, string>;
239
+ baseUrl: Schema<string, string>;
240
+ token: Schema<string, string>;
241
+ botId: Schema<string, string>;
242
+ cdnBaseUrl: Schema<string, string>;
243
+ pollIntervalMs: Schema<number, number>;
244
+ }>, Schemastery.ObjectT<{
245
+ clientId: Schema<string, string>;
246
+ clientSecret: Schema<string, string>;
247
+ callbackBaseUrl: Schema<string, string>;
248
+ appId: Schema<string, string>;
249
+ botAppId: Schema<string, string>;
250
+ baseUrl: Schema<string, string>;
251
+ token: Schema<string, string>;
252
+ botId: Schema<string, string>;
253
+ cdnBaseUrl: Schema<string, string>;
254
+ pollIntervalMs: Schema<number, number>;
255
+ }>>;
256
+ }>[]>;
257
+ }>>;
258
+ /**
259
+ * Core IM gateway service.
260
+ *
261
+ * Responsibilities (ported from pi-desk-top/src/main/im/im-gateway.ts):
262
+ * - channel registry (`registerChannel`)
263
+ * - conversation → host session mapping
264
+ * - per-conversation serial queue (one agent turn at a time)
265
+ * - slash-command handling (/help, /reset, /clear)
266
+ * - agent-event → channel reply routing (streaming, tool notifications, flush)
267
+ *
268
+ * ─────────────────────────────────────────────────────────────────────────
269
+ * HOST API CONTRACT (verified against deepseek-harness source):
270
+ * - Sessions: `ctx.sessions` is the `SessionStore` (from @deepseek-ai/dsh-session).
271
+ * `create(id, { meta: { cwd } })` builds an event-sourced Session.
272
+ * The Session object has NO `.prompt()` / `.on()` — it is a log.
273
+ * - Agents: `ctx.agents` is the `AgentRegistry` (from @deepseek-ai/dsh-agent).
274
+ * `get(sessionId)` → Agent | undefined; `createAgent(ownerCtx, opts)`
275
+ * starts the agent loop on a session. Drive a turn with
276
+ * `agent.followup(userMessage)` (see host/apiproxy/src/api-proxy.ts:2461).
277
+ * - Events: subscribe at the CONTEXT level: `ctx.on('session/event', (session, event) => …)`.
278
+ * Typed events (packages/core/session/src/types.ts):
279
+ * 'turn/start' { turn }
280
+ * 'assistant/chunk' { turn, step, chunk: StreamChunk } // chunk.type==='text-delta' → text
281
+ * 'assistant/message' { turn, step, message: AssistantMessage }
282
+ * 'tool/call' { turn, step, callId, name, arguments }
283
+ * 'turn/end' { turn, reason }
284
+ * ─────────────────────────────────────────────────────────────────────────
285
+ */
286
+ //#endregion
287
+ //#region src/index.d.ts
288
+ /**
289
+ * Plugin-level dependency declaration: the bundle waits for these host
290
+ * services before apply runs (mirrors the official dsh-skill-viewer host:
291
+ * `export const inject = ["typert", ...]`). `imGateway` (our own core
292
+ * service) is created inside apply and waited on via ctx.inject below.
293
+ */
294
+ declare const inject: string[];
295
+ /**
296
+ * Single-entry IM gateway bundle. One plugin (`@lijian-ui/dsh-im-gateway`) provides the
297
+ * core gateway service AND every channel (DingTalk / QQ / 个人微信) as
298
+ * INSTANCES: `config.channels` is an array, and the same channel type may
299
+ * appear multiple times (multi-bot support, mirroring pi-desk-top). Only
300
+ * `enabled` instances connect out.
301
+ */
302
+ declare function apply(ctx: Context$1, config: ImGatewayConfig): void;
303
+ //#endregion
304
+ export { ChannelStatus, ChannelType, Config, type Context, DingtalkChannelConfig, ImChannelAdapter, ImChannelInstance, ImGateway, ImGatewayConfig, ImImage, ImInboundMessage, QqChannelConfig, WeixinChannelConfig, apply, inject };