@ty_krystal/sei-ai 0.1.5 → 0.1.8

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.
Files changed (53) hide show
  1. package/README.md +701 -175
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +5 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/commands/api-docs.d.ts +20 -0
  7. package/dist/commands/api-docs.js +4 -5
  8. package/dist/commands/call.d.ts +27 -0
  9. package/dist/commands/call.js +12 -13
  10. package/dist/commands/dict/add-category.d.ts +31 -0
  11. package/dist/commands/dict/add-category.js +9 -10
  12. package/dist/commands/dict/add-item.d.ts +32 -0
  13. package/dist/commands/dict/add-item.js +11 -12
  14. package/dist/commands/dict/list.d.ts +22 -0
  15. package/dist/commands/dict/list.js +8 -9
  16. package/dist/commands/init/base-data.d.ts +26 -0
  17. package/dist/commands/init/base-data.js +14 -15
  18. package/dist/commands/query-sql.d.ts +24 -0
  19. package/dist/commands/query-sql.js +6 -8
  20. package/dist/commands/query.d.ts +21 -0
  21. package/dist/commands/query.js +6 -7
  22. package/dist/commands/relogin.d.ts +17 -0
  23. package/dist/commands/relogin.js +2 -3
  24. package/dist/commands/save.d.ts +21 -0
  25. package/dist/commands/save.js +6 -7
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +1 -2
  28. package/oclif.manifest.json +1302 -0
  29. package/package.json +63 -54
  30. package/config.example.json +0 -33
  31. package/dist/README.md +0 -239
  32. package/dist/cli-actions.js +0 -157
  33. package/dist/cli-helpers.js +0 -246
  34. package/dist/command-base/context.js +0 -10
  35. package/dist/command-base/output.js +0 -6
  36. package/dist/command-base/payload.js +0 -33
  37. package/dist/command-base/sei-command.js +0 -88
  38. package/dist/commands/stdio.js +0 -16
  39. package/dist/commands/streamable-http.js +0 -29
  40. package/dist/config.example.json +0 -33
  41. package/dist/config.js +0 -82
  42. package/dist/constants.js +0 -48
  43. package/dist/env.js +0 -33
  44. package/dist/errors.js +0 -55
  45. package/dist/logger.js +0 -71
  46. package/dist/openapi.js +0 -261
  47. package/dist/permissions.js +0 -209
  48. package/dist/schema.js +0 -112
  49. package/dist/sei-client.js +0 -535
  50. package/dist/server.js +0 -237
  51. package/dist/tools.js +0 -175
  52. package/dist/types.js +0 -1
  53. package/dist/utils.js +0 -53
