@hedeqiang/dsh-nl2sql 0.0.0-stage → 0.10.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.
- package/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/README.en.md +62 -0
- package/README.md +88 -2
- package/analysis.js +888 -0
- package/cordis.patch.yml +68 -0
- package/drivers.js +694 -0
- package/filters.js +237 -0
- package/icon.svg +13 -0
- package/im.js +511 -0
- package/index.js +650 -0
- package/lark-bot.js +606 -0
- package/lib/client.echarts.js +57 -0
- package/lib/client.js +4845 -0
- package/locale/en.json +1 -0
- package/locale/zh.json +1 -0
- package/package.json +90 -4
- package/policy.js +362 -0
- package/service/access.js +145 -0
- package/service/admin.js +539 -0
- package/service/alerts.js +233 -0
- package/service/dashboards.js +359 -0
- package/service/datasources.js +392 -0
- package/service/evaluation.js +121 -0
- package/service/lark-actions.js +223 -0
- package/service/lark.js +250 -0
- package/service/push.js +314 -0
- package/service/query.js +411 -0
- package/service/schema.js +507 -0
- package/service/shared.js +424 -0
- package/service/suggestions.js +772 -0
- package/service/uploads.js +258 -0
- package/sql-guard.js +222 -0
- package/sqlite-driver.js +677 -0
- package/store.js +225 -0
- package/xlsx.js +1001 -0
package/service/push.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nl2SqlService methods: push.
|
|
3
|
+
* Mixed into the service prototype by index.js; `this` is the service.
|
|
4
|
+
*/
|
|
5
|
+
import { ImError, RECEIVE_ID_TYPES, listBotChats, sendMessage } from '../im.js'
|
|
6
|
+
import { CREDENTIAL_SCOPE, UserError, newId, oneOf, pick, requireText, upsert } from './shared.js'
|
|
7
|
+
|
|
8
|
+
export const methods = {
|
|
9
|
+
// ---------------------------------------------------------- IM channels
|
|
10
|
+
|
|
11
|
+
publicChannel(c) {
|
|
12
|
+
return { ...pick(c, ['id', 'name', 'type', 'domain', 'appId', 'receiveIdType', 'receiveId', 'description', 'isDefault', 'createdAt', 'updatedAt', 'lastSentAt', 'lastError', 'chartMode']), hasSecret: Boolean(c.hasSecret), hasSign: Boolean(c.hasSign), ...(c.needsSecret ? { needsSecret: true } : {}) }
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
listChannels() {
|
|
16
|
+
return this.state.channels.map(c => this.publicChannel(c))
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
findChannel(ref) {
|
|
20
|
+
if (!ref) return this.state.channels.find(c => c.isDefault) ?? (this.state.channels.length === 1 ? this.state.channels[0] : undefined)
|
|
21
|
+
return this.state.channels.find(c => c.id === ref) ?? this.state.channels.find(c => c.name === ref)
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
requireChannel(ref) {
|
|
25
|
+
const c = this.findChannel(ref)
|
|
26
|
+
if (!c) throw new UserError('nl2sql/unknown-channel', ref ? `推送渠道不存在:${ref}` : `没有可用的推送渠道。${this.state.channels.length ? `可用渠道:${this.state.channels.map(x => x.name).join('、')}` : '请用户在「数据问答 → 推送」页面添加飞书/Lark 渠道。'}`)
|
|
27
|
+
return c
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
channelSecretKey(id) {
|
|
31
|
+
return `${CREDENTIAL_SCOPE}/${id}`
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async saveChannel(input) {
|
|
35
|
+
const existing = input.id ? this.state.channels.find(c => c.id === input.id) : undefined
|
|
36
|
+
if (input.id && !existing) throw new UserError('nl2sql/unknown-channel', '推送渠道不存在')
|
|
37
|
+
const c = existing ? { ...existing } : { id: newId('ch'), createdAt: Date.now() }
|
|
38
|
+
c.name = requireText(input.name ?? c.name, 'name', 64)
|
|
39
|
+
if (this.state.channels.some(x => x.name === c.name && x.id !== c.id)) throw new UserError('nl2sql/duplicate', `渠道名称已存在:${c.name}`)
|
|
40
|
+
c.type = oneOf(input.type ?? c.type ?? 'lark_webhook', ['lark_webhook', 'lark_app'], 'type')
|
|
41
|
+
c.domain = input.domain === 'lark' ? 'lark' : input.domain === 'feishu' ? 'feishu' : (c.domain ?? 'feishu')
|
|
42
|
+
c.description = String(input.description ?? c.description ?? '').slice(0, 300)
|
|
43
|
+
c.chartMode = ['native', 'image', 'table'].includes(input.chartMode) ? input.chartMode : (c.chartMode ?? 'native')
|
|
44
|
+
if (c.chartMode === 'image' && c.type !== 'lark_app') c.chartMode = 'native'
|
|
45
|
+
if (c.type === 'lark_app') {
|
|
46
|
+
c.appId = requireText(input.appId ?? c.appId, 'appId', 64)
|
|
47
|
+
c.receiveIdType = oneOf(input.receiveIdType ?? c.receiveIdType ?? 'chat_id', RECEIVE_ID_TYPES, 'receiveIdType')
|
|
48
|
+
c.receiveId = requireText(input.receiveId ?? c.receiveId, 'receiveId', 200)
|
|
49
|
+
} else {
|
|
50
|
+
delete c.appId; delete c.receiveIdType; delete c.receiveId
|
|
51
|
+
}
|
|
52
|
+
const patch = {}
|
|
53
|
+
if (c.type === 'lark_webhook') {
|
|
54
|
+
if (typeof input.webhookUrl === 'string' && input.webhookUrl.trim()) {
|
|
55
|
+
const url = input.webhookUrl.trim()
|
|
56
|
+
if (!/^https:\/\/open\.(feishu\.cn|larksuite\.com)\/open-apis\/bot\/v2\/hook\/[\w-]+$/.test(url)) throw new UserError('nl2sql/invalid', 'Webhook 地址格式不正确,应为 https://open.feishu.cn/open-apis/bot/v2/hook/… (Lark 国际版为 open.larksuite.com)')
|
|
57
|
+
patch.webhookUrl = url
|
|
58
|
+
c.domain = url.includes('larksuite') ? 'lark' : 'feishu'
|
|
59
|
+
}
|
|
60
|
+
if (typeof input.signSecret === 'string' && (input.signSecret !== '' || input.clearSign)) patch.signSecret = input.signSecret.trim()
|
|
61
|
+
} else if (typeof input.appSecret === 'string' && input.appSecret !== '') {
|
|
62
|
+
patch.appSecret = input.appSecret.trim()
|
|
63
|
+
}
|
|
64
|
+
const id = `ch-${c.id.replace(/^ch-/, '')}`
|
|
65
|
+
const secrets = Object.keys(patch).length ? await this.writeSecrets(id, patch) : await this.readSecrets(id)
|
|
66
|
+
c.hasSecret = Boolean(c.type === 'lark_webhook' ? secrets.webhookUrl : secrets.appSecret)
|
|
67
|
+
c.hasSign = Boolean(secrets.signSecret)
|
|
68
|
+
if (!c.hasSecret) throw new UserError('nl2sql/invalid', c.type === 'lark_webhook' ? '请填写 Webhook 地址' : '请填写 App Secret')
|
|
69
|
+
delete c.needsSecret
|
|
70
|
+
if (input.isDefault || !this.state.channels.some(x => x.isDefault && x.id !== c.id)) {
|
|
71
|
+
for (const x of this.state.channels) x.isDefault = false
|
|
72
|
+
c.isDefault = true
|
|
73
|
+
}
|
|
74
|
+
c.updatedAt = Date.now()
|
|
75
|
+
this.state.channels = upsert(this.state.channels, c)
|
|
76
|
+
await this.save()
|
|
77
|
+
return this.publicChannel(c)
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async deleteChannel(id) {
|
|
81
|
+
const c = this.state.channels.find(x => x.id === id)
|
|
82
|
+
if (!c) return { deleted: id }
|
|
83
|
+
this.state.channels = this.state.channels.filter(x => x.id !== id)
|
|
84
|
+
if (c.isDefault && this.state.channels[0]) this.state.channels[0].isDefault = true
|
|
85
|
+
for (const r of this.state.reports) if (r.channelId === id) delete r.channelId
|
|
86
|
+
try { await this.ctx.get('credentials')?.deleteRecord(this.credentialKey(`ch-${id.replace(/^ch-/, '')}`)) } catch {}
|
|
87
|
+
await this.save()
|
|
88
|
+
return { deleted: id }
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async channelSecrets(c) {
|
|
92
|
+
return this.readSecrets(`ch-${c.id.replace(/^ch-/, '')}`)
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/** Chats of an app bot: a saved channel, or unsaved credentials from the form (nothing is stored). */
|
|
96
|
+
async listChannelChats({ id, appId, appSecret, domain }) {
|
|
97
|
+
const saved = id ? this.state.channels.find(c => c.id === id) : undefined
|
|
98
|
+
const c = saved ?? { type: 'lark_app', appId: requireText(appId, 'appId', 64), domain: domain === 'lark' ? 'lark' : 'feishu' }
|
|
99
|
+
if (c.type !== 'lark_app') throw new UserError('nl2sql/invalid', '只有应用机器人渠道可以列出群聊')
|
|
100
|
+
const secrets = appSecret ? { appSecret } : saved ? await this.channelSecrets(saved) : {}
|
|
101
|
+
if (!secrets.appSecret) throw new UserError('nl2sql/invalid', '请先填写 App Secret')
|
|
102
|
+
try {
|
|
103
|
+
return await listBotChats({ ...c, ...(appId ? { appId } : {}) }, secrets)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw error instanceof ImError ? new UserError(error.code, error.message) : error
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Send to an IM channel. Results are referenced by resultId (from this
|
|
111
|
+
* process' cache) or given inline; each is re-checked against privacy:
|
|
112
|
+
* masked values stay masked because results are stored masked.
|
|
113
|
+
*/
|
|
114
|
+
async sendToChannel({ channelId, title, text, resultIds = [], results = [], includeSql = false, source = 'ui', sessionId, footer, chartMode }) {
|
|
115
|
+
const c = this.requireChannel(channelId)
|
|
116
|
+
const resolved = []
|
|
117
|
+
const sender = this.actorFor(sessionId)
|
|
118
|
+
for (const id of resultIds.slice(0, 10)) {
|
|
119
|
+
const r = this.lookupResult(id)
|
|
120
|
+
if (!r) throw new UserError('nl2sql/result-expired', `结果 ${id} 已过期,请重新查询后再发送。`)
|
|
121
|
+
if (source === 'agent') this.assertResultAccess(r, sender)
|
|
122
|
+
resolved.push(r)
|
|
123
|
+
}
|
|
124
|
+
resolved.push(...results.slice(0, 10))
|
|
125
|
+
if (!text && resolved.length === 0) throw new UserError('nl2sql/invalid', '没有可发送的内容')
|
|
126
|
+
const message = {
|
|
127
|
+
title: requireText(title || resolved[0]?.title || '数据问答', 'title', 100),
|
|
128
|
+
text: text ? String(text).slice(0, 20000) : '',
|
|
129
|
+
chartMode: ['native', 'image', 'table'].includes(chartMode) ? chartMode : (c.chartMode ?? 'native'),
|
|
130
|
+
results: resolved.map(r => pick(r, ['title', 'columns', 'rows', 'rowCount', 'truncated', 'chart', 'sql', 'filters', 'error'])),
|
|
131
|
+
includeSql: Boolean(includeSql),
|
|
132
|
+
footer: footer ?? `数据问答 · ${new Date().toLocaleString('zh-CN', { hour12: false })}${resolved[0]?.datasourceName ? ` · ${resolved[0].datasourceName}` : ''}`,
|
|
133
|
+
}
|
|
134
|
+
const started = Date.now()
|
|
135
|
+
try {
|
|
136
|
+
const sent = await sendMessage(c, await this.channelSecrets(c), message)
|
|
137
|
+
c.lastSentAt = Date.now()
|
|
138
|
+
delete c.lastError
|
|
139
|
+
this.recordDelivery({ channel: c, title: message.title, status: 'ok', results: resolved.length, images: sent.images, source, sessionId, elapsedMs: Date.now() - started })
|
|
140
|
+
await this.save()
|
|
141
|
+
return { ok: true, channel: c.name, messageId: sent.messageId ?? null, images: sent.images, results: resolved.length }
|
|
142
|
+
} catch (error) {
|
|
143
|
+
c.lastError = error.message
|
|
144
|
+
this.recordDelivery({ channel: c, title: message.title, status: 'error', error: error.message, results: resolved.length, source, sessionId, elapsedMs: Date.now() - started })
|
|
145
|
+
await this.save()
|
|
146
|
+
throw error instanceof ImError ? new UserError(error.code, error.message) : error
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/** Text of one assistant message of a session, for "send this reply to IM". */
|
|
151
|
+
async assistantMessageText(sessionId, messageId) {
|
|
152
|
+
const read = (session) => {
|
|
153
|
+
for (const e of session.snapshotEvents()) {
|
|
154
|
+
if (e.type !== 'assistant/message' || e.data.message?.id !== messageId) continue
|
|
155
|
+
return e.data.message.content.filter(b => b.type === 'text').map(b => b.text).join('\n\n').trim()
|
|
156
|
+
}
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
const live = this.ctx.get('agents')?.get(sessionId)
|
|
160
|
+
let text = live ? read(live.session) : undefined
|
|
161
|
+
if (text === undefined) {
|
|
162
|
+
const q = this.ctx.get('sessionQuery')
|
|
163
|
+
if (!q) throw new UserError('nl2sql/unavailable', '无法读取会话内容')
|
|
164
|
+
text = await q.inspectSession(sessionId, read)
|
|
165
|
+
}
|
|
166
|
+
if (!text) throw new UserError('nl2sql/invalid', '这条回复没有文字内容')
|
|
167
|
+
return text
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/** resultIds of nl2sql tool results in the same turn as an assistant message. */
|
|
171
|
+
async turnResultIds(sessionId, messageId) {
|
|
172
|
+
const read = (session) => {
|
|
173
|
+
const events = session.snapshotEvents()
|
|
174
|
+
const at = events.findIndex(e => e.type === 'assistant/message' && e.data.message?.id === messageId)
|
|
175
|
+
if (at < 0) return []
|
|
176
|
+
const turn = events[at].data.turn
|
|
177
|
+
const ids = []
|
|
178
|
+
for (const e of events) {
|
|
179
|
+
if (e.type !== 'tool/result' || e.data.turn !== turn) continue
|
|
180
|
+
const meta = e.data.meta
|
|
181
|
+
if (meta?.resultId) ids.push(meta.resultId)
|
|
182
|
+
for (const r of meta?.results ?? []) if (r.resultId) ids.push(r.resultId)
|
|
183
|
+
}
|
|
184
|
+
return ids
|
|
185
|
+
}
|
|
186
|
+
const live = this.ctx.get('agents')?.get(sessionId)
|
|
187
|
+
if (live) return read(live.session)
|
|
188
|
+
return (await this.ctx.get('sessionQuery')?.inspectSession(sessionId, read)) ?? []
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
async sendAssistantMessage({ channelId, sessionId, messageId, title, withResults = true }) {
|
|
192
|
+
const text = await this.assistantMessageText(sessionId, messageId)
|
|
193
|
+
const resultIds = withResults ? (await this.turnResultIds(sessionId, messageId)).filter(id => this.lookupResult(id)).slice(-6) : []
|
|
194
|
+
const firstLine = text.split('\n').map(l => l.replace(/^#+\s*|\*\*/g, '').trim()).find(Boolean) ?? '数据问答'
|
|
195
|
+
return this.sendToChannel({ channelId, title: title || firstLine.slice(0, 60), text, resultIds, source: 'ui', sessionId })
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
async previewAssistantMessage({ sessionId, messageId }) {
|
|
199
|
+
const text = await this.assistantMessageText(sessionId, messageId)
|
|
200
|
+
const resultIds = (await this.turnResultIds(sessionId, messageId)).filter(id => this.lookupResult(id)).slice(-6)
|
|
201
|
+
return { text, resultIds, titles: resultIds.map(id => this.lookupResult(id)?.title ?? '') }
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
recordDelivery({ channel, title, status, error, results, images, source, sessionId, elapsedMs }) {
|
|
205
|
+
this.records.put('delivery', { id: newId('dl'), at: Date.now(), channelId: channel.id, channelName: channel.name, title, status, ...(error ? { error: String(error).slice(0, 500) } : {}), results, images: images ?? 0, source, sessionId: sessionId ?? null, elapsedMs })
|
|
206
|
+
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
listDeliveries(limit = 100) {
|
|
210
|
+
return this.records.list('delivery', { limit })
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
async testChannel(id) {
|
|
214
|
+
return this.sendToChannel({ channelId: id, title: '数据问答 · 推送测试', text: `这是一条测试消息。\n发送时间:${new Date().toLocaleString('zh-CN', { hour12: false })}`, source: 'test' })
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
/** Send the current dashboard (with saved global filters) to a channel. */
|
|
218
|
+
async sendDashboard({ channelId, title, filters, dashboardId, chartMode }) {
|
|
219
|
+
const results = []
|
|
220
|
+
const d = dashboardId ? this.requireDashboard(dashboardId) : this.currentDashboardObj()
|
|
221
|
+
for (const w of d.widgets.slice(0, 10)) {
|
|
222
|
+
try {
|
|
223
|
+
const r = await this.runWidget(w.id, filters ?? d.filters)
|
|
224
|
+
results.push({ ...r, title: w.title })
|
|
225
|
+
} catch (error) {
|
|
226
|
+
results.push({ title: w.title, error: error.message, columns: [], rows: [] })
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!results.length) throw new UserError('nl2sql/invalid', '看板为空')
|
|
230
|
+
const sent = await this.sendToChannel({ channelId, title: title || d.name || '数据看板', results, source: 'dashboard', chartMode })
|
|
231
|
+
await this.saveDashboardSnapshot(d, results, 'push')
|
|
232
|
+
return sent
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
/** Run every widget of a dashboard and keep a snapshot (for history / export). */
|
|
236
|
+
async snapshotDashboard({ dashboardId, filters, reason = 'manual' } = {}) {
|
|
237
|
+
const d = dashboardId ? this.requireDashboard(dashboardId) : this.currentDashboardObj()
|
|
238
|
+
const results = []
|
|
239
|
+
for (const w of d.widgets.slice(0, 30)) {
|
|
240
|
+
try { results.push({ ...(await this.runWidget(w.id, filters ?? d.filters)), title: w.title }) } catch (error) { results.push({ title: w.title, error: error.message, columns: [], rows: [] }) }
|
|
241
|
+
}
|
|
242
|
+
return this.saveDashboardSnapshot(d, results, reason)
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
async saveDashboardSnapshot(d, results, reason) {
|
|
246
|
+
const snap = { id: newId('snap'), dashboardId: d.id, name: d.name, reason, at: Date.now(), filters: d.filters ?? [], resultIds: results.map(r => r.resultId).filter(Boolean), widgets: results.map(r => ({ title: r.title, resultId: r.resultId ?? null, error: r.error ?? null, rowCount: r.rowCount ?? 0 })) }
|
|
247
|
+
try { this.store?.putSnapshot({ id: snap.id, kind: 'dashboard', ref: d.id, createdAt: snap.at, body: snap }) } catch {}
|
|
248
|
+
return snap
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
listDashboardSnapshots(dashboardId) {
|
|
252
|
+
const d = dashboardId ? this.requireDashboard(dashboardId) : this.currentDashboardObj()
|
|
253
|
+
return (this.store?.listSnapshots('dashboard', d.id, 30) ?? []).map(s => s.body)
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
// -------------------------------------------------------------- feedback
|
|
257
|
+
|
|
258
|
+
async submitFeedback({ resultId, rating, comment, question, sql, datasourceId, sessionId, title }) {
|
|
259
|
+
const r = resultId ? this.lookupResult(resultId) : undefined
|
|
260
|
+
const ds = this.requireDatasource(r?.datasourceId ?? datasourceId)
|
|
261
|
+
const entry = {
|
|
262
|
+
id: newId('fb'),
|
|
263
|
+
at: Date.now(),
|
|
264
|
+
rating: rating === 'up' ? 'up' : 'down',
|
|
265
|
+
datasourceId: ds.id,
|
|
266
|
+
datasourceName: ds.name,
|
|
267
|
+
resultId: resultId ?? null,
|
|
268
|
+
sessionId: sessionId ?? null,
|
|
269
|
+
title: String(title ?? r?.title ?? '').slice(0, 120),
|
|
270
|
+
question: String(question ?? '').slice(0, 1000),
|
|
271
|
+
sql: String(sql ?? r?.baseSql ?? r?.sql ?? '').slice(0, 20000),
|
|
272
|
+
comment: String(comment ?? '').slice(0, 2000),
|
|
273
|
+
status: rating === 'up' ? 'resolved' : 'open',
|
|
274
|
+
}
|
|
275
|
+
if (!entry.sql) throw new UserError('nl2sql/invalid', '缺少 SQL')
|
|
276
|
+
// A newer rating of the same result replaces the earlier one.
|
|
277
|
+
if (resultId) for (const old of this.records.list('feedback', { limit: 5, filter: (f) => f.resultId === resultId })) this.records.delete('feedback', old.id)
|
|
278
|
+
this.records.put('feedback', entry)
|
|
279
|
+
await this.save()
|
|
280
|
+
return entry
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
listFeedback({ status, datasourceId, rating } = {}) {
|
|
284
|
+
return this.records.list('feedback', { ref: datasourceId, status, limit: 1000, filter: (f) => !rating || f.rating === rating })
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
feedbackStats(datasourceId) {
|
|
288
|
+
const list = this.records.list('feedback', { ref: datasourceId, limit: 100000 })
|
|
289
|
+
const up = list.filter(f => f.rating === 'up').length
|
|
290
|
+
return { up, down: list.length - up, open: list.filter(f => f.status === 'open').length, rate: list.length ? Number((up / list.length).toFixed(3)) : null }
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
async resolveFeedback({ id, status, resolution, saveExample, correctedSql, question }) {
|
|
294
|
+
const f = this.records.get('feedback', id)
|
|
295
|
+
if (!f) throw new UserError('nl2sql/unknown-feedback', '反馈不存在')
|
|
296
|
+
if (saveExample) {
|
|
297
|
+
await this.saveKnowledgeItem({ datasourceId: f.datasourceId, kind: 'example', item: { question: question || f.question || f.title, sql: correctedSql || f.sql, note: f.rating === 'down' && f.comment ? `修正自反馈:${f.comment}`.slice(0, 500) : '' } })
|
|
298
|
+
f.savedAsExample = true
|
|
299
|
+
}
|
|
300
|
+
f.status = status === 'open' ? 'open' : status === 'ignored' ? 'ignored' : 'resolved'
|
|
301
|
+
if (resolution !== undefined) f.resolution = String(resolution).slice(0, 1000)
|
|
302
|
+
if (correctedSql) f.correctedSql = String(correctedSql).slice(0, 20000)
|
|
303
|
+
f.resolvedAt = Date.now()
|
|
304
|
+
this.records.put('feedback', f)
|
|
305
|
+
await this.save()
|
|
306
|
+
return f
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
async deleteFeedback(id) {
|
|
310
|
+
this.records.delete('feedback', id)
|
|
311
|
+
await this.save()
|
|
312
|
+
return { deleted: id }
|
|
313
|
+
},
|
|
314
|
+
}
|