@hedeqiang/dsh-nl2sql 0.10.0 → 0.11.1

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,184 @@
1
+ # Channel architecture
2
+
3
+ A **channel** is one configured connection to an IM platform. Every platform is an
4
+ **adapter** module under `channels/`; the service core (`service/channels.js`,
5
+ `service/bots.js`) only talks to the adapter interface, never to a platform API.
6
+
7
+ ```
8
+ channels/
9
+ index.js registry: list/get adapters, shared types and helpers
10
+ message.js platform-neutral message model + Markdown utilities + chart PNG
11
+ http.js fetch with timeout, JSON helpers, ChannelError
12
+ lark.js Lark / Feishu (webhook bot, app bot; long connection; card buttons)
13
+ telegram.js Telegram (Bot API; long polling; inline keyboard)
14
+ wecom.js WeCom 企业微信 (group webhook bot; AI bot long connection)
15
+ dingtalk.js DingTalk 钉钉 (custom robot webhook; enterprise app robot over Stream)
16
+ ```
17
+
18
+ ## Message model (platform neutral)
19
+
20
+ Producers (agent `nl2sql_send`, dashboards, reports, alerts, schema notices,
21
+ bot answers) build one `OutboundMessage`; each adapter renders it natively.
22
+
23
+ ```js
24
+ OutboundMessage = {
25
+ title: string,
26
+ text?: string, // Markdown (CommonMark subset + <font color='green|red|grey'>, <at id=all></at>)
27
+ results?: ResultBlock[], // ≤ 10
28
+ footer?: string,
29
+ includeSql?: boolean,
30
+ chartMode?: 'native' | 'image' | 'table',
31
+ mentionAll?: boolean,
32
+ template?: 'blue' | 'red' | 'green' | 'orange' | 'grey', // accent; adapters may ignore
33
+ }
34
+ ResultBlock = { title, columns:[{name,kind}], rows, rowCount, truncated, chart, sql?, filters?, error?,
35
+ actions?: ActionSpec } // interactive controls, only when the target supports them
36
+ ActionSpec = { buttons?: [{ text, value }], select?: { placeholder, value, options:[{ text, value }] } }
37
+ // value objects are opaque tokens: { a, t } — never trusted on receipt
38
+ ```
39
+
40
+ ## Adapter interface
41
+
42
+ ```js
43
+ export default {
44
+ id: 'telegram', // platform id
45
+ name: { zh: 'Telegram', en: 'Telegram' },
46
+ kinds: [ChannelKind, ...], // what a user can configure for this platform
47
+ // Optional inbound bot (one per configured channel of a bot-capable kind):
48
+ createBot?(channel, secrets, handlers, opts) -> Bot,
49
+ }
50
+
51
+ ChannelKind = {
52
+ id: 'telegram_bot', // stored as channel.type (legacy: lark_webhook, lark_app)
53
+ platform: 'telegram',
54
+ label: { zh, en }, description: { zh, en },
55
+ // Form schema rendered generically by the UI:
56
+ fields: [{ key, label:{zh,en}, type:'text'|'secret'|'select'|'url', required?, placeholder?, help?:{zh,en},
57
+ options?:[{value,label:{zh,en}}], pattern?: string, secret?: boolean, showIf?: { key, equals },
58
+ target?: boolean }], // target: the field the 「选择群聊」 picker (listTargets) fills, e.g. Lark receiveId
59
+ capabilities: {
60
+ push: true,
61
+ markdown: 'full' | 'basic' | 'none',
62
+ charts: 'native' | 'image' | 'none', // native card charts vs PNG upload vs tables only
63
+ chartImages?: boolean, // also supports PNG images when charts is 'native' (Lark app)
64
+ chartModes?: ('native'|'image'|'table')[], // selectable channel.chartMode values; first = default
65
+ // (derived from charts/chartImages when omitted)
66
+ tables: 'native' | 'markdown' | 'text',
67
+ buttons: boolean, // interactive buttons on pushed/bot messages
68
+ bot: boolean, // inbound questions (no public URL needed)
69
+ listTargets: boolean, // can list chats/groups the bot is in
70
+ mentionAll: boolean,
71
+ maxMessageBytes?: number,
72
+ },
73
+ setupSteps: { zh: string[], en: string[] }, // shown in the UI; console paths, permissions
74
+ validate(input, existing) -> { config, secrets } // throws ChannelError with a user message
75
+ send(channel, secrets, message, ctx) -> { messageId?, images } // ctx: { renderChartPng, logger, fetch }
76
+ test?(channel, secrets) -> { ok, detail } // cheap auth check (token fetch / getMe)
77
+ listTargets?(channel, secrets) -> [{ id, name, type }]
78
+ }
79
+
80
+ Bot = {
81
+ start(), stop(), status() -> { state:'idle'|'connecting'|'connected'|'error'|'stopped', error?, since? },
82
+ info() -> { name?, id? } | null,
83
+ }
84
+ handlers = {
85
+ onMessage(InboundMessage) -> Promise<void>,
86
+ onAction?(InboundAction) -> Promise<{ toast?: { type:'info'|'success'|'error', content } }>,
87
+ }
88
+ InboundMessage = {
89
+ platform, channelId, messageId, chatId, chatType: 'p2p' | 'group', text, // mention markup stripped
90
+ mentionedBot: boolean, sender: { id, name?, email?, unionId? }, raw?,
91
+ reply(OutboundMessage) -> Promise<{ messageId? }>, // threaded reply when supported
92
+ replyText(text) -> Promise<void>,
93
+ react?(kind: 'working'|'done'|'failed') -> Promise<void>,
94
+ }
95
+ InboundAction = { platform, channelId, operator: { id }, chatId, messageId, value, option?, raw?,
96
+ reply(OutboundMessage) -> Promise<{ messageId? }> }
97
+ ```
98
+
99
+ Errors: adapters throw `ChannelError(code, message, { field? })` with a Chinese
100
+ user-facing message and a stable code such as `channel/auth`, `channel/config`,
101
+ `channel/network`, `channel/rate-limit`, `channel/rejected` (Lark keeps its
102
+ legacy `im/lark-<code>` / `im/config` codes). `field` names the form field key
103
+ the error belongs to; page RPC errors pass it through as `error.field`
104
+ (`{ ok:false, error:{ code, message, field? } }`).
105
+
106
+ Adapters must not import `channels/index.js` (the registry imports them); use
107
+ `channels/http.js` and `channels/message.js`. The `channel` object passed to
108
+ `send` / `test` / `listTargets` / `createBot` is the stored record with its
109
+ `config` fields also spread at the top level; `secrets` is a plain object keyed by
110
+ the kind's secret field keys. `validate(input, existing)` receives
111
+ `input = { name, config, secrets }` where `secrets` already merges stored values
112
+ (omitted / empty secrets keep their stored value, `null` clears one), and returns
113
+ the normalized `{ config, secrets }`. `test` may return `{ ok, detail, send:false }`
114
+ to skip the follow-up test message (`send` defaults to true: a test message is
115
+ sent after a successful check).
116
+
117
+ ## Identities (members)
118
+
119
+ Members carry platform-scoped identities instead of Lark-only fields:
120
+
121
+ ```js
122
+ user = { id, kind: 'person' | 'chat', name, role, datasourceIds, rowRules, disabled, note,
123
+ identities: [{ platform: 'lark'|'telegram'|'wecom'|'dingtalk', id, email? }] }
124
+ ```
125
+
126
+ Legacy fields migrate on load: `larkOpenId → { platform:'lark', id }`,
127
+ `larkChatId → { platform:'lark', id }`, `email` stays and also matches any
128
+ platform that reports an email. `resolveActor({ platform, userId, email, name,
129
+ chatId, chatType })` replaces `resolveLarkActor` (kept as a thin alias).
130
+
131
+ Settings: `botDefaultRole` / `botDefaultDatasourceIds` replace
132
+ `larkDefaultRole` / `larkDefaultDatasourceIds` (read both; write the new keys).
133
+
134
+ ## Bots
135
+
136
+ Bot configuration lives on the channel: `channel.bot = { enabled, allowP2p,
137
+ sessionScope: 'chat'|'user' }`. Every enabled bot channel runs its adapter's
138
+ `createBot`; `service/bots.js` owns lifecycle, the per-conversation queue, the
139
+ agent turn (`runAgentTurn`), actor resolution, card actions (opaque tokens,
140
+ rate limit, re-authorization), delivery log and audit. Conversations are keyed
141
+ `<channelId>:<chatId>[:<userId>]`.
142
+
143
+ Legacy `state.larkBot = { enabled, channelId, allowP2p, sessionScope }`
144
+ migrates to `channel.bot` on the referenced channel.
145
+
146
+ ## Platform notes
147
+
148
+ | Platform | Push | Bot without public URL | Buttons | Charts |
149
+ |---|---|---|---|---|
150
+ | Lark / Feishu | webhook bot; app bot (chat / user / email) | long connection (`im.message.receive_v1`, `card.action.trigger`) | yes | native card charts / PNG |
151
+ | Telegram | Bot API `sendMessage` / `sendPhoto` to chat ids | long polling `getUpdates` | inline keyboard (`callback_query`) | PNG (`sendPhoto`) |
152
+ | WeCom 企业微信 | group robot webhook (markdown, image base64+md5) | AI bot long connection (`wss://openws.work.weixin.qq.com`, bot id + secret) | template card buttons where supported, else none | PNG via webhook image |
153
+ | DingTalk 钉钉 | custom robot webhook (+ HMAC sign) | enterprise app robot over Stream (`/v1.0/im/bot/messages/get`), reply via `sessionWebhook` | ActionCard links only | PNG not uploadable via webhook → tables |
154
+
155
+ Only verified behaviour should be claimed in the UI; anything tested against
156
+ fakes only is labelled in `CHANGELOG.md`.
157
+
158
+ ## Page RPC contract (UI ↔ host)
159
+
160
+ Existing RPC names keep working (`listChannels`, `saveChannel`, `deleteChannel`,
161
+ `testChannel`, `listChannelChats`, `sendToChannel`, `sendDashboard`,
162
+ `larkBotStatus`, `saveLarkBot`, `restartLarkBot`, `listDeliveries`). New:
163
+
164
+ - `channelCatalog({ locale? })` → `[{ platform, name:{zh,en}, kinds:[{ id, label, description, fields, capabilities, setupSteps }] }]`
165
+ (secrets fields marked `secret: true`; never any value; with `locale`, `*Text` strings are added).
166
+ Adapters that fail to load are simply absent.
167
+ - `listChannels()` → `[{ id, name, type, platform, description, isDefault, chartMode, config: {non-secret fields},
168
+ secretsSet: { <secretKey>: true }, hasSecret, needsSecret?, unsupported?, lastSentAt?, lastError?,
169
+ capabilities, bot?: { enabled, allowP2p, sessionScope, status:{state,error?}, info? } }]`
170
+ (Lark channels also keep the legacy top-level `domain, appId, receiveIdType, receiveId, hasSign`;
171
+ `unsupported: true` when the channel's adapter is not loaded.)
172
+ - `saveChannel({ id?, name, type, description?, isDefault?, chartMode?, config: {...}, secrets: {...only changed...}, bot?: {...} })`
173
+ — legacy flat payloads (`webhookUrl`, `appId`, `appSecret`, `receiveId`…) are still accepted for Lark.
174
+ - `listChannelTargets({ id? , type?, config?, secrets? })` → `[{ id, name, type }]` (unsaved credentials allowed; nothing stored).
175
+ - `testChannel({ id })` → `{ ok, detail, … }` (runs `kind.test` when present, then sends a test message
176
+ unless the check returned `send: false`).
177
+ - `botStatus()` → `[{ channelId, channelName, platform, type, enabled, allowP2p, sessionScope, status, info, conversations: [...] }]`
178
+ (one entry per channel whose kind has `capabilities.bot`).
179
+ - `saveBot({ channelId, enabled?, allowP2p?, sessionScope? })` → bot status for that channel; `restartBot({ channelId })`.
180
+ - `listUsers` / `saveUser` use `identities: [{ platform, id, email? }]`; legacy `larkOpenId` / `larkChatId` / `email` accepted.
181
+ - `getSettings` exposes `botDefaultRole` / `botDefaultDatasourceIds`; `larkDefaultRole` / `larkDefaultDatasourceIds`
182
+ are still returned as aliases and accepted by `saveSettings` / `resetSettings`.
183
+ - `listDeliveries({ limit? })`: entries carry `platform` for channel deliveries; bot answers use `source: '<platform>bot'`
184
+ (`larkbot`, `telegrambot`, …).