@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,63 @@
1
+ /**
2
+ * Shared HTTP helpers for channel adapters.
3
+ *
4
+ * Adapters throw `ChannelError(code, message)`: a stable code such as
5
+ * `channel/auth`, `channel/config`, `channel/network`, `channel/rate-limit`,
6
+ * `channel/rejected` (or a platform-specific `channel/<platform>-<code>`) and
7
+ * a Chinese user-facing message.
8
+ */
9
+
10
+ export class ChannelError extends Error {
11
+ /** @param {{ field?: string }} [opts] - `field`: the form field key the error belongs to. */
12
+ constructor(code, message, { field } = {}) {
13
+ super(message)
14
+ this.name = 'ChannelError'
15
+ this.code = code
16
+ if (field) this.field = field
17
+ }
18
+ }
19
+
20
+ function parseBody(text) {
21
+ try { return JSON.parse(text) } catch { return { raw: String(text ?? '').slice(0, 300) } }
22
+ }
23
+
24
+ /**
25
+ * fetch with a timeout. Returns { status, json, headers } — json is the parsed
26
+ * body, or `{ raw }` (first 300 chars) when the body is not JSON.
27
+ * Network failures and timeouts throw ChannelError('channel/network').
28
+ * @param {string} url
29
+ * @param {{ method?: string, headers?: object, body?: any, json?: any, timeoutMs?: number, label?: string, fetch?: Function }} [opts]
30
+ * `json` is serialized as the request body with a JSON content type;
31
+ * `label` names the platform in error messages (e.g. '飞书').
32
+ */
33
+ export async function request(url, { method, headers = {}, body, json, timeoutMs = 15000, label = '', fetch: fetchImpl = globalThis.fetch } = {}) {
34
+ const controller = new AbortController()
35
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
36
+ const init = { method: method ?? (json !== undefined || body !== undefined ? 'POST' : 'GET'), headers: { ...headers }, signal: controller.signal }
37
+ if (json !== undefined) {
38
+ init.headers['content-type'] ??= 'application/json; charset=utf-8'
39
+ init.body = JSON.stringify(json)
40
+ } else if (body !== undefined) {
41
+ init.body = body
42
+ }
43
+ try {
44
+ const res = await fetchImpl(url, init)
45
+ const text = await res.text()
46
+ return { status: res.status, json: parseBody(text), headers: res.headers }
47
+ } catch (error) {
48
+ const who = label ? `${label}接口` : '接口'
49
+ throw new ChannelError('channel/network', error?.name === 'AbortError' ? `请求${who}超时` : `无法访问${who}:${error?.cause?.code ?? error?.message ?? error}`)
50
+ } finally {
51
+ clearTimeout(timer)
52
+ }
53
+ }
54
+
55
+ /** POST a JSON body. Returns { status, json }. */
56
+ export function postJson(url, body, headers = {}, opts = {}) {
57
+ return request(url, { ...opts, method: 'POST', headers, json: body })
58
+ }
59
+
60
+ /** GET a URL. Returns { status, json }. */
61
+ export function getJson(url, headers = {}, opts = {}) {
62
+ return request(url, { ...opts, method: 'GET', headers })
63
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Channel adapter registry (see docs/CHANNELS.md).
3
+ *
4
+ * Built-in adapters: lark (static), telegram / wecom / dingtalk (loaded with
5
+ * dynamic import so a missing or broken module never breaks the registry).
6
+ * Adapters must NOT import this module (import ./http.js and ./message.js
7
+ * instead) — the registry imports them.
8
+ *
9
+ * Call `await loadAdapters()` once before using the synchronous lookups; the
10
+ * service core does this on start and in every channel RPC.
11
+ */
12
+ import lark from './lark.js'
13
+ import { ChannelError } from './http.js'
14
+
15
+ const adapters = new Map()
16
+ const kinds = new Map()
17
+
18
+ /** Register (or replace) an adapter. Its kinds become valid channel types. */
19
+ export function registerAdapter(adapter) {
20
+ if (!adapter?.id || !Array.isArray(adapter.kinds)) throw new Error('adapter needs id and kinds')
21
+ const previous = adapters.get(adapter.id)
22
+ if (previous) for (const k of previous.kinds) kinds.delete(k.id)
23
+ adapters.set(adapter.id, adapter)
24
+ for (const kind of adapter.kinds) kinds.set(kind.id, { adapter, kind: { platform: adapter.id, ...kind } })
25
+ return adapter
26
+ }
27
+
28
+ /** Remove an adapter (tests). */
29
+ export function unregisterAdapter(id) {
30
+ const adapter = adapters.get(id)
31
+ if (!adapter) return false
32
+ for (const k of adapter.kinds) kinds.delete(k.id)
33
+ adapters.delete(id)
34
+ return true
35
+ }
36
+
37
+ registerAdapter(lark)
38
+
39
+ const OPTIONAL = ['telegram', 'wecom', 'dingtalk']
40
+ const loadErrors = {}
41
+ let loading
42
+
43
+ /** Load the optional built-in adapters (idempotent). Resolves to the list of loaded ids. */
44
+ export function loadAdapters() {
45
+ loading ??= Promise.all(OPTIONAL.map(async (id) => {
46
+ try {
47
+ const mod = await import(`./${id}.js`)
48
+ const adapter = mod.default ?? mod.adapter
49
+ // A test may have registered a replacement already: keep it.
50
+ if (adapter && !adapters.has(adapter.id)) registerAdapter(adapter)
51
+ return adapter?.id
52
+ } catch (error) {
53
+ loadErrors[id] = error?.message ?? String(error)
54
+ return undefined
55
+ }
56
+ })).then(ids => ids.filter(Boolean))
57
+ return loading
58
+ }
59
+
60
+ /** Errors from optional adapters that failed to load: { [platform]: message }. */
61
+ export function adapterLoadErrors() {
62
+ return { ...loadErrors }
63
+ }
64
+
65
+ export function getAdapter(platform) {
66
+ return adapters.get(platform)
67
+ }
68
+
69
+ export function listAdapters() {
70
+ return [...adapters.values()]
71
+ }
72
+
73
+ /** Resolve a channel type to { adapter, kind }, or undefined. */
74
+ export function kindOf(type) {
75
+ return kinds.get(type)
76
+ }
77
+
78
+ /** Like kindOf, but throws ChannelError('channel/config') for unknown types. */
79
+ export function requireKind(type) {
80
+ const found = kinds.get(type)
81
+ if (!found) throw new ChannelError('channel/config', `不支持的渠道类型:${type}`)
82
+ return found
83
+ }
84
+
85
+ /** Pick zh/en text from a { zh, en } object (or return a plain string). */
86
+ export function localized(text, locale = 'zh') {
87
+ if (text === null || text === undefined) return text
88
+ if (typeof text !== 'object' || Array.isArray(text)) return text
89
+ return text[locale] ?? text.zh ?? text.en
90
+ }
91
+
92
+ /** A JSON-safe description of a kind for the UI: no functions, never a value. */
93
+ export function describeKind(kind) {
94
+ return {
95
+ id: kind.id,
96
+ platform: kind.platform,
97
+ label: kind.label,
98
+ description: kind.description,
99
+ fields: (kind.fields ?? []).map(f => {
100
+ const secret = Boolean(f.secret || f.type === 'secret')
101
+ const out = { ...f, secret }
102
+ if (secret) delete out.default // never any secret value
103
+ return JSON.parse(JSON.stringify(out))
104
+ }),
105
+ capabilities: { ...kind.capabilities },
106
+ setupSteps: kind.setupSteps ?? { zh: [], en: [] },
107
+ }
108
+ }
109
+
110
+ /**
111
+ * The catalog for the page: [{ platform, name, kinds:[{ id, label, description, fields, capabilities, setupSteps }] }].
112
+ * With a locale, `name` / `label` / `description` / `setupSteps` / field labels are also given as `*Text` strings.
113
+ */
114
+ export function listCatalog(locale) {
115
+ return listAdapters().map(adapter => ({
116
+ platform: adapter.id,
117
+ name: adapter.name,
118
+ ...(locale ? { nameText: localized(adapter.name, locale) } : {}),
119
+ kinds: adapter.kinds.map(k => {
120
+ const d = describeKind({ platform: adapter.id, ...k })
121
+ if (!locale) return d
122
+ return {
123
+ ...d,
124
+ labelText: localized(d.label, locale),
125
+ descriptionText: localized(d.description, locale),
126
+ setupStepsText: localized(d.setupSteps, locale) ?? [],
127
+ fields: d.fields.map(f => ({ ...f, labelText: localized(f.label, locale), helpText: localized(f.help, locale) })),
128
+ }
129
+ }),
130
+ }))
131
+ }
132
+
133
+ /** Secret field keys of a kind. */
134
+ export function secretKeys(kind) {
135
+ return (kind.fields ?? []).filter(f => f.secret || f.type === 'secret').map(f => f.key)
136
+ }
137
+
138
+ export { ChannelError }