package/dist/errors.js DELETED
@@ -1,55 +0,0 @@
1
- export class SeiMcpError extends Error {
2
- code;
3
- details;
4
- constructor(message, code = 'INTERNAL_ERROR', details) {
5
- super(message);
6
- this.name = 'SeiMcpError';
7
- this.code = code;
8
- this.details = details;
9
- }
10
- }
11
- export function isSeiMcpError(error) {
12
- return error instanceof SeiMcpError;
13
- }
14
- export function normalizeErrorMessage(error) {
15
- return error instanceof Error ? error.message : 'Unknown error';
16
- }
17
- export function formatCliError(error) {
18
- if (error instanceof SeiMcpError) {
19
- return `[${error.code}] ${error.message}`;
20
- }
21
- return `[INTERNAL_ERROR] ${normalizeErrorMessage(error)}`;
22
- }
23
- export function toErrorLogMeta(error) {
24
- if (error instanceof SeiMcpError) {
25
- return {
26
- code: error.code,
27
- message: error.message,
28
- details: toJsonValue(error.details),
29
- };
30
- }
31
- if (error instanceof Error) {
32
- return {
33
- message: error.message,
34
- stack: error.stack ?? '',
35
- };
36
- }
37
- return {
38
- message: normalizeErrorMessage(error),
39
- };
40
- }
41
- function toJsonValue(value) {
42
- if (value === null) {
43
- return null;
44
- }
45
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
46
- return value;
47
- }
48
- if (Array.isArray(value)) {
49
- return value.map((item) => toJsonValue(item));
50
- }
51
- if (value && typeof value === 'object') {
52
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, toJsonValue(item)]));
53
- }
54
- return String(value);
55
- }
package/dist/logger.js DELETED
@@ -1,71 +0,0 @@
1
- import { DEFAULT_LOG_LEVEL, MAX_LOG_TEXT } from './constants.js';
2
- import { truncateText } from './utils.js';
3
- const LEVEL_PRIORITY = {
4
- error: 0,
5
- warn: 1,
6
- info: 2,
7
- debug: 3,
8
- };
9
- const SENSITIVE_KEYS = new Set([
10
- 'authorization',
11
- 'cookie',
12
- 'password',
13
- 'token',
14
- 'aiKey',
15
- 'ai_key',
16
- 'secret',
17
- 'body',
18
- ]);
19
- export function createLogger(level = DEFAULT_LOG_LEVEL) {
20
- const threshold = LEVEL_PRIORITY[normalizeLevel(level)] ?? LEVEL_PRIORITY.info;
21
- function write(targetLevel, message, meta) {
22
- if (LEVEL_PRIORITY[targetLevel] > threshold) {
23
- return;
24
- }
25
- const entry = {
26
- level: targetLevel,
27
- message: truncateText(message, MAX_LOG_TEXT),
28
- meta: sanitizeMeta(meta),
29
- time: new Date().toISOString(),
30
- };
31
- process.stderr.write(`${JSON.stringify(entry)}\n`);
32
- }
33
- return {
34
- debug(message, meta) {
35
- write('debug', message, meta);
36
- },
37
- info(message, meta) {
38
- write('info', message, meta);
39
- },
40
- warn(message, meta) {
41
- write('warn', message, meta);
42
- },
43
- error(message, meta) {
44
- write('error', message, meta);
45
- },
46
- };
47
- }
48
- function normalizeLevel(level) {
49
- const normalized = level.trim().toLowerCase();
50
- if (normalized === 'debug' || normalized === 'info' || normalized === 'warn' || normalized === 'error') {
51
- return normalized;
52
- }
53
- return 'info';
54
- }
55
- function sanitizeMeta(value) {
56
- if (Array.isArray(value)) {
57
- return value.map((item) => sanitizeMeta(item));
58
- }
59
- if (!value || typeof value !== 'object') {
60
- return value;
61
- }
62
- const result = {};
63
- for (const [key, item] of Object.entries(value)) {
64
- if (SENSITIVE_KEYS.has(key)) {
65
- result[key] = '[REDACTED]';
66
- continue;
67
- }
68
- result[key] = sanitizeMeta(item);
69
- }
70
- return result;
71
- }
package/dist/openapi.js DELETED
@@ -1,261 +0,0 @@
1
- import { SeiMcpError } from './errors.js';
2
- const OPENAPI_HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE'];
3
- export function buildOpenApiDocSummary(openapiDoc, keyword) {
4
- if (!isObject(openapiDoc)) {
5
- throw new SeiMcpError('OpenAPI 文档不是 JSON 对象', 'INVALID_REQUEST');
6
- }
7
- throwIfOpenApiErrorPayload(openapiDoc);
8
- const info = isObject(openapiDoc.info) ? openapiDoc.info : {};
9
- const rawPaths = openapiDoc.paths;
10
- if (!isObject(rawPaths)) {
11
- throw new SeiMcpError('OpenAPI 文档缺少 paths 对象', 'INVALID_REQUEST');
12
- }
13
- const normalizedKeyword = normalizeText(keyword).toLowerCase();
14
- const items = [];
15
- for (const [path, rawPathItem] of Object.entries(rawPaths)) {
16
- if (!isObject(rawPathItem)) {
17
- continue;
18
- }
19
- for (const [method, rawOperation] of Object.entries(rawPathItem)) {
20
- const methodUpper = method.toUpperCase();
21
- if (!OPENAPI_HTTP_METHODS.includes(methodUpper) || !isObject(rawOperation)) {
22
- continue;
23
- }
24
- const tags = Array.isArray(rawOperation.tags)
25
- ? rawOperation.tags.map((item) => normalizeText(item)).filter(Boolean)
26
- : [];
27
- const item = {
28
- method: methodUpper,
29
- path: normalizeText(path),
30
- tags,
31
- summary: normalizeText(rawOperation.summary),
32
- description: normalizeText(rawOperation.description),
33
- operationId: normalizeText(rawOperation.operationId),
34
- parameters: extractOpenApiParameters(rawOperation),
35
- requestBody: extractOpenApiRequestBody(rawOperation),
36
- responses: extractOpenApiResponses(rawOperation),
37
- };
38
- if (normalizedKeyword) {
39
- const searchable = JSON.stringify(item).toLowerCase();
40
- if (!searchable.includes(normalizedKeyword)) {
41
- continue;
42
- }
43
- }
44
- items.push(item);
45
- }
46
- }
47
- items.sort((a, b) => {
48
- const tagA = normalizeText((Array.isArray(a.tags) ? a.tags[0] : '') || '未分组');
49
- const tagB = normalizeText((Array.isArray(b.tags) ? b.tags[0] : '') || '未分组');
50
- if (tagA !== tagB) {
51
- return tagA.localeCompare(tagB, 'zh-CN');
52
- }
53
- const pathCompare = normalizeText(a.path).localeCompare(normalizeText(b.path), 'en');
54
- if (pathCompare !== 0) {
55
- return pathCompare;
56
- }
57
- return OPENAPI_HTTP_METHODS.indexOf(normalizeText(a.method)) -
58
- OPENAPI_HTTP_METHODS.indexOf(normalizeText(b.method));
59
- });
60
- return {
61
- title: normalizeText(info.title) || 'SEI API',
62
- version: normalizeText(info.version),
63
- openapi: normalizeText(openapiDoc.openapi ?? openapiDoc.swagger),
64
- count: items.length,
65
- items,
66
- };
67
- }
68
- export function renderOpenApiMarkdown(summary) {
69
- const lines = [`# ${normalizeText(summary.title) || 'SEI API'}`, ''];
70
- const version = normalizeText(summary.version);
71
- const openapiVersion = normalizeText(summary.openapi);
72
- if (version) {
73
- lines.push(`- 版本:${version}`);
74
- }
75
- if (openapiVersion) {
76
- lines.push(`- OpenAPI:${openapiVersion}`);
77
- }
78
- lines.push(`- 接口数:${Number(summary.count ?? 0)}`);
79
- let currentTag = '';
80
- const items = Array.isArray(summary.items) ? summary.items : [];
81
- for (const rawItem of items) {
82
- if (!isObject(rawItem)) {
83
- continue;
84
- }
85
- const tags = Array.isArray(rawItem.tags) ? rawItem.tags : [];
86
- const tag = normalizeText(tags[0] ?? '未分组') || '未分组';
87
- if (tag !== currentTag) {
88
- lines.push('', `## ${tag}`);
89
- currentTag = tag;
90
- }
91
- const method = normalizeText(rawItem.method);
92
- const path = normalizeText(rawItem.path);
93
- lines.push('', `### ${method} ${path}`);
94
- const summaryText = normalizeText(rawItem.summary);
95
- const description = normalizeText(rawItem.description);
96
- const operationId = normalizeText(rawItem.operationId);
97
- if (summaryText) {
98
- lines.push(`- 摘要:${summaryText}`);
99
- }
100
- if (description && description !== summaryText) {
101
- lines.push(`- 说明:${description}`);
102
- }
103
- if (operationId) {
104
- lines.push(`- operationId:\`${operationId}\``);
105
- }
106
- const parameters = Array.isArray(rawItem.parameters) ? rawItem.parameters : [];
107
- if (parameters.length > 0) {
108
- lines.push('- 参数:');
109
- for (const rawParameter of parameters) {
110
- if (!isObject(rawParameter)) {
111
- continue;
112
- }
113
- const name = normalizeText(rawParameter.name);
114
- const location = normalizeText(rawParameter.in);
115
- const schema = normalizeText(rawParameter.schema);
116
- const required = rawParameter.required ? '必填' : '可选';
117
- const descriptionText = normalizeText(rawParameter.description);
118
- const detail = [schema, required, descriptionText].filter(Boolean).join(',');
119
- lines.push(` - \`${location} ${name}\`${detail ? `:${detail}` : ''}`);
120
- }
121
- }
122
- const requestBody = Array.isArray(rawItem.requestBody) ? rawItem.requestBody : [];
123
- if (requestBody.length > 0) {
124
- const parts = requestBody
125
- .filter(isObject)
126
- .map((body) => {
127
- const contentType = normalizeText(body.contentType);
128
- const schema = normalizeText(body.schema);
129
- return schema ? `${contentType}(${schema})` : contentType;
130
- })
131
- .filter(Boolean);
132
- if (parts.length > 0) {
133
- lines.push(`- 请求体:${parts.join(', ')}`);
134
- }
135
- }
136
- const responses = Array.isArray(rawItem.responses) ? rawItem.responses : [];
137
- if (responses.length > 0) {
138
- const parts = responses
139
- .filter(isObject)
140
- .map((response) => `${normalizeText(response.status)} ${normalizeText(response.description)}`.trim())
141
- .filter(Boolean);
142
- if (parts.length > 0) {
143
- lines.push(`- 响应:${parts.join(', ')}`);
144
- }
145
- }
146
- }
147
- return `${lines.join('\n').trimEnd()}\n`;
148
- }
149
- function extractOpenApiParameters(operation) {
150
- const rawParameters = operation.parameters;
151
- if (!Array.isArray(rawParameters)) {
152
- return [];
153
- }
154
- return rawParameters
155
- .filter(isObject)
156
- .map((parameter) => ({
157
- name: normalizeText(parameter.name),
158
- in: normalizeText(parameter.in),
159
- required: Boolean(parameter.required),
160
- schema: summarizeSchema(parameter.schema),
161
- description: normalizeText(parameter.description),
162
- }))
163
- .filter((item) => item.name || item.in);
164
- }
165
- function extractOpenApiRequestBody(operation) {
166
- const requestBody = operation.requestBody;
167
- if (!isObject(requestBody) || !isObject(requestBody.content)) {
168
- return [];
169
- }
170
- return Object.entries(requestBody.content)
171
- .map(([contentType, contentInfo]) => ({
172
- contentType: normalizeText(contentType),
173
- schema: summarizeSchema(isObject(contentInfo) ? contentInfo.schema : undefined),
174
- }))
175
- .filter((item) => item.contentType);
176
- }
177
- function extractOpenApiResponses(operation) {
178
- const rawResponses = operation.responses;
179
- if (!isObject(rawResponses)) {
180
- return [];
181
- }
182
- return Object.entries(rawResponses)
183
- .map(([status, response]) => ({
184
- status: normalizeText(status),
185
- description: normalizeText(isObject(response) ? response.description : ''),
186
- }))
187
- .filter((item) => item.status);
188
- }
189
- function summarizeSchema(schema) {
190
- if (!isObject(schema)) {
191
- return '';
192
- }
193
- const ref = normalizeText(schema.$ref);
194
- if (ref) {
195
- return ref.split('/').pop() ?? ref;
196
- }
197
- const schemaType = normalizeText(schema.type);
198
- const schemaFormat = normalizeText(schema.format);
199
- if (schemaType === 'array') {
200
- const itemText = summarizeSchema(schema.items);
201
- return `array<${itemText || 'item'}>`;
202
- }
203
- if (schemaType && schemaFormat) {
204
- return `${schemaType}/${schemaFormat}`;
205
- }
206
- if (schemaType) {
207
- return schemaType;
208
- }
209
- return Array.isArray(schema.enum) && schema.enum.length > 0 ? 'enum' : '';
210
- }
211
- function throwIfOpenApiErrorPayload(openapiDoc) {
212
- const rootCause = isObject(openapiDoc.rootCause) ? openapiDoc.rootCause : null;
213
- const stackTrace = Array.isArray(rootCause?.stackTrace) ? rootCause.stackTrace : [];
214
- const topFrame = stackTrace.find(isObject);
215
- const rootMessage = normalizeText(rootCause?.message);
216
- const rootException = normalizeText(rootCause?.exception);
217
- const message = normalizeText(openapiDoc.message);
218
- const error = normalizeText(openapiDoc.error);
219
- const path = normalizeText(openapiDoc.path);
220
- if (!rootCause && !message && !error) {
221
- return;
222
- }
223
- const parts = [
224
- message || error,
225
- rootException,
226
- rootMessage,
227
- formatStackFrame(topFrame),
228
- path ? `path=${path}` : '',
229
- ].filter(Boolean);
230
- throw new SeiMcpError(parts.length > 0
231
- ? `OpenAPI 文档生成失败:${parts.join(' | ')}`
232
- : 'OpenAPI 文档生成失败,服务端返回异常对象', 'INVALID_REQUEST', {
233
- error,
234
- message,
235
- path,
236
- rootCause: rootCause ?? undefined,
237
- });
238
- }
239
- function formatStackFrame(frame) {
240
- if (!isObject(frame)) {
241
- return '';
242
- }
243
- const className = normalizeText(frame.className);
244
- const methodName = normalizeText(frame.methodName);
245
- const fileName = normalizeText(frame.fileName);
246
- const lineNumber = Number(frame.lineNumber);
247
- const locationParts = [className, methodName].filter(Boolean);
248
- const sourceParts = [fileName, Number.isFinite(lineNumber) && lineNumber > 0 ? String(lineNumber) : ''].filter(Boolean);
249
- const location = locationParts.join('#');
250
- const source = sourceParts.join(':');
251
- if (location && source) {
252
- return `${location} (${source})`;
253
- }
254
- return location || source;
255
- }
256
- function normalizeText(value) {
257
- return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
258
- }
259
- function isObject(value) {
260
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
261
- }
@@ -1,209 +0,0 @@
1
- import { DEFAULT_QUERY_LIMIT, DEFAULT_QUERY_SQL_PAGE, DEFAULT_QUERY_SQL_SIZE } from './constants.js';
2
- import { SeiMcpError } from './errors.js';
3
- import { isObject, toTrimmedString, uniqueStrings } from './utils.js';
4
- const RESERVED_SAVE_ROW_KEYS = new Set(['keyVal', 'filter', 'log', 'attFile', 'serverEvn', 'row']);
5
- export function resolveTarget(config, head) {
6
- const entries = Object.entries(head).filter(([, value]) => toTrimmedString(value));
7
- if (entries.length !== 1) {
8
- throw new SeiMcpError('head 必须且只能包含一个目标: module/source/table/view', 'INVALID_PARAMS');
9
- }
10
- const [kind, rawName] = entries[0];
11
- if (kind !== 'module' && kind !== 'source' && kind !== 'table' && kind !== 'view') {
12
- throw new SeiMcpError('head 必须且只能包含一个目标: module/source/table/view', 'INVALID_PARAMS');
13
- }
14
- const name = toTrimmedString(rawName);
15
- const pool = config.targets[`${kind}s`];
16
- const target = pool[name];
17
- if (!target) {
18
- throw new SeiMcpError(`目标未配置或未授权: ${kind}.${name}`, 'INVALID_PARAMS');
19
- }
20
- return { kind, name, target };
21
- }
22
- export function authorizeQuery(config, input) {
23
- const head = requireHead(input.head);
24
- const { target, kind, name } = resolveTarget(config, head);
25
- const queryConfig = target.query ?? {};
26
- const requestedFields = normalizeList(input.option?.fields);
27
- const allowedFields = normalizeList(queryConfig.fields);
28
- const defaultFields = normalizeList(queryConfig.defaultFields);
29
- let effectiveFields = requestedFields;
30
- if (effectiveFields.length === 0) {
31
- if (defaultFields.length > 0) {
32
- effectiveFields = defaultFields;
33
- }
34
- else if (queryConfig.allowAllFields === true) {
35
- effectiveFields = ['*'];
36
- }
37
- else {
38
- throw new SeiMcpError('query 需要显式字段或默认字段配置', 'INVALID_PARAMS');
39
- }
40
- }
41
- if (effectiveFields.includes('*') && queryConfig.allowAllFields !== true) {
42
- throw new SeiMcpError('query 的字段通配符未被白名单允许', 'INVALID_PARAMS');
43
- }
44
- if (!effectiveFields.includes('*')) {
45
- ensureFieldsAllowed(effectiveFields, allowedFields, 'query');
46
- }
47
- return {
48
- head,
49
- option: {
50
- ...(isObject(input.option) ? input.option : {}),
51
- fields: effectiveFields.join(','),
52
- limit: toPositiveInteger(input.option?.limit, DEFAULT_QUERY_LIMIT),
53
- },
54
- _target: {
55
- kind,
56
- name,
57
- },
58
- };
59
- }
60
- export function authorizeSave(config, input) {
61
- const head = requireHead(input.head);
62
- const { kind, name, target } = resolveTarget(config, head);
63
- const saveConfig = target.save ?? {};
64
- const allowedActions = normalizeActions(saveConfig.actions);
65
- const allowedFields = normalizeList(saveConfig.fields);
66
- const data = Array.isArray(input.data) ? input.data : [];
67
- if (data.length === 0) {
68
- throw new SeiMcpError('save 需要 data 数组', 'INVALID_PARAMS');
69
- }
70
- for (const item of data) {
71
- const action = normalizeAction(item?.action);
72
- if (!allowedActions.includes(action)) {
73
- throw new SeiMcpError(`save 动作不在白名单内: ${action}`, 'INVALID_PARAMS');
74
- }
75
- for (const row of Array.isArray(item?.rows) ? item.rows : []) {
76
- const current = isObject(row?.row) ? row.row : {};
77
- const fieldKeys = Object.keys(current).filter((key) => !RESERVED_SAVE_ROW_KEYS.has(key));
78
- ensureFieldsAllowed(fieldKeys, allowedFields, 'save');
79
- }
80
- }
81
- return {
82
- head,
83
- data,
84
- query: input.query,
85
- _target: {
86
- kind,
87
- name,
88
- },
89
- };
90
- }
91
- export function authorizeSchema(config, input) {
92
- const head = requireHead(input.head);
93
- const target = resolveTarget(config, head);
94
- return {
95
- head,
96
- _target: target,
97
- };
98
- }
99
- export function authorizeQuerySql(config, input) {
100
- if (config.tools.querySql !== true) {
101
- throw new SeiMcpError('querySql 工具未启用', 'INVALID_PARAMS');
102
- }
103
- const sql = toTrimmedString(input.sql);
104
- if (!sql) {
105
- throw new SeiMcpError('sql is required', 'INVALID_PARAMS');
106
- }
107
- if (!isSafeSelectSql(sql)) {
108
- throw new SeiMcpError('querySql 只允许单条 SELECT/WITH 查询', 'INVALID_PARAMS');
109
- }
110
- const tables = extractTableNames(sql);
111
- if (tables.some((table) => !isTargetAllowed(config, table))) {
112
- throw new SeiMcpError('querySql 引用了未授权的表', 'INVALID_PARAMS');
113
- }
114
- if (/\bselect\s+\*/i.test(sql)) {
115
- throw new SeiMcpError('querySql 不允许使用 *', 'INVALID_PARAMS');
116
- }
117
- return {
118
- sql,
119
- page: toPositiveInteger(input.page, DEFAULT_QUERY_SQL_PAGE),
120
- size: toPositiveInteger(input.size, DEFAULT_QUERY_SQL_SIZE),
121
- };
122
- }
123
- function requireHead(head) {
124
- if (!isObject(head) || Array.isArray(head)) {
125
- throw new SeiMcpError('head is required', 'INVALID_PARAMS');
126
- }
127
- const result = {};
128
- for (const [key, value] of Object.entries(head)) {
129
- const trimmed = toTrimmedString(value);
130
- if (trimmed) {
131
- result[key] = trimmed;
132
- }
133
- }
134
- return result;
135
- }
136
- function normalizeList(value) {
137
- if (Array.isArray(value)) {
138
- return uniqueStrings(value.map((item) => toTrimmedString(item)));
139
- }
140
- if (typeof value === 'string') {
141
- return uniqueStrings(value.split(',').map((item) => item.trim()));
142
- }
143
- return [];
144
- }
145
- function normalizeActions(value) {
146
- return normalizeList(value).map(normalizeAction);
147
- }
148
- function normalizeAction(value) {
149
- return toTrimmedString(value).toLowerCase();
150
- }
151
- function ensureFieldsAllowed(requested, allowed, label) {
152
- if (allowed.length === 0) {
153
- throw new SeiMcpError(`${label} 未配置字段白名单`, 'INVALID_PARAMS');
154
- }
155
- const allowedSet = new Set(allowed);
156
- const blocked = requested.filter((field) => !allowedSet.has(field));
157
- if (blocked.length > 0) {
158
- throw new SeiMcpError(`${label} 字段未授权: ${blocked.join(', ')}`, 'INVALID_PARAMS');
159
- }
160
- }
161
- function toPositiveInteger(value, fallback) {
162
- const number = Number(value);
163
- return Number.isInteger(number) && number > 0 ? number : fallback;
164
- }
165
- function isTargetAllowed(config, tableName) {
166
- const normalized = toTrimmedString(tableName);
167
- if (!normalized) {
168
- return false;
169
- }
170
- const pools = [config.targets.tables, config.targets.sources, config.targets.views, config.targets.modules];
171
- for (const pool of pools) {
172
- if (pool[normalized]) {
173
- return true;
174
- }
175
- for (const target of Object.values(pool)) {
176
- if (isObject(target)) {
177
- const mainTable = toTrimmedString(target.mainTable);
178
- if (mainTable && mainTable === normalized) {
179
- return true;
180
- }
181
- }
182
- }
183
- }
184
- return false;
185
- }
186
- function isSafeSelectSql(sql) {
187
- const normalized = stripSqlComments(sql).trim();
188
- if (normalized.includes(';')) {
189
- return false;
190
- }
191
- return /^(with\s+[\s\S]+?\s+select\b|select\b)/i.test(normalized);
192
- }
193
- function extractTableNames(sql) {
194
- const normalized = stripSqlComments(sql);
195
- const tables = [];
196
- const regex = /\b(from|join)\s+([`"\[]?)([A-Za-z0-9_.$]+)\2/gi;
197
- for (const match of normalized.matchAll(regex)) {
198
- const table = match[3];
199
- if (table && table.toLowerCase() !== 'select') {
200
- tables.push(table);
201
- }
202
- }
203
- return tables;
204
- }
205
- function stripSqlComments(sql) {
206
- return sql
207
- .replace(/\/\*[\s\S]*?\*\//g, ' ')
208
- .replace(/--.*$/gm, ' ');
209
- }